Skip to content
Open
Show file tree
Hide file tree
Changes from 6 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" />;
};
59 changes: 18 additions & 41 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 @@ -15,8 +14,9 @@ import {
EnsembleMenuModelType,
} from "@ensembleui/react-framework";
import { Outlet, Link, useLocation } from "react-router-dom";
import { cloneDeep, omit } from "lodash-es";
import { cloneDeep, isString, omit } from "lodash-es";
import { getColor } from "../shared/styles";
import type { IconProps } from "../shared/types";
import { EnsembleRuntime } from "./runtime";
// eslint-disable-next-line import/no-cycle
import { useEnsembleAction } from "./hooks";
Expand Down Expand Up @@ -48,8 +48,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 +98,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 +335,26 @@ 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 = isString(iconProps)
? {
name: iconProps,
styles: { width: styles.iconWidth, height: styles.iconHeight },
}
: iconProps;

return EnsembleRuntime.render([{ ...unwrapWidget({ Icon: icon }), key }]);
},
[styles.iconHeight, styles.iconWidth, selectedItem],
);
Expand Down Expand Up @@ -425,14 +401,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.

Loading