From ce60aa1ed2204e274f05c37bc9a0d381611f5335 Mon Sep 17 00:00:00 2001 From: Zohar Manor-Abel Date: Tue, 6 Oct 2026 17:33:11 +0100 Subject: [PATCH] Introduce Lucide based Icon component and updates - Added `LucideIcon` and `iconSizes`, with icons built via `createIcon` - Exported the icons from the package entry; they stay tree-shakeable - `DiamondDSTheme` uses Lucide by default for `Alert`, `Select`, `NativeSelect`, `Autocomplete`, `Chip` - Components and stories now use the Lucide icons, including the `SidebarNav` story - `lucide-react` is a new peer dependency (`^1.35.0`) - Removed `react-icons` and the `@mui/icons-material` peer and dev dependencies - Updated README, installation docs and changelog --- changelog.md | 7 + dev/src/App.tsx | 135 ++++++++++++++++ package.json | 9 +- pnpm-lock.yaml | 25 +-- readme.md | 13 +- rollup.config.mjs | 2 +- .../DataDisplay/Icons/LucideIcon.test.tsx | 31 ++++ .../DataDisplay/Icons/LucideIcon.tsx | 50 ++++++ src/components/DataDisplay/Icons/iconSizes.ts | 9 ++ src/components/DataDisplay/Icons/icons.tsx | 143 +++++++++++++++++ src/components/DataDisplay/Icons/index.ts | 7 + .../DataDisplay/LucideIcon.stories.tsx | 114 ++++++++++++++ .../MUI/DataDisplay/Badge.stories.tsx | 10 +- .../MUI/Feedback/Tooltip.stories.tsx | 2 +- src/components/MUI/MuiWrapped.tsx | 145 ++++-------------- .../controls/ColourSchemeButton.test.tsx | 3 +- .../controls/ColourSchemeButton.tsx | 10 +- src/components/controls/ScrollableImages.tsx | 18 ++- src/components/controls/User.stories.tsx | 17 +- src/components/controls/User.tsx | 17 +- src/components/navigation/Breadcrumbs.tsx | 5 +- src/components/navigation/NavMenu.stories.tsx | 5 +- src/components/navigation/NavMenu.tsx | 4 +- src/components/navigation/Navbar.tsx | 4 +- .../navigation/SidebarNav.stories.tsx | 55 +++---- src/index.ts | 3 + src/storybook/Installation.mdx | 8 +- src/storybook/foundation/3. icons.mdx | 3 +- src/themes/DiamondDSTheme.test.ts | 18 +++ src/themes/DiamondDSTheme.ts | 35 +++++ 30 files changed, 687 insertions(+), 220 deletions(-) create mode 100644 src/components/DataDisplay/Icons/LucideIcon.test.tsx create mode 100644 src/components/DataDisplay/Icons/LucideIcon.tsx create mode 100644 src/components/DataDisplay/Icons/iconSizes.ts create mode 100644 src/components/DataDisplay/Icons/icons.tsx create mode 100644 src/components/DataDisplay/Icons/index.ts create mode 100644 src/components/DataDisplay/LucideIcon.stories.tsx diff --git a/changelog.md b/changelog.md index c9ade83c..fd05c4b5 100644 --- a/changelog.md +++ b/changelog.md @@ -2,8 +2,15 @@ ## [Unreleased] - TBD +### Added + +- Exported Lucide-based icons (`HomeIcon`, `SettingsIcon` and others), `LucideIcon` and the `iconSizes` scale. Icons remain tree-shakeable. + ### Changed +- **Breaking** Library icons now use Lucide. `lucide-react` is a new peer dependency (`^1.35.0`). `@mui/icons-material` and `react-icons` are no longer required unless used elsewhere in your app. JsonForms users still need `@mui/icons-material` for `@jsonforms/material-renderers`. +- `Alert`, `Select`, `NativeSelect`, `Autocomplete` and `Chip` now use Lucide icons by default through `DiamondDSTheme`. +- `ColourSchemeButton` test ids are now `SunIcon` and `MoonIcon` instead of `LightModeIcon` and `BedtimeIcon`. - _Dialog_ now defaults to elevation 16 (`surface.strong`'s tone) instead of MUI's 24, and temporary _Drawer_ to elevation 4 instead of 16, so floating surfaces order Drawer < Menu < Dialog and dialog text keeps more contrast. - **Breaking** _Dialog_'s deprecated `PaperProps` is now ignored. Move it to `slotProps.paper`, e.g. with `npx @mui/codemod@latest deprecations/dialog-props `. If you use MRT, also check `muiEditRowDialogProps` and `muiCreateRowModalProps` by hand, as the codemod won't reach them. diff --git a/dev/src/App.tsx b/dev/src/App.tsx index 6d1d252b..5ecba58e 100644 --- a/dev/src/App.tsx +++ b/dev/src/App.tsx @@ -1,14 +1,19 @@ import * as React from "react"; import { + Alert, AppBar, + Autocomplete, Box, Checkbox, Chip, Container, IconButton, Link, + MenuItem, Paper, + Select, Stack, + TextField, Toolbar, Typography, Table, @@ -24,6 +29,7 @@ import { Layers as LayersIcon, Menu as MenuIcon, Palette as PaletteIcon, + Shapes as ShapesIcon, Type as TypeIcon, } from "lucide-react"; import { @@ -56,6 +62,8 @@ import { ColourSchemeButton } from "../../src/components/controls/ColourSchemeBu import { Breadcrumbs } from "../../src/components/navigation/Breadcrumbs"; import { Bar } from "../../src/components/controls/Bar"; import { Logo } from "../../src/components/controls/Logo"; +import * as Icons from "../../src/components/DataDisplay/Icons"; +import { iconSizes } from "../../src/components/DataDisplay/Icons"; /* TABLE */ @@ -273,6 +281,11 @@ const navigation: Navigation = [ icon: , linkProps: { to: "/typography", component: NavLink }, }, + { + label: "Icons", + icon: , + linkProps: { to: "/icons", component: NavLink }, + }, { label: "Elevation", icon: , @@ -335,6 +348,7 @@ const AppLayout = () => { } /> } /> } /> + } /> } /> } /> @@ -1103,4 +1117,125 @@ const TypographyPage = () => { ); }; +const libraryIcons = Object.entries(Icons).filter( + ([name]) => name.endsWith("Icon") && name !== "LucideIcon", +) as [string, typeof Icons.HomeIcon][]; + +const IconsPage = () => { + return ( + + + Icons + + Lucide icons wrapped in MUI's SvgIcon. Toggle + light/dark mode to check colour inheritance. + + + + + + Size scale + + + {(Object.keys(iconSizes) as (keyof typeof iconSizes)[]).map( + (size) => ( + + + + {size} ({iconSizes[size].size}px) + + + ), + )} + + + + + + MUI fontSize + + + {(["small", "medium", "large"] as const).map((fontSize) => ( + + + {fontSize} + + ))} + + + + + inherit (32px parent) + + + + + + + Library icons + + + {libraryIcons.map(([name, Icon]) => ( + + + {name} + + ))} + + + + + + Theme-driven icons + + + + {(["success", "info", "warning", "error"] as const).map( + (severity) => ( + {}}> + {severity} + + ), + )} + + + + } + /> + {}} /> + + + + + ); +}; + export default App; diff --git a/package.json b/package.json index 632b5e34..b8b89ce5 100644 --- a/package.json +++ b/package.json @@ -68,11 +68,10 @@ "@jsonforms/core": "^3.7.0", "@jsonforms/material-renderers": "^3.7.0", "@jsonforms/react": "^3.7.0", - "@mui/icons-material": "^7.0.0", "@mui/material": "^7.0.0", "keycloak-js": "^26.2.1", - "react": "^18.3.1", - "react-icons": "^5.3.0" + "lucide-react": "^1.35.0", + "react": "^18.3.1" }, "devDependencies": { "@babel/core": "^7.26.10", @@ -90,7 +89,6 @@ "@jsonforms/core": "^3.7.0", "@jsonforms/material-renderers": "^3.7.0", "@jsonforms/react": "^3.7.0", - "@mui/icons-material": "^7.0.0", "@mui/material": "^7.0.0", "@rollup/plugin-commonjs": "^29.0.3", "@rollup/plugin-image": "^3.0.3", @@ -122,11 +120,10 @@ "gh-pages": "^6.2.0", "jsdom": "^29.1.1", "keycloak-js": "^26.2.1", - "lucide-react": "^1.26.0", + "lucide-react": "^1.35.0", "postcss-url": "^10.1.4", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-icons": "^5.3.0", "react-router-dom": "^7.18.2", "rollup": "^4.27.3", "rollup-plugin-dts": "^6.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bc78337b..fc8193b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -85,9 +85,6 @@ importers: '@jsonforms/react': specifier: ^3.7.0 version: 3.8.0(@jsonforms/core@3.8.0)(react@18.3.1) - '@mui/icons-material': - specifier: ^7.0.0 - version: 7.3.11(@mui/material@7.3.11(@emotion/react@11.14.0(@types/react@18.3.31)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.31)(react@18.3.1))(@types/react@18.3.31)(react@18.3.1))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(@types/react@18.3.31)(react@18.3.1) '@mui/material': specifier: ^7.0.0 version: 7.3.11(@emotion/react@11.14.0(@types/react@18.3.31)(react@18.3.1))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.31)(react@18.3.1))(@types/react@18.3.31)(react@18.3.1))(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -179,8 +176,8 @@ importers: specifier: ^29.1.1 version: 29.1.1 lucide-react: - specifier: ^1.26.0 - version: 1.26.0(react@18.3.1) + specifier: ^1.35.0 + version: 1.48.0(react@18.3.1) postcss-url: specifier: ^10.1.4 version: 10.1.4(postcss@8.5.15) @@ -190,9 +187,6 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) - react-icons: - specifier: ^5.3.0 - version: 5.7.0(react@18.3.1) react-router-dom: specifier: ^7.18.2 version: 7.18.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -3624,8 +3618,8 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - lucide-react@1.26.0: - resolution: {integrity: sha512-raglYVR2+VkMfJL158krjVmE+rV5ST2lzA/KQm1FRSjMHT4MnWaegHxoVEpmc2So3nOEhp9oGejJwAPX8MoAjg==} + lucide-react@1.48.0: + resolution: {integrity: sha512-R0CIKY/fXiC6y9xRBADgsK+VW2p/pcTJOMhLZf1T+uG+vJUvyUR02nGOgmIIC7MPkiNK4Ox8QDnbqF8rJYWPZQ==} peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -4197,11 +4191,6 @@ packages: peerDependencies: react: ^18.3.1 - react-icons@5.7.0: - resolution: {integrity: sha512-LBLy340Rzqy6+/yVhZKT3B/QpP1BZaesGqasf09HPOBzRarcDIFH0WwXlXQfE7q7ipxK4MSiC5DIBWURCny6fw==} - peerDependencies: - react: '*' - react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -8724,7 +8713,7 @@ snapshots: dependencies: yallist: 3.1.1 - lucide-react@1.26.0(react@18.3.1): + lucide-react@1.48.0(react@18.3.1): dependencies: react: 18.3.1 @@ -9265,10 +9254,6 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 - react-icons@5.7.0(react@18.3.1): - dependencies: - react: 18.3.1 - react-is@16.13.1: {} react-is@17.0.2: {} diff --git a/readme.md b/readme.md index ef2ccdc7..bf6a4295 100644 --- a/readme.md +++ b/readme.md @@ -26,16 +26,7 @@ Import the library's font styles once, e.g. in your app's entry point: import "@diamondlightsource/sci-react-ui/font-styles.css"; ``` -`@mui/icons-material` and `react-icons` are peer dependencies, so install them too if you use icon-based components: - -```sh -"One of:" -- pnpm add @mui/icons-material react-icons -- npm i @mui/icons-material react-icons -- yarn add @mui/icons-material react-icons -``` - -For icons in your own application code (not required by sci-react-ui itself), we recommend [Lucide](https://lucide.dev/) - see the Icons foundation page in Storybook for guidance: +`lucide-react` is a peer dependency used for the library's icons, so install it too. We also recommend [Lucide](https://lucide.dev/) for icons in your own application code - see the Icons foundation page in Storybook for guidance: ```sh "One of:" @@ -44,6 +35,8 @@ For icons in your own application code (not required by sci-react-ui itself), we - yarn add lucide-react ``` +Upgrading from 0.7.x? `@mui/icons-material` and `react-icons` are no longer required unless used elsewhere in your app. JsonForms users still need `@mui/icons-material` for `@jsonforms/material-renderers`. See the [changelog](changelog.md). + ### Usage First use the ThemeProvider and wrap your App. diff --git a/rollup.config.mjs b/rollup.config.mjs index e3741fe9..77688acc 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -33,7 +33,7 @@ export default [ image(), resolve(), commonjs(), - terser(), + terser({ format: { preserve_annotations: true } }), typescript({ tsconfig: "./tsconfig.rollup.json", }), diff --git a/src/components/DataDisplay/Icons/LucideIcon.test.tsx b/src/components/DataDisplay/Icons/LucideIcon.test.tsx new file mode 100644 index 00000000..8a645aa8 --- /dev/null +++ b/src/components/DataDisplay/Icons/LucideIcon.test.tsx @@ -0,0 +1,31 @@ +import { createRef } from "react"; +import { Settings } from "lucide-react"; + +import { LucideIcon } from "./LucideIcon"; +import { renderWithProviders } from "../../../__test-utils__/helpers"; + +describe("LucideIcon", () => { + it("leaves sizing to MUI's font size unless `size` is given", () => { + const { getByTestId, rerender } = renderWithProviders( + , + ); + expect(getByTestId("icon")).not.toHaveStyle({ width: "24px" }); + + rerender(); + expect(getByTestId("icon")).toHaveStyle({ width: "40px", height: "40px" }); + }); + + it("forwards refs and merges function sx", () => { + const ref = createRef(); + const { getByTestId } = renderWithProviders( + ({ opacity: 0.5 })} + />, + ); + expect(ref.current).toBe(getByTestId("icon")); + expect(getByTestId("icon")).toHaveStyle({ opacity: "0.5" }); + }); +}); diff --git a/src/components/DataDisplay/Icons/LucideIcon.tsx b/src/components/DataDisplay/Icons/LucideIcon.tsx new file mode 100644 index 00000000..505f1fc0 --- /dev/null +++ b/src/components/DataDisplay/Icons/LucideIcon.tsx @@ -0,0 +1,50 @@ +import { forwardRef } from "react"; +import { SvgIcon, type SvgIconProps } from "@mui/material"; +import type { LucideIcon as LucideIconType } from "lucide-react"; +import { iconSizes, type IconSize } from "./iconSizes"; + +// Picks the stroke weight for MUI's `fontSize`; the rendered size stays with MUI. +const muiFontSizeMap: Record = { + small: "sm", + medium: "md", + large: "lg", +}; + +export interface LucideIconProps extends Omit { + icon: LucideIconType; + + /** + * Explicit pixel size, a key of `iconSizes`. + * Without `size`, the icon follows MUI's `fontSize` (and the surrounding font size). + */ + size?: IconSize; +} + +export const LucideIcon = forwardRef( + ({ icon, size, fontSize = "medium", sx, ...props }, ref) => { + const config = iconSizes[size ?? muiFontSizeMap[fontSize] ?? "md"]; + + return ( + + ); + }, +); + +LucideIcon.displayName = "LucideIcon"; + +export default LucideIcon; diff --git a/src/components/DataDisplay/Icons/iconSizes.ts b/src/components/DataDisplay/Icons/iconSizes.ts new file mode 100644 index 00000000..848fc336 --- /dev/null +++ b/src/components/DataDisplay/Icons/iconSizes.ts @@ -0,0 +1,9 @@ +export const iconSizes = { + xs: { size: 16, strokeWidth: 1.5 }, + sm: { size: 20, strokeWidth: 1.75 }, + md: { size: 24, strokeWidth: 2 }, + lg: { size: 32, strokeWidth: 2.25 }, + xl: { size: 40, strokeWidth: 2.25 }, +} as const; + +export type IconSize = keyof typeof iconSizes; diff --git a/src/components/DataDisplay/Icons/icons.tsx b/src/components/DataDisplay/Icons/icons.tsx new file mode 100644 index 00000000..caaab8a3 --- /dev/null +++ b/src/components/DataDisplay/Icons/icons.tsx @@ -0,0 +1,143 @@ +import { forwardRef } from "react"; +import { + ArrowLeft, + ArrowRight, + Bell, + Briefcase, + Check, + ChartLine, + ChevronDown, + ChevronLeft, + ChevronRight, + ChevronUp, + CircleX, + ClipboardList, + Copy, + Flame, + Folder, + Grid3x3, + Heart, + History, + House, + Inbox, + Info, + LayoutDashboard, + LogIn, + LogOut, + Mail, + MapPin, + Menu, + Moon, + Plus, + Printer, + RefreshCw, + ScanQrCode, + Save, + Search, + Send, + Settings, + SlidersHorizontal, + Share2, + Star, + Sun, + Trash2, + UserRound, + X, + CircleCheck, + TriangleAlert, + type LucideIcon as LucideIconType, +} from "lucide-react"; + +import { LucideIcon, type LucideIconProps } from "./LucideIcon"; + +type IconAliasProps = Omit; + +const createIcon = (icon: LucideIconType, displayName: string) => { + const Icon = forwardRef((props, ref) => ( + + )); + Icon.displayName = displayName; + return Icon; +}; + +export const AlertTriangleIcon = /*#__PURE__*/ createIcon( + TriangleAlert, + "AlertTriangleIcon", +); +export const ArrowLeftIcon = /*#__PURE__*/ createIcon( + ArrowLeft, + "ArrowLeftIcon", +); +export const ArrowRightIcon = /*#__PURE__*/ createIcon( + ArrowRight, + "ArrowRightIcon", +); +export const BellIcon = /*#__PURE__*/ createIcon(Bell, "BellIcon"); +export const BriefcaseIcon = /*#__PURE__*/ createIcon( + Briefcase, + "BriefcaseIcon", +); +export const ChartIcon = /*#__PURE__*/ createIcon(ChartLine, "ChartIcon"); +export const CheckIcon = /*#__PURE__*/ createIcon(Check, "CheckIcon"); +export const CheckCircleIcon = /*#__PURE__*/ createIcon( + CircleCheck, + "CheckCircleIcon", +); +export const ChevronDownIcon = /*#__PURE__*/ createIcon( + ChevronDown, + "ChevronDownIcon", +); +export const ChevronLeftIcon = /*#__PURE__*/ createIcon( + ChevronLeft, + "ChevronLeftIcon", +); +export const ChevronRightIcon = /*#__PURE__*/ createIcon( + ChevronRight, + "ChevronRightIcon", +); +export const ChevronUpIcon = /*#__PURE__*/ createIcon( + ChevronUp, + "ChevronUpIcon", +); +export const CircleXIcon = /*#__PURE__*/ createIcon(CircleX, "CircleXIcon"); +export const ClipboardListIcon = /*#__PURE__*/ createIcon( + ClipboardList, + "ClipboardListIcon", +); +export const CopyIcon = /*#__PURE__*/ createIcon(Copy, "CopyIcon"); +export const FlameIcon = /*#__PURE__*/ createIcon(Flame, "FlameIcon"); +export const FolderIcon = /*#__PURE__*/ createIcon(Folder, "FolderIcon"); +export const GridIcon = /*#__PURE__*/ createIcon(Grid3x3, "GridIcon"); +export const HeartIcon = /*#__PURE__*/ createIcon(Heart, "HeartIcon"); +export const HistoryIcon = /*#__PURE__*/ createIcon(History, "HistoryIcon"); +export const HomeIcon = /*#__PURE__*/ createIcon(House, "HomeIcon"); +export const InboxIcon = /*#__PURE__*/ createIcon(Inbox, "InboxIcon"); +export const DashboardIcon = /*#__PURE__*/ createIcon( + LayoutDashboard, + "DashboardIcon", +); +export const InfoIcon = /*#__PURE__*/ createIcon(Info, "InfoIcon"); +export const LoginIcon = /*#__PURE__*/ createIcon(LogIn, "LoginIcon"); +export const LogoutIcon = /*#__PURE__*/ createIcon(LogOut, "LogoutIcon"); +export const MailIcon = /*#__PURE__*/ createIcon(Mail, "MailIcon"); +export const MapPinIcon = /*#__PURE__*/ createIcon(MapPin, "MapPinIcon"); +export const MenuIcon = /*#__PURE__*/ createIcon(Menu, "MenuIcon"); +export const MoonIcon = /*#__PURE__*/ createIcon(Moon, "MoonIcon"); +export const PlusIcon = /*#__PURE__*/ createIcon(Plus, "PlusIcon"); +export const PrinterIcon = /*#__PURE__*/ createIcon(Printer, "PrinterIcon"); +export const RefreshIcon = /*#__PURE__*/ createIcon(RefreshCw, "RefreshIcon"); +export const QrScanIcon = /*#__PURE__*/ createIcon(ScanQrCode, "QrScanIcon"); +export const SaveIcon = /*#__PURE__*/ createIcon(Save, "SaveIcon"); +export const SearchIcon = /*#__PURE__*/ createIcon(Search, "SearchIcon"); +export const SendIcon = /*#__PURE__*/ createIcon(Send, "SendIcon"); +export const SettingsIcon = /*#__PURE__*/ createIcon(Settings, "SettingsIcon"); +export const SlidersIcon = /*#__PURE__*/ createIcon( + SlidersHorizontal, + "SlidersIcon", +); +export const ShareIcon = /*#__PURE__*/ createIcon(Share2, "ShareIcon"); +export const StarIcon = /*#__PURE__*/ createIcon(Star, "StarIcon"); +export const SunIcon = /*#__PURE__*/ createIcon(Sun, "SunIcon"); +export const TrashIcon = /*#__PURE__*/ createIcon(Trash2, "TrashIcon"); +export const UserIcon = /*#__PURE__*/ createIcon(UserRound, "UserIcon"); +export const CloseIcon = /*#__PURE__*/ createIcon(X, "CloseIcon"); diff --git a/src/components/DataDisplay/Icons/index.ts b/src/components/DataDisplay/Icons/index.ts new file mode 100644 index 00000000..318cfaf0 --- /dev/null +++ b/src/components/DataDisplay/Icons/index.ts @@ -0,0 +1,7 @@ +export * from "./icons"; + +export { iconSizes } from "./iconSizes"; +export type { IconSize } from "./iconSizes"; + +export { LucideIcon } from "./LucideIcon"; +export type { LucideIconProps } from "./LucideIcon"; diff --git a/src/components/DataDisplay/LucideIcon.stories.tsx b/src/components/DataDisplay/LucideIcon.stories.tsx new file mode 100644 index 00000000..04f30e7e --- /dev/null +++ b/src/components/DataDisplay/LucideIcon.stories.tsx @@ -0,0 +1,114 @@ +import type { Meta, StoryObj } from "@storybook/react-webpack5"; +import { Box, Stack, Typography } from "@mui/material"; +import { Plus, Save, Search, Send, Settings, Star, Trash2 } from "lucide-react"; +import { LucideIcon } from "./Icons/LucideIcon"; + +const icons = { + Star, + Save, + Plus, + Send, + Trash2, + Search, + Settings, +}; + +const meta: Meta = { + title: "Components/DataDisplay/LucideIcon", + component: LucideIcon, + tags: ["autodocs"], + argTypes: { + icon: { + control: "select", + options: Object.keys(icons), + mapping: icons, + }, + size: { + control: "select", + options: ["xs", "sm", "md", "lg", "xl"], + }, + color: { + control: "select", + options: [ + "inherit", + "action", + "disabled", + "primary", + "secondary", + "success", + "error", + "info", + "warning", + ], + }, + fontSize: { + control: "select", + options: ["inherit", "small", "medium", "large"], + table: { + category: "MUI compatibility", + }, + }, + }, + args: { + icon: Star, + size: "md", + color: "inherit", + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Basic: Story = {}; + +export const Sizes: Story = { + render: () => ( + + + + + + + + ), +}; + +export const Colours: Story = { + render: () => ( + + + + + + + + + ), +}; + +export const CommonIcons: Story = { + render: () => ( + + + + + + + + + ), +}; + +export const WithLabels: Story = { + render: () => ( + + {Object.entries(icons).map(([name, icon]) => ( + + + {name} + + ))} + + ), +}; diff --git a/src/components/MUI/DataDisplay/Badge.stories.tsx b/src/components/MUI/DataDisplay/Badge.stories.tsx index b09135c2..f48da89b 100644 --- a/src/components/MUI/DataDisplay/Badge.stories.tsx +++ b/src/components/MUI/DataDisplay/Badge.stories.tsx @@ -10,8 +10,8 @@ import { colourSet } from "../../../utils/diamond"; import { muiDocsParameters } from "../../../../.storybook/muiDocsParameters"; const childMap = { - mail: , notifications: , + mail: , work: , } as const; @@ -46,8 +46,8 @@ const meta: Meta = { max: 99, invisible: false, showZero: false, - overlap: "circular", - children: "mail", + overlap: "rectangular", + children: "notifications", }, }; export default meta; @@ -68,14 +68,14 @@ export const InvisibleNumber: Story = { export const Colours: Story = { args: { badgeContent: 7 }, render: (args) => ( - <> + - + ), }; diff --git a/src/components/MUI/Feedback/Tooltip.stories.tsx b/src/components/MUI/Feedback/Tooltip.stories.tsx index f9cc8b17..001193ab 100644 --- a/src/components/MUI/Feedback/Tooltip.stories.tsx +++ b/src/components/MUI/Feedback/Tooltip.stories.tsx @@ -1,6 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react-webpack5"; import { Tooltip, IconButton } from "../MuiWrapped"; -import InfoIcon from "@mui/icons-material/Info"; +import { InfoIcon } from "../../DataDisplay/Icons"; const meta: Meta = { title: "MUI/Feedback/Tooltip", diff --git a/src/components/MUI/MuiWrapped.tsx b/src/components/MUI/MuiWrapped.tsx index 40801a9c..19e86759 100644 --- a/src/components/MUI/MuiWrapped.tsx +++ b/src/components/MUI/MuiWrapped.tsx @@ -1,28 +1,5 @@ import MuiWrapper from "./MuiWrapper"; -import MuiAddIcon from "@mui/icons-material/Add"; -import MuiAssignmentIcon from "@mui/icons-material/Assignment"; -import MuiDeleteIcon from "@mui/icons-material/Delete"; -import MuiExpandMoreIcon from "@mui/icons-material/ExpandMore"; -import MuiFavoriteIcon from "@mui/icons-material/Favorite"; -import MuiFileCopyIcon from "@mui/icons-material/FileCopy"; -import MuiFolderIcon from "@mui/icons-material/Folder"; -import MuiGrainIcon from "@mui/icons-material/Grain"; -import MuiHomeIcon from "@mui/icons-material/Home"; -import MuiInboxIcon from "@mui/icons-material/Inbox"; -import MuiLocationOnIcon from "@mui/icons-material/LocationOn"; -import MuiMailIcon from "@mui/icons-material/Mail"; -import MuiMenuIcon from "@mui/icons-material/Menu"; -import MuiNotificationsIcon from "@mui/icons-material/Notifications"; -import MuiPageviewIcon from "@mui/icons-material/Pageview"; -import MuiPrintIcon from "@mui/icons-material/Print"; -import MuiRestoreIcon from "@mui/icons-material/Restore"; -import MuiSaveIcon from "@mui/icons-material/Save"; -import MuiSendIcon from "@mui/icons-material/Send"; -import MuiShareIcon from "@mui/icons-material/Share"; -import MuiWhatshotIcon from "@mui/icons-material/Whatshot"; -import MuiWorkIcon from "@mui/icons-material/Work"; - import MuiAccordion, { AccordionProps as MuiAccordionProps, } from "@mui/material/Accordion"; @@ -151,7 +128,9 @@ import MuiSpeedDial, { import MuiSpeedDialAction, { SpeedDialActionProps as MuiSpeedDialActionProps, } from "@mui/material/SpeedDialAction"; -import MuiSpeedDialIcon from "@mui/material/SpeedDial"; +import MuiSpeedDialIcon, { + SpeedDialIconProps as MuiSpeedDialIconProps, +} from "@mui/material/SpeedDialIcon"; import MuiStack, { StackProps as MuiStackProps } from "@mui/material/Stack"; import MuiStepper, { StepperProps as MuiStepperProps, @@ -213,98 +192,36 @@ import MuiTypography, { TypographyProps as MuiTypographyProps, } from "@mui/material/Typography"; -export const AddIcon = MuiWrapper( - MuiAddIcon, - "AddIcon", -); -export const AssignmentIcon = MuiWrapper( - MuiAssignmentIcon, - "AssignmentIcon", -); -export const DeleteIcon = MuiWrapper( - MuiDeleteIcon, - "DeleteIcon", -); -export const ExpandMoreIcon = MuiWrapper( - MuiExpandMoreIcon, - "ExpandMoreIcon", -); -export const FavoriteIcon = MuiWrapper( - MuiFavoriteIcon, - "FavoriteIcon", -); -export const FileCopyIcon = MuiWrapper( - MuiFileCopyIcon, - "FileCopyIcon", -); -export const FolderIcon = MuiWrapper( - MuiFolderIcon, - "FolderIcon", -); -export const GrainIcon = MuiWrapper( - MuiGrainIcon, - "GrainIcon", -); -export const HomeIcon = MuiWrapper( - MuiHomeIcon, - "HomeIcon", -); -export const InboxIcon = MuiWrapper( - MuiInboxIcon, - "InboxIcon", -); -export const LocationOnIcon = MuiWrapper( - MuiLocationOnIcon, - "LocationOnIcon", -); -export const MailIcon = MuiWrapper( - MuiMailIcon, - "MailIcon", -); -export const MenuIcon = MuiWrapper( - MuiMenuIcon, - "MenuIcon", -); -export const NotificationsIcon = MuiWrapper( - MuiNotificationsIcon, - "NotificationIcon", -); -export const PageviewIcon = MuiWrapper( - MuiPageviewIcon, - "PageviewIcon", -); -export const PrintIcon = MuiWrapper( - MuiPrintIcon, - "PrintIcon", -); -export const RestoreIcon = MuiWrapper( - MuiRestoreIcon, - "RestoreIcon", -); -export const SaveIcon = MuiWrapper( - MuiSaveIcon, - "SaveIcon", -); -export const SendIcon = MuiWrapper( - MuiSendIcon, - "SendIcon", -); -export const ShareIcon = MuiWrapper( - MuiShareIcon, - "ShareIcon", -); -export const SpeedDialIcon = MuiWrapper( +export { + PlusIcon as AddIcon, + ClipboardListIcon as AssignmentIcon, + TrashIcon as DeleteIcon, + ChevronDownIcon as ExpandMoreIcon, + HeartIcon as FavoriteIcon, + CopyIcon as FileCopyIcon, + FolderIcon, + GridIcon as GrainIcon, + HomeIcon, + InboxIcon, + MapPinIcon as LocationOnIcon, + MailIcon, + MenuIcon, + BellIcon as NotificationsIcon, + SearchIcon as PageviewIcon, + PrinterIcon as PrintIcon, + HistoryIcon as RestoreIcon, + SaveIcon, + SendIcon, + ShareIcon, + FlameIcon as WhatshotIcon, + BriefcaseIcon as WorkIcon, + InfoIcon, +} from "../DataDisplay/Icons"; + +export const SpeedDialIcon = MuiWrapper( MuiSpeedDialIcon, "SpeedDialIcon", ); -export const WhatshotIcon = MuiWrapper( - MuiWhatshotIcon, - "WhatshotIcon", -); -export const WorkIcon = MuiWrapper( - MuiWorkIcon, - "WorkIcon", -); export const Accordion = MuiWrapper( MuiAccordion, @@ -318,7 +235,9 @@ export const AccordionSummary = MuiWrapper( MuiAccordionSummary, "AccordionSummary", ); + export const Alert = MuiWrapper(MuiAlert, "Alert"); + export const AppBar = MuiWrapper(MuiAppBar, "AppBar"); export const Avatar = MuiWrapper(MuiAvatar, "Avatar"); export const AvatarGroup = MuiWrapper( diff --git a/src/components/controls/ColourSchemeButton.test.tsx b/src/components/controls/ColourSchemeButton.test.tsx index 5fabd624..a1ac807d 100644 --- a/src/components/controls/ColourSchemeButton.test.tsx +++ b/src/components/controls/ColourSchemeButton.test.tsx @@ -46,8 +46,7 @@ describe("ColourSchemeButton", () => { const button = getByRole("button"); expect(button).toBeInTheDocument(); - const icon = getByTestId("LightModeIcon"); - expect(icon).toBeInTheDocument(); + expect(getByTestId("SunIcon")).toBeInTheDocument(); }); it("should change colour scheme on click", () => { diff --git a/src/components/controls/ColourSchemeButton.tsx b/src/components/controls/ColourSchemeButton.tsx index 910e433d..d5701a88 100644 --- a/src/components/controls/ColourSchemeButton.tsx +++ b/src/components/controls/ColourSchemeButton.tsx @@ -1,7 +1,7 @@ import { IconButton, IconButtonProps } from "@mui/material"; import { useColorScheme } from "@mui/material/styles"; -import LightModeIcon from "@mui/icons-material/LightMode"; -import BedtimeIcon from "@mui/icons-material/Bedtime"; + +import { SunIcon, MoonIcon } from "../DataDisplay/Icons"; export const ColourSchemeButton = (props: IconButtonProps) => { const { mode, systemMode, setMode } = useColorScheme(); @@ -36,7 +36,11 @@ export const ColourSchemeButton = (props: IconButtonProps) => { }, })} > - {isDark ? : } + {isDark ? ( + + ) : ( + + )} ); }; diff --git a/src/components/controls/ScrollableImages.tsx b/src/components/controls/ScrollableImages.tsx index 037bb2a0..465fcf77 100644 --- a/src/components/controls/ScrollableImages.tsx +++ b/src/components/controls/ScrollableImages.tsx @@ -8,10 +8,12 @@ import { useTheme, } from "@mui/material"; -import ArrowBackIcon from "@mui/icons-material/ArrowBack"; -import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; -import ArrowBackIosNewIcon from "@mui/icons-material/ArrowBackIosNew"; -import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"; +import { + ArrowLeftIcon, + ArrowRightIcon, + ChevronLeftIcon, + ChevronRightIcon, +} from "../DataDisplay/Icons"; import { extractFramesFromTiff, isTiff } from "../../utils/TiffUtils"; @@ -131,7 +133,7 @@ const ScrollableImages = ({ zIndex: 2, }} > - + - + ); @@ -201,7 +203,7 @@ const ScrollableImages = ({ size="small" sx={{ minWidth: 36, width: 36, height: 36 }} > - + )} @@ -252,7 +254,7 @@ const ScrollableImages = ({ size="small" sx={{ minWidth: 36, width: 36, height: 36 }} > - + )} diff --git a/src/components/controls/User.stories.tsx b/src/components/controls/User.stories.tsx index 8ef1f2f0..68668dca 100644 --- a/src/components/controls/User.stories.tsx +++ b/src/components/controls/User.stories.tsx @@ -1,7 +1,8 @@ import { Meta, StoryObj } from "@storybook/react-webpack5"; import { User } from "./User"; -import { Avatar, Link, MenuItem } from "@mui/material"; +import { Avatar, Link, ListItemIcon, MenuItem } from "@mui/material"; +import { SettingsIcon, UserIcon } from "../DataDisplay/Icons"; const meta: Meta = { title: "Components/Controls/User", @@ -89,10 +90,20 @@ export const AdditionalMenuItems: Story = { }, menuItems: [ - Profile + + + + + Profile + , - Settings + + + + + Settings + , ], onLogout: () => {}, diff --git a/src/components/controls/User.tsx b/src/components/controls/User.tsx index 565398f4..a9cdb6a8 100644 --- a/src/components/controls/User.tsx +++ b/src/components/controls/User.tsx @@ -4,13 +4,14 @@ import { Button, Box, Link, + ListItemIcon, Menu, MenuItem, Stack, Typography, } from "@mui/material"; import { ReactElement, ReactNode, useState } from "react"; -import { MdLogin } from "react-icons/md"; +import { LoginIcon, LogoutIcon, UserIcon } from "../DataDisplay/Icons"; import { Auth } from "../systems/auth"; @@ -74,10 +75,12 @@ const User = ({ {avatar || ( + > + + )} @@ -110,6 +113,9 @@ const User = ({ {auth && ( + + + + + + Logout @@ -131,7 +140,7 @@ const User = ({ ) : ( diff --git a/src/components/navigation/NavMenu.tsx b/src/components/navigation/NavMenu.tsx index 866135a7..01f4b21c 100644 --- a/src/components/navigation/NavMenu.tsx +++ b/src/components/navigation/NavMenu.tsx @@ -7,8 +7,8 @@ import { type MenuItemProps, } from "@mui/material"; import React, { useState, forwardRef, useId } from "react"; -import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import { NavLink, NavLinkProps } from "./Navbar"; +import { ChevronDownIcon } from "../DataDisplay/Icons"; type NavMenuLinkProps = MenuItemProps & NavLinkProps; @@ -96,7 +96,7 @@ const NavMenu = ({ label, children }: NavMenuProps) => { }} > {label} - { onClick={isOpen ? onClose : onOpen} sx={{ display: { md: "none" }, order: -1, color: "inherit" }} > - {isOpen ? : } + {isOpen ? : } , + icon: , linkProps: { href: "" }, }, { label: "Acquisition", - icon: , + icon: , linkProps: { href: "" }, selected: true, }, { label: "Analysis", - icon: , + icon: , linkProps: { href: "" }, }, ], @@ -93,7 +94,7 @@ export const NormalLinks: Story = { setOpen(!open)}> - + When using standard links, the caller must handle the selected state @@ -113,17 +114,17 @@ const reactRouterNavigation = [ navItems: [ { label: "Setup", - icon: , + icon: , linkProps: { to: "/1", component: NavLink }, }, { label: "Acquisition", - icon: , + icon: , linkProps: { to: "/2", component: NavLink }, }, { label: "Analysis", - icon: , + icon: , linkProps: { to: "/3", component: NavLink }, }, ], @@ -132,7 +133,7 @@ const reactRouterNavigation = [ navItems: [ { label: "Organisation", - icon: , + icon: , linkProps: { to: "/4", component: NavLink }, }, ], @@ -151,7 +152,7 @@ export const RouterLinks: Story = { /> setOpen(!open)}> - + React Router NavLinks will handle selected state @@ -171,12 +172,12 @@ const groupedNavigation = [ navItems: [ { label: "Setup", - icon: , + icon: , linkProps: { to: "/1", component: NavLink }, }, { label: "Acquisition", - icon: , + icon: , linkProps: { to: "/2", component: NavLink }, }, ], @@ -185,12 +186,12 @@ const groupedNavigation = [ navItems: [ { label: "Analysis", - icon: , + icon: , linkProps: { to: "/3", component: NavLink }, }, { label: "Data Browse", - icon: , + icon: , linkProps: { to: "/4", component: NavLink }, }, ], @@ -199,7 +200,7 @@ const groupedNavigation = [ navItems: [ { label: "Log", - icon: , + icon: , linkProps: { to: "/5", component: NavLink }, }, ], @@ -218,7 +219,7 @@ export const GroupedNavigation: Story = { /> setOpen(!open)}> - + Sections are grouped with dividers. @@ -279,7 +280,7 @@ export const WithSlots: Story = { sx={{ p: 1, borderRadius: 2, gap: 1.5 }} > - + - + setOpen(!open)}> - + Adds slots to the navbar, boxes are only there to highlight what @@ -352,7 +353,7 @@ export const WithAppBar: Story = { sx={{ mr: 2 }} onClick={() => setOpen(!open)} > - + diff --git a/src/index.ts b/src/index.ts index 70ff7b3d..53ae27db 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,6 +18,9 @@ export * from "./components/controls/ScrollableImages"; export * from "./components/controls/VisitInput"; export * from "./components/controls/NumberInput"; +// components/DataDisplay +export * from "./components/DataDisplay/Icons"; + // components/systems export * from "./components/systems/auth"; diff --git a/src/storybook/Installation.mdx b/src/storybook/Installation.mdx index 26ff9150..dd526b99 100644 --- a/src/storybook/Installation.mdx +++ b/src/storybook/Installation.mdx @@ -47,13 +47,7 @@ Use this page to install SciReactUI, configure the Diamond Design System theme, import "@diamondlightsource/sci-react-ui/font-styles.css"; ``` - `@mui/icons-material` and `react-icons` are peer dependencies, so install them too if you use icon-based components: - - ```shell - pnpm add @mui/icons-material react-icons - ``` - - For icons in your own application code (not required by SciReactUI itself), we recommend [Lucide](https://lucide.dev/) - see the Icons foundation page for guidance: + `lucide-react` is a peer dependency used for the library's icons, so install it too. We also recommend [Lucide](https://lucide.dev/) for icons in your own application code - see the Icons foundation page for guidance: ```shell pnpm add lucide-react diff --git a/src/storybook/foundation/3. icons.mdx b/src/storybook/foundation/3. icons.mdx index df2fcdf3..049db6c5 100644 --- a/src/storybook/foundation/3. icons.mdx +++ b/src/storybook/foundation/3. icons.mdx @@ -53,7 +53,8 @@ Avoid mixing multiple icon families within the same application.

- **Material Icons** may remain in existing screens during the transition. For new + **Material Icons** and **React Icons** may remain in existing screens during + the transition, but the library no longer requires either package. For new components, new patterns, and new application work, use Lucide or the exported Diamond DS icon aliases.

diff --git a/src/themes/DiamondDSTheme.test.ts b/src/themes/DiamondDSTheme.test.ts index 26610117..8499dfc5 100644 --- a/src/themes/DiamondDSTheme.test.ts +++ b/src/themes/DiamondDSTheme.test.ts @@ -1186,6 +1186,24 @@ describe("DiamondDS component overrides", () => { ); }); + it("defaults MUI's built-in icons to Lucide", () => { + const components = DiamondDSTheme.components; + + expect(components?.MuiAlert?.defaultProps?.iconMapping).toEqual({ + success: expect.anything(), + info: expect.anything(), + warning: expect.anything(), + error: expect.anything(), + }); + expect(components?.MuiAlert?.defaultProps?.slots?.closeIcon).toBeDefined(); + expect(components?.MuiSelect?.defaultProps?.IconComponent).toBeDefined(); + expect( + components?.MuiNativeSelect?.defaultProps?.IconComponent, + ).toBeDefined(); + expect(components?.MuiAutocomplete?.defaultProps?.popupIcon).toBeDefined(); + expect(components?.MuiChip?.defaultProps?.deleteIcon).toBeDefined(); + }); + it("maps filled alerts to solid status roles", () => { const root = DiamondDSTheme.components?.MuiAlert?.styleOverrides?.root; diff --git a/src/themes/DiamondDSTheme.ts b/src/themes/DiamondDSTheme.ts index c367f79c..49fbfbab 100644 --- a/src/themes/DiamondDSTheme.ts +++ b/src/themes/DiamondDSTheme.ts @@ -22,6 +22,16 @@ import "@fontsource/ibm-plex-mono/latin-600.css"; import "../styles/diamondDS/DiamondDSTokens.css"; import "../styles/diamondDS/DiamondDSTypography.css"; +import { createElement } from "react"; +import { + AlertTriangleIcon, + CheckCircleIcon, + ChevronDownIcon, + CircleXIcon, + CloseIcon, + InfoIcon, +} from "../components/DataDisplay/Icons"; + // Enables `theme.vars` typings for MUI CSS variable themes. import type {} from "@mui/material/themeCssVarsAugmentation"; import { @@ -907,6 +917,10 @@ const DiamondDSTheme = extendTheme({ }, MuiAutocomplete: { + defaultProps: { + popupIcon: createElement(ChevronDownIcon), + clearIcon: createElement(CloseIcon, { fontSize: "small" }), + }, /** * Autocomplete renders its own Paper and defaults to elevation 1 * internally, unlike Popover/Menu/Select's 8 — even though this is @@ -1273,6 +1287,9 @@ const DiamondDSTheme = extendTheme({ * * Interactive chips receive focus and overlay states; static chips remain calm. */ + defaultProps: { + deleteIcon: createElement(CircleXIcon), + }, styleOverrides: { root: ({ ownerState }: OverrideArgs): CSSObject => { const base: CSSObject = { @@ -1630,7 +1647,16 @@ const DiamondDSTheme = extendTheme({ }, }, + MuiNativeSelect: { + defaultProps: { + IconComponent: ChevronDownIcon, + }, + }, + MuiSelect: { + defaultProps: { + IconComponent: ChevronDownIcon, + }, styleOverrides: { /** Read-only Select never opens, so the chevron is muted like a disabled one. */ icon: ({ ownerState }: OverrideArgs): CSSObject => @@ -1743,6 +1769,15 @@ const DiamondDSTheme = extendTheme({ * Alerts use status intents only. Filled alerts use solid/onSolid; standard and * outlined alerts use container/onContainer. */ + defaultProps: { + iconMapping: { + success: createElement(CheckCircleIcon, { size: "sm" }), + info: createElement(InfoIcon, { size: "sm" }), + warning: createElement(AlertTriangleIcon, { size: "sm" }), + error: createElement(CircleXIcon, { size: "sm" }), + }, + slots: { closeIcon: CloseIcon }, + }, styleOverrides: { root: ({ ownerState }: OverrideArgs): CSSObject => { const severity = getIntentFromColourProp(