Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/rich-avocados-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@ensembleui/react-framework": patch
"@ensembleui/react-kitchen-sink": patch
"@ensembleui/react-runtime": patch
---

reduce runtime bundle size by modularize icons
2 changes: 2 additions & 0 deletions apps/kitchen-sink/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"dependencies": {
"@ensembleui/react-framework": "workspace:*",
"@ensembleui/react-runtime": "workspace:*",
"@mui/icons-material": "^6.4.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"web-vitals": "^2.1.4"
Expand All @@ -22,6 +23,7 @@
"https-browserify": "^1.0.0",
"node-polyfill-webpack-plugin": "^2.0.1",
"path-browserify": "^1.0.1",
"react-feather": "^2.0.10",
"react-scripts": "5.0.1",
"stream-browserify": "^3.0.0",
"stream-http": "^3.2.0",
Expand Down
8 changes: 8 additions & 0 deletions apps/kitchen-sink/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { ApplicationDTO } from "@ensembleui/react-framework";
import { EnsembleApp } from "@ensembleui/react-runtime";
import * as Icons from "react-feather";
// Screens
import React from "react";
import MenuYAML from "./ensemble/screens/menu.yaml";
import HomeYAML from "./ensemble/screens/home.yaml";
import WidgetsYAML from "./ensemble/screens/widgets.yaml";
Expand Down Expand Up @@ -143,6 +145,12 @@ const testApp: ApplicationDTO = {
},
],
config: EnsembleConfig,
icons: [
{
icons: Icons,
prefix: "Fe",
},
],
};

const App: React.FC = () => {
Expand Down
2 changes: 1 addition & 1 deletion apps/kitchen-sink/src/ensemble/screens/menu.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ ViewGroup:
source: /logo.svg
items:
- label: Home
icon: HomeOutlined
icon: FeHome
page: home
selected: true
- label: Widgets
Expand Down
1 change: 1 addition & 0 deletions apps/preview/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"dependencies": {
"@ensembleui/react-framework": "workspace:*",
"@ensembleui/react-runtime": "workspace:*",
"@mui/icons-material": "^6.4.1",
"antd": "^5.9.0",
"firebase": "9.10.0",
"lodash-es": "^4.17.21",
Expand Down
1 change: 1 addition & 0 deletions apps/starter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"dependencies": {
"@ensembleui/react-framework": "workspace:*",
"@ensembleui/react-runtime": "workspace:*",
"@mui/icons-material": "^6.4.1",
"firebase": "9.10.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
7 changes: 6 additions & 1 deletion packages/framework/src/shared/dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface ApplicationDTO extends Omit<EnsembleDocument, "content"> {
readonly languages?: LanguageDTO[];
readonly config?: string | EnsembleConfigYAML;
readonly fonts?: FontDTO[];

readonly icons?: IconDTO[];
readonly description?: string;
readonly isPublic?: boolean;
readonly isAutoGenerated?: boolean;
Expand Down Expand Up @@ -63,3 +63,8 @@ export interface FontDTO {
readonly fontWeight: string;
readonly fontStyle: string;
}

export interface IconDTO {
readonly prefix?: string;
readonly icons?: { [key: string]: unknown };
}
3 changes: 2 additions & 1 deletion packages/runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
},
"peerDependencies": {
"@ensembleui/react-framework": "*",
"@mui/icons-material": "^6.4.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
Expand All @@ -27,7 +28,7 @@
"@emotion/styled": "^11.11.0",
"@ensembleui/react-framework": "workspace:*",
"@lottiefiles/react-lottie-player": "^3.5.3",
"@mui/icons-material": "^5.14.9",
"@mui/icons-material": "^6.4.1",
"@mui/material": "^5.14.9",
"@mui/x-date-pickers": "^6.18.3",
"@react-oauth/google": "^0.12.1",
Expand Down
21 changes: 19 additions & 2 deletions packages/runtime/src/EnsembleApp.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useEffect, useMemo, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import type {
ApplicationDTO,
EnsembleAppModel,
ApplicationLoader,
IconDTO,
} from "@ensembleui/react-framework";
import {
ApplicationContextProvider,
Expand All @@ -13,13 +14,15 @@ import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { ToastContainer } from "react-toastify";
import { injectStyle } from "react-toastify/dist/inject-style";
import { QueryClientProvider } from "@tanstack/react-query";
import * as Icons from "@mui/icons-material";
import { ThemeProvider } from "./ThemeProvider";
import { EnsembleEntry } from "./runtime/entry";
import { EnsembleScreen } from "./runtime/screen";
import { ErrorPage } from "./runtime/error";
// Register built in widgets;
import "./widgets";
import { WidgetRegistry } from "./registry";
import type { WidgetComponent } from "./registry";
import { IconRegistry, WidgetRegistry } from "./registry";
import { createCustomWidget } from "./runtime/customWidget";
import { ModalWrapper } from "./runtime/modal";

Expand Down Expand Up @@ -58,6 +61,20 @@ export const EnsembleApp: React.FC<EnsembleAppProps> = ({
);
});

Object.entries(Icons).forEach(([name, icon]) => {
IconRegistry.register(name, icon as WidgetComponent<unknown>);
});

appDto?.icons?.forEach((iconSet: IconDTO) => {
const { prefix = "", icons = {} } = iconSet;
Object.entries(icons).forEach(([name, icon]) => {
IconRegistry.register(
`${prefix}${name}`,
icon as WidgetComponent<unknown>,
);
});
});

setApp(parsedApp);
};

Expand Down
28 changes: 27 additions & 1 deletion packages/runtime/src/registry.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { Alert } from "antd";
import type { ReactElement } from "react";

export type WidgetComponent<T> = React.FC<T>;
export type WidgetComponent<T> = React.ComponentType<T>;

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const registry: { [key: string]: WidgetComponent<any> | undefined } = {};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const iconRegistry: { [key: string]: WidgetComponent<any> | undefined } = {};

export const WidgetRegistry = {
register: <T,>(name: string, component: WidgetComponent<T>): void => {
registry[name] = component;
Expand All @@ -25,6 +28,29 @@ export const WidgetRegistry = {
},
};

export const IconRegistry = {
register: <T,>(name: string, component: WidgetComponent<T>): void => {
iconRegistry[name] = component;
},
find: (name: string): WidgetComponent<any> | ReactElement => {
const Icon = iconRegistry[name];
if (!Icon) {
return <UnknownIcon missingName={name} />;
}
return Icon;
},
findOrNull: (name: string): WidgetComponent<any> | null => {
return iconRegistry[name] || null;
},
unregister: (name: string): void => {
delete iconRegistry[name];
},
};

const UnknownWidget: React.FC<{ missingName: string }> = ({ missingName }) => {
return <Alert message={`Unknown widget: ${missingName}`} type="error" />;
};

const UnknownIcon: React.FC<{ missingName: string }> = ({ missingName }) => {
return <Alert message={`Unknown icon: ${missingName}`} type="error" />;
};
55 changes: 15 additions & 40 deletions packages/runtime/src/runtime/menu.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import type { PropsWithChildren, ReactNode } from "react";
import type { PropsWithChildren } from "react";
import React, { useState, useEffect, useCallback, useMemo } from "react";
import {
Menu as AntMenu,
Col,
Drawer as AntDrawer,
ConfigProvider,
} from "antd";
import * as MuiIcons from "@mui/icons-material";
import {
unwrapWidget,
useRegisterBindings,
Expand All @@ -17,6 +16,8 @@ import {
import { Outlet, Link, useLocation } from "react-router-dom";
import { cloneDeep, omit } from "lodash-es";
import { getColor } from "../shared/styles";
import type { IconProps } from "../shared/types";
import { normalizeIconProps } from "../shared/utils";
import { EnsembleRuntime } from "./runtime";
// eslint-disable-next-line import/no-cycle
import { useEnsembleAction } from "./hooks";
Expand Down Expand Up @@ -48,8 +49,8 @@ export interface EnsembleMenuContext {
interface MenuItemProps {
id?: string;
testId?: string;
icon?: string | { [key: string]: unknown };
activeIcon?: string | { [key: string]: unknown };
icon?: string | IconProps;
activeIcon?: string | IconProps;
iconLibrary?: "default" | "fontAwesome";
label?: string;
url?: string;
Expand Down Expand Up @@ -98,29 +99,6 @@ interface DrawerMenuStyles extends MenuStyles {
position?: "left" | "right" | "top" | "bottom";
}

const renderMuiIcon = (
iconName?: string,
width = "15px",
height = "15px",
): ReactNode => {
if (!iconName) {
return null;
}

const MuiIconComponent = MuiIcons[iconName as keyof typeof MuiIcons];
if (MuiIconComponent) {
return (
<MuiIconComponent
style={{
width,
height,
}}
/>
);
}
return null;
};

const CustomLink: React.FC<PropsWithChildren & { item: MenuItemProps }> = ({
item,
children,
Expand Down Expand Up @@ -358,27 +336,23 @@ const MenuItems: React.FC<{
setSelectedItem,
isCollapsed = false,
}) => {
const getIcon = useCallback(
const getCustomIcon = useCallback(
(item: MenuItemProps) => {
const key = selectedItem === item.page ? "activeIcon" : "icon";
const icon =
const iconProps =
selectedItem === item.page && item.activeIcon
? item.activeIcon
: item.icon;

if (!icon) {
if (!iconProps) {
return null;
}

if (typeof icon === "string") {
return renderMuiIcon(icon, styles.iconWidth, styles.iconHeight);
}
return EnsembleRuntime.render([
{
...unwrapWidget({ Icon: icon }),
key,
},
]);
const icon = normalizeIconProps(iconProps, {
styles: { width: styles.iconWidth, height: styles.iconHeight },
});

return EnsembleRuntime.render([{ ...unwrapWidget({ Icon: icon }), key }]);
},
[styles.iconHeight, styles.iconWidth, selectedItem],
);
Expand Down Expand Up @@ -425,14 +399,15 @@ const MenuItems: React.FC<{
{items.map((item, itemIndex) => (
<AntMenu.Item
data-testid={item.id ?? item.testId}
icon={getIcon(item)}
icon={getCustomIcon(item)}
key={item.page || item.url || `customItem${itemIndex}`}
onClick={(): void => {
if (!item.openNewTab && item.page) {
setSelectedItem(item.page);
}
}}
style={{
gap: "10px",
color:
selectedItem === item.page
? (styles.selectedColor as string) ?? "white"
Expand Down
12 changes: 7 additions & 5 deletions packages/runtime/src/runtime/modal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import {
useState,
} from "react";
import { createPortal } from "react-dom";
import OpenInFullIcon from "@mui/icons-material/OpenInFull";
import CloseFullscreenIcon from "@mui/icons-material/CloseFullscreen";
import { CloseOutlined } from "@ant-design/icons";
import {
CloseOutlined,
FullscreenOutlined,
FullscreenExitOutlined,
} from "@ant-design/icons";
import { generateRandomString, useEvaluate } from "@ensembleui/react-framework";
import { isString, omit, pick } from "lodash-es";
import { useNavigate } from "react-router-dom";
Expand Down Expand Up @@ -253,7 +255,7 @@ export const ModalWrapper: React.FC<PropsWithChildren> = ({ children }) => {

const getFullScreenIcon = (index: number): React.ReactNode =>
isFullScreen[index] ? (
<CloseFullscreenIcon
<FullscreenExitOutlined
onClick={(): void =>
setIsFullScreen((oldIsFullScreen) => {
const newIsFullScreen = [...oldIsFullScreen];
Expand All @@ -264,7 +266,7 @@ export const ModalWrapper: React.FC<PropsWithChildren> = ({ children }) => {
style={iconStyles}
/>
) : (
<OpenInFullIcon
<FullscreenOutlined
onClick={(): void =>
setIsFullScreen((oldIsFullScreen) => {
const newIsFullScreen = [...oldIsFullScreen];
Expand Down
20 changes: 0 additions & 20 deletions packages/runtime/src/shared/icons.tsx

This file was deleted.

13 changes: 7 additions & 6 deletions packages/runtime/src/shared/styles.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import * as Icons from "@mui/icons-material";
import type { SvgIconComponent } from "@mui/icons-material";
import { get, isInteger } from "lodash-es";
import React from "react";
import { TextAlignment } from "./styleSchema";
import type React from "react";
import { IconRegistry } from "../registry";
import type { TextAlignment } from "./styleSchema";

type Color = number | string;

Expand Down Expand Up @@ -97,8 +96,10 @@ export const getCrossAxis = (crossAxis: string): string | undefined => {
}
};

export const getIcon = (name: string): SvgIconComponent | undefined => {
return get(Icons, name) as SvgIconComponent;
export const getIcon = <T = React.ComponentType<any>>(
name: string,
): T | undefined => {
return IconRegistry.find(name) as T | undefined;
};

export const getComponentStyles = (
Expand Down
Loading