diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index 3ddb8296..e1611868 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -4,26 +4,12 @@ import type { Preview } from "@storybook/react"; import "@fontsource-variable/inter"; import "./storybook.css"; /* Storybook CSS override */ import { ThemeProvider } from "../src"; -import { GenericTheme, DiamondTheme, DiamondDSTheme } from "../src"; +import { DiamondDSTheme } from "../src"; import { ThemeSwapper, TextLight, TextDark, TextSystem } from "./ThemeSwapper"; import "../src/styles/diamondDS/diamond-ds-roles.css"; -const TextThemeBase = "Theme: Generic"; -const TextThemeDiamond = "Theme: Diamond"; const TextThemeDiamondDS = "Theme: DiamondDS"; -function resolveTheme(selectedTheme: string) { - switch (selectedTheme) { - case TextThemeBase: - return GenericTheme; - case TextThemeDiamond: - return DiamondTheme; - case TextThemeDiamondDS: - default: - return DiamondDSTheme; - } -} - function resolveDefaultMode(selectedThemeMode: string) { if (selectedThemeMode === TextLight) return "light"; if (selectedThemeMode === TextDark) return "dark"; @@ -41,12 +27,11 @@ export const decorators = [ }, (Story, context) => { - const selectedTheme = context.globals.theme || TextThemeDiamondDS; const selectedThemeMode = context.globals.themeMode || TextSystem; return ( @@ -61,28 +46,19 @@ export const decorators = [ const preview: Preview = { globalTypes: { - theme: { - description: "Global theme for components", - toolbar: { - title: "Theme", - icon: "cog", - items: [TextThemeBase, TextThemeDiamond, TextThemeDiamondDS], - dynamicTitle: true, - }, - }, themeMode: { description: "Global theme mode for components", toolbar: { title: "Theme Mode", icon: "mirror", - items: [TextLight, TextDark, TextSystem], + items: [TextLight, TextDark], dynamicTitle: true, }, }, }, initialGlobals: { - theme: TextThemeDiamond, - themeMode: TextSystem, + theme: TextThemeDiamondDS, + themeMode: TextLight, }, parameters: { controls: { @@ -103,7 +79,6 @@ const preview: Preview = { "Helpers", "Theme", "Theme/Logos", - "Theme/Colours", "MUI", "Components", ], diff --git a/changelog.md b/changelog.md index 3797b903..7a2fa47c 100644 --- a/changelog.md +++ b/changelog.md @@ -2,6 +2,14 @@ ## [Unreleased] +### Changed +- **Breaking** Introduced new design system theme based on semantic surface tokens. +- **Breaking** Removed Diamond and Generic themes which are no longer supported. +- **Breaking** Updated components to new theme and ensure compatability in light/dark modes. +- *Logo* and *ImageColourSchemeSwitch* use tone (default/inverse) and fixedTone to adapt to surface colour. Deprecated use of interchange prop. + +## [v0.5.0] - 2026-06-03 + ### Changed - **Breaking** `keycloak-js` has been moved from a direct dependency to a peer and optional dependency, so must now be installed by the consuming application. diff --git a/src/__test-utils__/helpers.tsx b/src/__test-utils__/helpers.tsx index 9f17382a..ee9016d3 100644 --- a/src/__test-utils__/helpers.tsx +++ b/src/__test-utils__/helpers.tsx @@ -1,7 +1,7 @@ import React from "react"; import { ThemeProvider, ThemeProviderProps } from "@mui/material/styles"; -import { DiamondTheme } from "../themes/DiamondTheme"; +import { DiamondDSTheme } from "../themes/DiamondDSTheme"; import { render, RenderResult } from "@testing-library/react"; type ThemeProviderPropsWithOptionalTheme = Omit & @@ -12,7 +12,7 @@ export const addProviders = ( themeOptions?: ThemeProviderPropsWithOptionalTheme, ) => { return ( - + {children} ); diff --git a/src/components/controls/AppTitlebar.stories.tsx b/src/components/controls/AppTitlebar.stories.tsx index 1bb6f76d..806f02d2 100644 --- a/src/components/controls/AppTitlebar.stories.tsx +++ b/src/components/controls/AppTitlebar.stories.tsx @@ -1,7 +1,7 @@ import { Meta, StoryObj } from "@storybook/react"; -import { AppTitle, AppTitlebar } from "./AppTitlebar"; +import { AppTitle, AppTitlebar, AppTitlebarProps } from "./AppTitlebar"; -const meta: Meta = { +const meta: Meta = { title: "Components/Controls/AppTitlebar", component: AppTitlebar, tags: ["autodocs"], @@ -15,10 +15,21 @@ const meta: Meta = { }, }, }, + + argTypes: { + surface: { table: { disable: true } }, + variant: { table: { disable: true } }, + elevation: { table: { disable: true } }, + + leftSlot: { control: false }, + centreSlot: { control: false }, + rightSlot: { control: false }, + children: { control: false }, + }, }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Simple: Story = { args: { @@ -47,34 +58,54 @@ export const InCentreSlot: Story = { }, }; -export const DifferentBackground: Story = { +export const WithActions: Story = { args: { - title: "My Great App", - sx: { backgroundColor: "red" }, + title: "My App", + rightSlot: , }, parameters: { docs: { description: { - story: "You can pass styles to the bar.", + story: "Use slots to add actions or controls to the title bar.", }, }, }, }; -export const DifferentColourAndLarge: Story = { +export const CustomTypography: Story = { args: { - children: ( - - ), + children: , + }, + parameters: { + docs: { + description: { + story: + "You can override the title content if you need custom typography.", + }, + }, }, +}; +export const AppTitlebarVariants: Story = { + render: (_args) => ( + <> + + + + + + + + + ), parameters: { docs: { description: { story: - "You can add styles directly to the title when it's a child or in a slot.", + "AppTitlebar defaults to surface container but can be adapted for different emphasis and context.", }, }, }, diff --git a/src/components/controls/AppTitlebar.tsx b/src/components/controls/AppTitlebar.tsx index 32723246..e2fdf786 100644 --- a/src/components/controls/AppTitlebar.tsx +++ b/src/components/controls/AppTitlebar.tsx @@ -1,38 +1,33 @@ -import React from "react"; -import { styled, Typography, TypographyProps } from "@mui/material"; - +import { Typography, TypographyProps } from "@mui/material"; import { Bar, BarSlotsProps } from "./Bar"; interface AppTitleProps extends TypographyProps { title: string; } -const TypographyStyled = styled(Typography)(({ theme }) => ({ - color: theme.vars.palette.primary.contrastText, - fontSize: "2em", -})); - -/** - * A simple wrapper for a H1 title - * @param title The title to display - * @param props Additional styles, etc. - */ const AppTitle = ({ title, ...props }: AppTitleProps) => ( - + {title} - + ); -interface AppTitlebarProps extends BarSlotsProps { +type AppTitlebarProps = BarSlotsProps & { title?: string; -} +}; /** A Title bar for your App. */ -const AppTitlebar = ({ title, children, ...props }: AppTitlebarProps) => { +const AppTitlebar = ({ + surface = "surface", + variant = "container", + elevation, + title, + children, + ...props +}: AppTitlebarProps) => { return ( - + {title && } {children} @@ -40,4 +35,4 @@ const AppTitlebar = ({ title, children, ...props }: AppTitlebarProps) => { }; export { AppTitlebar, AppTitle }; -export type { AppTitlebarProps, AppTitleProps }; +export type { AppTitlebarProps }; diff --git a/src/components/controls/Bar.stories.tsx b/src/components/controls/Bar.stories.tsx index 15a25174..f0026aba 100644 --- a/src/components/controls/Bar.stories.tsx +++ b/src/components/controls/Bar.stories.tsx @@ -1,167 +1,306 @@ import { Meta, StoryObj } from "@storybook/react"; -import { Bar, BarProps } from "./Bar"; +import { BarSlotsProps, Bar } from "./Bar"; +import { Typography } from "../../components/MUI/MuiWrapped"; -const meta: Meta = { +const meta: Meta = { title: "Components/Controls/Bar", component: Bar, tags: ["autodocs"], + + argTypes: { + surface: { + control: "select", + options: [ + "surface", + "paper", + "background", + "primary", + "secondary", + "brand", + "brand-fixed", + "brand-fixedDim", + ], + table: { category: "Appearance" }, + }, + variant: { + control: "select", + options: ["base", "container", "solid"], + if: { arg: "surface", neq: ["background"] }, + description: + "Use 'base' only with surface/paper. Use 'container' or 'solid' for primary, secondary, and brand.", + table: { category: "Appearance" }, + }, + elevation: { + control: { type: "number", min: 0, max: 24 }, + if: { arg: "variant", eq: "base" }, + description: + "Only applies to surface/paper with variant='base'. Ignored otherwise.", + table: { category: "Appearance" }, + }, + containerWidth: { + control: "select", + options: [false, "xs", "sm", "md", "lg", "xl"], + table: { category: "Layout" }, + }, + leftSlot: { control: false }, + centreSlot: { control: false }, + rightSlot: { control: false }, + children: { control: false }, + }, + + args: { + surface: "surface", + variant: "base", + elevation: 0, + leftSlot: Bar, + }, }; export default meta; -type Story = StoryObj; - -const Slot = ({ children }: BarProps) => ( -
- {children} -
-); +type Story = StoryObj; -export const AllSlots: Story = { +export const Default: Story = { args: { - leftSlot: Left Slot, - centreSlot: Centre Slot, - rightSlot: Right Slot, + leftSlot: Default (surface), }, +}; + +export const VariantsOnSurface: Story = { + render: (_args) => ( + <> + Base} + /> + Container} + /> + Solid} + /> + + ), parameters: { docs: { description: { - story: "Three slots are available, left, centre and right.", + story: + "Variants control emphasis on neutral surfaces. Base relies on elevation, container is subtle, and solid is strong.", }, }, }, }; -export const Children: Story = { - args: { - leftSlot: Left Slot, - children: Children, - }, +export const ElevationScale: Story = { + render: (_args) => ( + <> + Elevation 0} + /> + Elevation 1} + /> + Elevation 3} + /> + Elevation 6} + /> + Elevation 12} + /> + Elevation 24} + /> + + ), parameters: { docs: { description: { story: - 'Children appear on the left, proceeding anything added directly to the "leftSlot".', + "Elevation controls hierarchy on neutral surfaces with base variant. Higher values appear more raised.", }, }, }, }; -export const Width: Story = { - args: { - leftSlot: |<, - centreSlot: Normal width, - rightSlot: >|, - }, +export const PrimaryVsSurface: Story = { + render: (_args) => ( + <> + Primary (action)} + /> + Surface (layout)} + /> + Surface Elevated} + /> + + ), parameters: { docs: { description: { story: - 'You can change the width of the content of the bar by setting "containerWidth", ' + - "either with a Breakpoint value (xs, sm, md, lg, xl) or false to match the screen width.", + "Semantic surfaces (e.g. primary, secondary) express intent, while neutral surfaces define structure and hierarchy.", }, }, }, }; -export const WidthMax: Story = { - args: { - leftSlot: |<, - centreSlot: Max width, - rightSlot: >|, - containerWidth: false, - }, +export const ActionVariants: Story = { + render: (_args) => ( + <> + Primary Solid} + /> + Primary Container} + /> + + Secondary Solid} + /> + Secondary Container} + /> + + ), parameters: { docs: { description: { - story: - 'When "containerWidth" is set to "false", the content of the bar is as wide as the screen.', + story: "Variants adjust emphasis.", }, }, }, }; -export const WidthThin: Story = { - args: { - leftSlot: |<, - centreSlot: "sm" width, - rightSlot: >|, - containerWidth: "sm", - }, +export const BrandOptions: Story = { + render: (_args) => ( + <> + Brand Solid} + /> + Brand Container} + /> + Brand Fixed} + /> + Brand Fixed Dim} + /> + + ), parameters: { docs: { description: { story: - 'When "containerWidth" is set to one of "xs, sm, md, lg, xl", the content of the bar ' + - "uses the corresponding width set in the theme.", + "Brand surfaces are used for identity. brand-fixed and brand-fixedDim ignore variant prop and remain consistent across dark/light modes.", }, }, }, }; -export const Styles: Story = { +export const AllSlots: Story = { + args: { + leftSlot: Left, + centreSlot: Centre, + rightSlot: Right, + }, +}; + +export const WithChildren: Story = { args: { - leftSlot: ( -

- Colours... -

- ), - centreSlot: ( -

- and text-size... -

- ), - rightSlot: ( -

- adjusted. -

- ), - style: { background: "#600", color: "#0df", fontSize: "larger" }, + leftSlot: Left, + children: Children, + }, +}; + +export const Width: Story = { + args: { + leftSlot: |<, + centreSlot: "md" Width, + rightSlot: >|, + containerWidth: "md", }, parameters: { docs: { description: { story: - 'Styles are passed through to the underlining Container with the "style" parameter.', + 'You can change the width of the content of the bar by setting "containerWidth", ' + + "either with a Breakpoint value (xs, sm, md, lg, xl) or false to match the screen width.", }, }, }, }; -export const Content: Story = { +export const WidthMax: Story = { args: { - leftSlot:

My text

, - centreSlot: ( - - ), - rightSlot: , - style: { color: "white" }, + leftSlot: |<, + centreSlot: Max Width, + rightSlot: >|, + containerWidth: false, }, parameters: { docs: { description: { - story: "Bars can hold anything you want", + story: + 'When "containerWidth" is set to "false", the content of the bar is as wide as the screen.', }, }, }, }; -export const Spacing: Story = { +export const WidthThin: Story = { args: { - style: { background: "green", height: "10px" }, + leftSlot: |<, + centreSlot: "sm" width, + rightSlot: >|, + containerWidth: "sm", }, parameters: { docs: { description: { - story: "They can become spacing devices to, to add a strip of colour.", + story: + 'When "containerWidth" is set to one of "xs, sm, md, lg, xl", the content of the bar ' + + "uses the corresponding width set in the theme.", }, }, }, diff --git a/src/components/controls/Bar.test.tsx b/src/components/controls/Bar.test.tsx index 66f15cb3..6877ffc8 100644 --- a/src/components/controls/Bar.test.tsx +++ b/src/components/controls/Bar.test.tsx @@ -38,7 +38,7 @@ describe("Bar", () => { // check new style is set expect(headerComputedStyle.border).toBe(borderStyle); // Check default values are still set - expect(headerComputedStyle.height).toBe("auto"); + expect(headerComputedStyle.height).not.toBe("0px"); }); }); diff --git a/src/components/controls/Bar.tsx b/src/components/controls/Bar.tsx index 8f909eca..aa047b19 100644 --- a/src/components/controls/Bar.tsx +++ b/src/components/controls/Bar.tsx @@ -4,46 +4,137 @@ import { BoxProps, Breakpoint, Container, - LinkProps, Stack, styled, } from "@mui/material"; +import { Theme } from "@mui/material/styles"; -interface SlotProps extends BoxProps, React.PropsWithChildren { +type BarProps = BoxProps & { + containerWidth?: false | Breakpoint; + surface?: + | "primary" + | "secondary" + | "brand" + | "brand-fixed" + | "brand-fixedDim" + | "surface" + | "paper" + | "background"; + + variant?: "solid" | "container" | "base"; + elevation?: number; +}; + +type BarSlotsProps = BarProps & { + centreSlot?: React.ReactNode; + rightSlot?: React.ReactNode; + leftSlot?: React.ReactNode; +}; + +const Slot = ({ + className, + children, +}: { className: string; -} -const Slot = ({ className, style, children }: SlotProps) => ( - + children?: React.ReactNode; +}) => ( + {children} ); -const BoxStyled = styled(Box)(({ theme }) => ({ - width: "100%", - height: "auto", - minHeight: "50px", - display: "flex", - alignItems: "center", - justifyContent: "space-between", - borderRadius: 0, - backgroundColor: theme.vars.palette.primary.main, -})); - -interface BarProps extends BoxProps, React.PropsWithChildren { - containerWidth?: false | Breakpoint; -} +const resolveBarSurface = ( + theme: Theme, + surface: string, + variant: "solid" | "container" | "base", + elevation: number, +) => { + const baseBg = + elevation > 0 + ? theme.palette.surface.elevated(elevation) + : theme.palette.background.paper; + + const semantic = ["primary", "secondary", "brand"] as const; + + if (semantic.includes(surface as "primary" | "secondary" | "brand")) { + const p = ( + surface === "brand" + ? theme.palette.brand + : theme.palette[surface as "primary" | "secondary"] + )!; + + if (variant === "solid") { + return { backgroundColor: p.solid, color: p.onSolid }; + } + + if (variant === "container") { + return { backgroundColor: p.container, color: p.onContainer }; + } + + return { backgroundColor: baseBg, color: theme.palette.text.primary }; + } + + if (surface === "brand-fixed" || surface === "brand-fixedDim") { + const p = theme.palette.brand!; + return { + backgroundColor: surface === "brand-fixed" ? p.fixed : p.fixedDim, + color: p.onFixed, + }; + } + + if (surface === "background") { + return { + backgroundColor: theme.palette.background.default, + color: theme.palette.text.primary, + }; + } + + if (surface === "surface" || surface === "paper") { + if (variant === "container") { + return { + backgroundColor: theme.palette.surface.subtle, + color: theme.palette.text.primary, + }; + } + + if (variant === "solid") { + return { + backgroundColor: theme.palette.surface.strong, + color: theme.palette.text.primary, + }; + } + + return { + backgroundColor: baseBg, + color: theme.palette.text.primary, + }; + } + + return { + backgroundColor: baseBg, + color: theme.palette.text.primary, + }; +}; + +const BoxStyled = styled(Box)(({ theme, ...ownerState }) => { + const { surface = "surface", variant = "base", elevation = 0 } = ownerState; + + const { backgroundColor, color } = resolveBarSurface( + theme, + surface, + variant, + elevation, + ); -interface BarSlotsProps extends BarProps { - centreSlot?: React.ReactElement; - rightSlot?: React.ReactElement; - leftSlot?: React.ReactElement; -} + return { + width: "100%", + minHeight: 50, + display: "flex", + alignItems: "center", + backgroundColor, + color, + }; +}); /** * Basic bar. Comes with three slots, and adjustable width. Children are placed in the left slot. @@ -57,20 +148,12 @@ const Bar = ({ ...props }: BarSlotsProps) => ( - + {leftSlot} @@ -78,7 +161,7 @@ const Bar = ({ { @@ -19,11 +20,13 @@ vi.mock("@mui/material", async () => { describe("ColourSchemeButton", () => { it("should render without errors", () => { - render(); + renderWithProviders(); }); it("should show dark icon and button", () => { - const { getByTestId, getByRole } = render(); + const { getByTestId, getByRole } = renderWithProviders( + , + ); const button = getByRole("button"); expect(button).toBeInTheDocument(); @@ -33,7 +36,7 @@ describe("ColourSchemeButton", () => { }); it("should change colour scheme on click", () => { - const { getByRole } = render(); + const { getByRole } = renderWithProviders(); const button = getByRole("button"); fireEvent.click(button); @@ -43,7 +46,9 @@ describe("ColourSchemeButton", () => { it("should call local onclick when button clicked", () => { const mockOnClick = vi.fn(); - const { getByRole } = render(); + const { getByRole } = renderWithProviders( + , + ); const button = getByRole("button"); fireEvent.click(button); diff --git a/src/components/controls/ColourSchemeButton.tsx b/src/components/controls/ColourSchemeButton.tsx index 7c7b1705..1473ee5e 100644 --- a/src/components/controls/ColourSchemeButton.tsx +++ b/src/components/controls/ColourSchemeButton.tsx @@ -1,47 +1,34 @@ -import { useColorScheme, useTheme } from "@mui/material"; -import { IconButton, IconButtonProps } from "@mui/material"; +import { IconButton, IconButtonProps, useColorScheme } from "@mui/material"; import LightModeIcon from "@mui/icons-material/LightMode"; import BedtimeIcon from "@mui/icons-material/Bedtime"; -import { ColourSchemes } from "../../utils/globals"; - -const ColourSchemeButton = (props: IconButtonProps) => { - const theme = useTheme(); - const { colorScheme: colourScheme, setColorScheme: setColourScheme } = - useColorScheme(); - - if (!colourScheme) return undefined; - - const isDark = (): boolean => colourScheme === ColourSchemes.Dark; +export const ColourSchemeButton = (props: IconButtonProps) => { + const { mode, setMode } = useColorScheme(); + const isDark = mode === "dark"; return ( { - setColourScheme(isDark() ? ColourSchemes.Light : ColourSchemes.Dark); - if (props.onClick) props.onClick(event); + setMode?.(isDark ? "light" : "dark"); + props.onClick?.(event); }} + sx={(theme) => ({ + ml: 1, + width: 32, + height: 32, + borderRadius: 1, + backgroundColor: theme.palette.surface.strong, + color: theme.palette.text.primary, + "&:hover": { + backgroundColor: theme.palette.surface.strong, + borderColor: theme.palette.border.subtle, + }, + })} > - {isDark() ? : } + {isDark ? : } ); }; - -export type { IconButtonProps }; -export { ColourSchemeButton }; diff --git a/src/components/controls/ImageColourSchemeSwitch.stories.tsx b/src/components/controls/ImageColourSchemeSwitch.stories.tsx index 4d72a762..78b1e327 100644 --- a/src/components/controls/ImageColourSchemeSwitch.stories.tsx +++ b/src/components/controls/ImageColourSchemeSwitch.stories.tsx @@ -1,26 +1,46 @@ import { Meta, StoryObj } from "@storybook/react"; import { ImageColourSchemeSwitch } from "./ImageColourSchemeSwitch"; -import imageDark from "../../public/generic/logo-dark.svg"; -import imageLight from "../../public/generic/logo-light.svg"; +import imageDark from "../../public/generic/logo-dark-surface.svg"; +import imageLight from "../../public/generic/logo-light-surface.svg"; const meta: Meta = { title: "Components/Controls/ImageColourSchemeSwitch", component: ImageColourSchemeSwitch, tags: ["autodocs"], + argTypes: { + fixedTone: { + control: { type: "select" }, + options: ["undefined", "light", "dark"], + mapping: { + undefined: undefined, + light: "light", + dark: "dark", + }, + description: "Force to light/dark version. Select 'undefined' to reset.", + }, + tone: { + control: { type: "radio" }, + options: ["default", "inverse"], + }, + }, }; export default meta; type Story = StoryObj; +const image = { + src: imageDark, + srcDark: imageLight, + alt: "Testing Switching Image", + width: "100", +}; + export const SwitchingImage: Story = { args: { - image: { - src: imageDark, - srcDark: imageLight, - alt: "Testing Switching Image", - width: "100", - }, + image: image, + + fixedTone: undefined, }, parameters: { docs: { @@ -34,12 +54,7 @@ export const SwitchingImage: Story = { export const LargeSwitchingImage: Story = { args: { - image: { - src: imageDark, - srcDark: imageLight, - alt: "Testing Switching Image", - width: "300", - }, + image: { ...image, width: "300" }, }, parameters: { docs: { @@ -52,12 +67,7 @@ export const LargeSwitchingImage: Story = { export const AddAdditionalStyles: Story = { args: { - image: { - src: imageDark, - srcDark: imageLight, - alt: "Testing Switching Image", - width: "100", - }, + image: { ...image }, style: { border: "3px dotted red" }, }, parameters: { @@ -68,22 +78,50 @@ export const AddAdditionalStyles: Story = { }, }, }; -export const LightImageForDarkTheme: Story = { + +export const InverseToneImage: Story = { args: { - image: { - src: imageDark, - srcDark: imageLight, - alt: "Testing Switching Image", - width: "120", - }, - interchange: true, + image: { ...image, width: "120" }, + tone: "inverse", + fixedTone: undefined, style: { padding: "10px", background: "grey" }, }, parameters: { docs: { description: { story: - "You can choose to flip which image shows in which mode. This may be useful with certain background colours.", + "Using tone='inverse' flips which image appears in light/dark mode.", + }, + }, + }, +}; + +export const ForcedDarkImage: Story = { + args: { + image: image, + fixedTone: "dark", + style: { background: "grey", padding: "10px" }, + }, + parameters: { + docs: { + description: { + story: + "Using fixedTone='dark' force dark image in both light and dark modes.", + }, + }, + }, +}; + +export const ForcedLightImage: Story = { + args: { + image: image, + fixedTone: "light", + }, + parameters: { + docs: { + description: { + story: + "Using fixedTone='light' force dark image in both light and dark modes.", }, }, }, @@ -96,7 +134,6 @@ export const NonSwitchingImage: Story = { alt: "Testing Non-Switching Image", width: "100", }, - style: { border: "1px solid black" }, }, parameters: { docs: { diff --git a/src/components/controls/ImageColourSchemeSwitch.test.tsx b/src/components/controls/ImageColourSchemeSwitch.test.tsx index b1abbe38..fea5c126 100644 --- a/src/components/controls/ImageColourSchemeSwitch.test.tsx +++ b/src/components/controls/ImageColourSchemeSwitch.test.tsx @@ -75,4 +75,68 @@ describe("ImageColourSchemeSwitch", () => { const img = screen.getByRole("img"); expect(img).toHaveAttribute("src", testVals.src); }); + + it("should use dark src when fixedTone='dark'", () => { + renderWithProviders( + , + { + defaultMode: "dark", + }, + ); + + const img = screen.getByRole("img"); + expect(img).toHaveAttribute("src", testVals.srcDark); + }); + + it("should use light src when fixedTone='light'", () => { + renderWithProviders( + , + { + defaultMode: "dark", + }, + ); + + const img = screen.getByRole("img"); + expect(img).toHaveAttribute("src", testVals.src); + }); + + it("should reset to default when fixedTone is undefined", () => { + renderWithProviders( + , + { + defaultMode: "light", + }, + ); + + const img = screen.getByRole("img"); + expect(img).toHaveAttribute("src", testVals.src); + }); + + it("should use dark src with tone='inverse' in light mode", () => { + renderWithProviders( + , + { + defaultMode: "light", + }, + ); + + const img = screen.getByRole("img"); + expect(img).toHaveAttribute("src", testVals.srcDark); + }); + + it("should prioritise fixedTone over tone", () => { + renderWithProviders( + , + { + defaultMode: "dark", + }, + ); + + const img = screen.getByRole("img"); + expect(img).toHaveAttribute("src", testVals.src); + }); }); diff --git a/src/components/controls/ImageColourSchemeSwitch.tsx b/src/components/controls/ImageColourSchemeSwitch.tsx index 70569094..986ead5c 100644 --- a/src/components/controls/ImageColourSchemeSwitch.tsx +++ b/src/components/controls/ImageColourSchemeSwitch.tsx @@ -1,4 +1,4 @@ -import { styled } from "@mui/material"; +import { useColorScheme } from "@mui/material"; import React from "react"; type ImageColourSchemeSwitchType = { @@ -17,64 +17,54 @@ type ImageColourSchemeSwitchType = { interface ImageColourSchemeSwitchProps { /** The definition for the two images. */ image: ImageColourSchemeSwitchType; - /** When true, the light image will appear in dark mode and vice-versa. */ - interchange?: boolean; + /** When light, the light image will appear in both light and dark mode and vice-versa. Takes priority over tone when both are defined. */ + fixedTone?: "light" | "dark"; + /** When inverse, the light image will appear in dark mode and vice-versa. */ + tone?: "default" | "inverse"; /** Additional styles to pass to the underlying img tag. */ style?: React.CSSProperties; + /** + * @deprecated Use `tone="inverse"` instead. + * When true, forces the inverse logo. + */ + interchange?: boolean; } -/** Styled component which is only displayed in dark mode. */ -const ImageDark = styled("img")(({ theme }) => [ - { display: "none" }, - theme.applyStyles("dark", { - display: "block", - }), -]); - -/** Styled component which is only displayed in light mode. */ -const ImageLight = styled("img")(({ theme }) => [ - { display: "block" }, - theme.applyStyles("dark", { - display: "none", - }), -]); - -/** - * Switch between two different images depending on the current color scheme selected (light or dark). - */ const ImageColourSchemeSwitch = ({ image, - interchange, + fixedTone = undefined, + tone = "default", style, -}: ImageColourSchemeSwitchProps) => - image.srcDark ? ( - <> - - - - ) : ( + interchange, +}: ImageColourSchemeSwitchProps) => { + const { mode } = useColorScheme(); + const isDark = (mode ?? "light") === "dark"; + + // Keep backwards compatibility for interchange + const effectiveTone = interchange ? "inverse" : tone; + let src = image.src; + + if ( + image.srcDark && + fixedTone != "light" && + (fixedTone === "dark" || + (isDark && effectiveTone === "default") || + (!isDark && effectiveTone === "inverse")) + ) { + src = image.srcDark; + } + + return ( {image.alt} ); +}; export { ImageColourSchemeSwitch }; export type { ImageColourSchemeSwitchProps, ImageColourSchemeSwitchType }; diff --git a/src/components/controls/Logo.stories.tsx b/src/components/controls/Logo.stories.tsx index b59ec863..77c6a686 100644 --- a/src/components/controls/Logo.stories.tsx +++ b/src/components/controls/Logo.stories.tsx @@ -12,13 +12,32 @@ const meta: Meta = { }, }, }, + argTypes: { + fixedTone: { + control: { type: "select" }, + options: ["undefined", "light", "dark"], + mapping: { + undefined: undefined, + light: "light", + dark: "dark", + }, + description: + "Force to light/dark version. Select 'undefined' to reset to default behavior.", + }, + tone: { + control: { type: "radio" }, + options: ["default", "inverse"], + }, + }, }; export default meta; type Story = StoryObj; export const TheLogo: Story = { - args: {}, + args: { + fixedTone: undefined, + }, parameters: { docs: { description: { @@ -29,7 +48,10 @@ export const TheLogo: Story = { }; export const TheShortLogo: Story = { - args: { short: true }, + args: { + short: true, + fixedTone: undefined, + }, parameters: { docs: { description: { @@ -41,14 +63,30 @@ export const TheShortLogo: Story = { export const LightLogoForDarkTheme: Story = { args: { - interchange: true, + tone: "inverse", + fixedTone: undefined, + style: { padding: "10px", background: "grey" }, + }, + parameters: { + docs: { + description: { + story: + "You can swap the light and dark logo. Useful depending on what background it is displayed on.", + }, + }, + }, +}; + +export const ForcedDarkLogoForLightAndDarkTheme: Story = { + args: { + fixedTone: "dark", style: { padding: "10px", background: "grey" }, }, parameters: { docs: { description: { story: - "You can switch-over the light and dark logo. Useful depending on what background it is displayed on.", + "Forces the dark logo regardless of theme. Use 'undefined' in controls to reset.", }, }, }, diff --git a/src/components/controls/Logo.test.tsx b/src/components/controls/Logo.test.tsx index 7b5ec620..c990c72e 100644 --- a/src/components/controls/Logo.test.tsx +++ b/src/components/controls/Logo.test.tsx @@ -10,11 +10,13 @@ import { Logo } from "./Logo"; describe("Logo", () => { const src = "a/test/src"; + const srcDark = "a/test/srcDark"; const TestTheme: Theme = createTheme({ ...BaseThemeOptions, logos: { normal: { src, + srcDark, alt: "alt", }, short: { @@ -55,4 +57,37 @@ describe("Logo", () => { expect(img).toBeInTheDocument(); expect(img).toHaveAttribute("style", "margin: 10px;"); }); + + it("should use dark logo when fixedTone='dark'", () => { + render(); + const img = screen.getByRole("img"); + + expect(img).toHaveAttribute("src", srcDark); + }); + + it("should use light logo when fixedTone='light'", () => { + render(); + const img = screen.getByRole("img"); + + expect(img).toHaveAttribute("src", src); + }); + + it("should fall back to default behaviour when fixedTone is undefined", () => { + render(); + const img = screen.getByRole("img"); + + expect(img).toHaveAttribute("src", src); + }); + + it("should be dark when tone='inverse' in light mode", () => { + render(); + const img = screen.getByRole("img"); + expect(img).toHaveAttribute("src", srcDark); + }); + + it("should prioritise fixedTone over tone", () => { + render(); + const img = screen.getByRole("img"); + expect(img).toHaveAttribute("src", src); + }); }); diff --git a/src/components/controls/Logo.tsx b/src/components/controls/Logo.tsx index ed2db8f4..4e96c75d 100644 --- a/src/components/controls/Logo.tsx +++ b/src/components/controls/Logo.tsx @@ -1,27 +1,54 @@ +import React from "react"; import { BoxProps, useTheme } from "@mui/material"; import { ImageColourSchemeSwitch } from "./ImageColourSchemeSwitch"; -import React from "react"; interface LogoProps extends BoxProps { short?: boolean; + + /** When light, the light image will appear in both light and dark mode and vice-versa. Takes priority over tone when both are defined. */ + fixedTone?: "light" | "dark"; + + /** + * Tone of the logo. + * - "default": follows light/dark mode + * - "inverse": forces the inverse version + */ + tone?: "default" | "inverse"; + + /** + * @deprecated Use `tone="inverse"` instead. + * When true, forces the inverse logo. + */ interchange?: boolean; + style?: React.CSSProperties; } -const Logo = ({ short = false, interchange = false, style }: LogoProps) => { +const Logo = ({ + short = false, + fixedTone = undefined, + tone = "default", + interchange, + style, +}: LogoProps) => { const theme = useTheme(); - const logo = - short !== undefined && short ? theme.logos?.short : theme.logos?.normal; + + const logo = short ? theme.logos?.short : theme.logos?.normal; + + if (!logo) return null; + + // Keep backwards compatibility for interchange + const effectiveTone = interchange ? "inverse" : tone; return ( - logo && ( - - ) + ); }; export { Logo }; +export type { LogoProps }; diff --git a/src/components/controls/Progress.tsx b/src/components/controls/Progress.tsx index 298da8a9..6ba51e07 100644 --- a/src/components/controls/Progress.tsx +++ b/src/components/controls/Progress.tsx @@ -28,20 +28,24 @@ const Progress = (props: ProgressProps) => ( zoom: size[props.size ?? "medium"], }} > + {/* Outer */} ({ - color: theme.vars.palette.primary.light, + color: theme.palette.primary.main, + opacity: 0.2, })} size={40} thickness={9} value={100} role={undefined} /> + {/* Inner */} ({ - color: theme.vars.palette.primary.dark, + color: theme.palette.primary.main, + opacity: 0.5, position: "absolute", left: 2, top: 2, @@ -51,11 +55,12 @@ const Progress = (props: ProgressProps) => ( value={100} role={undefined} /> + {/* Spinner */} ({ - color: theme.vars.palette.secondary.main, + color: theme.palette.primary.main, animationDuration: speed[props.speed ?? "medium"], position: "absolute", left: 2.4, diff --git a/src/components/controls/ProgressDelayed.test.tsx b/src/components/controls/ProgressDelayed.test.tsx index 057241b3..6eb54111 100644 --- a/src/components/controls/ProgressDelayed.test.tsx +++ b/src/components/controls/ProgressDelayed.test.tsx @@ -1,5 +1,3 @@ -import React from "react"; - import "@testing-library/jest-dom"; import { screen } from "@testing-library/react"; diff --git a/src/components/controls/ScrollableImages.stories.tsx b/src/components/controls/ScrollableImages.stories.tsx index f37734e9..c179e388 100644 --- a/src/components/controls/ScrollableImages.stories.tsx +++ b/src/components/controls/ScrollableImages.stories.tsx @@ -72,11 +72,10 @@ export const NoNumbers: Story = { numeration: false, }, }; - export const DifferentBackgroundColour: Story = { args: { images: imagesList, - backgroundColor: "#166", + backgroundColor: "primary.container", }, }; diff --git a/src/components/controls/ScrollableImages.test.tsx b/src/components/controls/ScrollableImages.test.tsx index 972763a8..acd9c5fb 100644 --- a/src/components/controls/ScrollableImages.test.tsx +++ b/src/components/controls/ScrollableImages.test.tsx @@ -1,5 +1,6 @@ import { ImageInfo, ScrollableImages } from "./ScrollableImages"; -import { screen, fireEvent, render } from "@testing-library/react"; +import { screen, fireEvent } from "@testing-library/react"; +import { renderWithProviders } from "../../__test-utils__/helpers"; const imagesList: ImageInfo[] = [ { src: "one", alt: "one" }, @@ -10,45 +11,53 @@ const imagesList: ImageInfo[] = [ describe("ScrollableImages – viewer mode", () => { it("should render", () => { - const { getByTestId } = render(); + const { getByTestId } = renderWithProviders( + , + ); expect(getByTestId("scrollable-images")).toBeInTheDocument(); expect(getByTestId("image-container")).toBeInTheDocument(); }); it("should render buttons by default", () => { - render(); + renderWithProviders(); expect(screen.getByTestId("prev-button")).toBeInTheDocument(); expect(screen.getByTestId("next-button")).toBeInTheDocument(); }); it("should not render buttons when buttons is false", () => { - render(); + renderWithProviders( + , + ); expect(screen.queryByTestId("prev-button")).not.toBeInTheDocument(); expect(screen.queryByTestId("next-button")).not.toBeInTheDocument(); }); it("should render slider by default", () => { - render(); + renderWithProviders(); expect(screen.getByTestId("slider")).toBeInTheDocument(); }); it("should not render slider when slider is false", () => { - render(); + renderWithProviders( + , + ); expect(screen.queryByTestId("slider")).not.toBeInTheDocument(); }); it("should render numeration by default", () => { - render(); + renderWithProviders(); expect(screen.getByTestId("numeration")).toBeInTheDocument(); }); it("should not render numeration when numeration is false", () => { - render(); + renderWithProviders( + , + ); expect(screen.queryByTestId("numeration")).not.toBeInTheDocument(); }); it("should wrap around by default", () => { - render(); + renderWithProviders(); fireEvent.click(screen.getByTestId("prev-button")); expect(screen.getByTestId("image-container")).toHaveAttribute( "data-index", @@ -57,7 +66,9 @@ describe("ScrollableImages – viewer mode", () => { }); it("should not wrap when wrapAround is false", () => { - render(); + renderWithProviders( + , + ); const imageContainer = screen.getByTestId("image-container"); fireEvent.click(screen.getByTestId("prev-button")); @@ -72,7 +83,7 @@ describe("ScrollableImages – viewer mode", () => { }); it("should not render controls with only one image", () => { - render(); + renderWithProviders(); expect(screen.queryByTestId("numeration")).not.toBeInTheDocument(); expect(screen.queryByTestId("slider")).not.toBeInTheDocument(); expect(screen.queryByTestId("prev-button")).not.toBeInTheDocument(); @@ -80,7 +91,7 @@ describe("ScrollableImages – viewer mode", () => { }); it("should respond to arrow keys when focused", () => { - render(); + renderWithProviders(); const container = screen.getByTestId("image-container"); container.focus(); @@ -92,7 +103,7 @@ describe("ScrollableImages – viewer mode", () => { }); it("should not respond to arrow keys when not focused", () => { - render(); + renderWithProviders(); const container = screen.getByTestId("image-container"); fireEvent.keyDown(window, { key: "ArrowRight" }); @@ -102,18 +113,18 @@ describe("ScrollableImages – viewer mode", () => { describe("ScrollableImages – scroll mode", () => { it("should render scroll container", () => { - render(); + renderWithProviders(); expect(screen.getByTestId("image-scroll-container")).toBeInTheDocument(); }); it("should render left and right scroll buttons", () => { - render(); + renderWithProviders(); expect(screen.getByTestId("scroll-left-button")).toBeInTheDocument(); expect(screen.getByTestId("scroll-right-button")).toBeInTheDocument(); }); it("should render all images in scroll mode", () => { - render(); + renderWithProviders(); imagesList.forEach((_, index) => { expect( @@ -123,7 +134,7 @@ describe("ScrollableImages – scroll mode", () => { }); it.skip("should call scrollBy when clicking scroll buttons", () => { - render(); + renderWithProviders(); const container = screen.getByTestId("image-scroll-container"); @@ -140,7 +151,7 @@ describe("ScrollableImages – scroll mode", () => { }); it("should not render viewer-only controls in scroll mode", () => { - render(); + renderWithProviders(); expect(screen.queryByTestId("slider")).not.toBeInTheDocument(); expect(screen.queryByTestId("numeration")).not.toBeInTheDocument(); diff --git a/src/components/controls/ScrollableImages.tsx b/src/components/controls/ScrollableImages.tsx index 780ccfe0..037bb2a0 100644 --- a/src/components/controls/ScrollableImages.tsx +++ b/src/components/controls/ScrollableImages.tsx @@ -1,5 +1,12 @@ import { useCallback, useEffect, useRef, useState } from "react"; -import { Box, Button, IconButton, Slider, Stack } from "@mui/material"; +import { + Box, + Button, + IconButton, + Slider, + Stack, + useTheme, +} from "@mui/material"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import ArrowForwardIcon from "@mui/icons-material/ArrowForward"; @@ -36,11 +43,11 @@ const ScrollableImages = ({ wrapAround = true, slider = true, numeration = true, - backgroundColor = "#eee", + backgroundColor, scrollStep = 320, }: ScrollableImagesProps) => { const [imageList, setImageList] = useState([]); - + const theme = useTheme(); const handleArrowKeys = (event: React.KeyboardEvent) => { if (event.key === "ArrowLeft") handlePrev(); else if (event.key === "ArrowRight") handleNext(); @@ -122,8 +129,6 @@ const ScrollableImages = ({ top: "50%", transform: "translateY(-50%)", zIndex: 2, - backgroundColor: "#4C5266", - color: "white", }} > @@ -147,8 +152,9 @@ const ScrollableImages = ({ height, flexShrink: 0, scrollSnapAlign: "start", - backgroundColor, - border: "1px solid #ccc", + backgroundColor: + backgroundColor ?? theme.palette.background.paper, + border: `1px solid ${theme.palette.divider}`, display: "flex", alignItems: "center", justifyContent: "center", @@ -176,8 +182,6 @@ const ScrollableImages = ({ top: "50%", transform: "translateY(-50%)", zIndex: 2, - backgroundColor: "#4C5266", - color: "white", }} > @@ -192,6 +196,7 @@ const ScrollableImages = ({ {buttons && imageCount > 1 && ( diff --git a/src/components/controls/VisitInput.test.tsx b/src/components/controls/VisitInput.test.tsx index 47f7be14..49efddb6 100644 --- a/src/components/controls/VisitInput.test.tsx +++ b/src/components/controls/VisitInput.test.tsx @@ -1,37 +1,41 @@ // Adapted from https://github.com/DiamondLightSource/workflows/blob/main/frontend/workflows-lib/tests/components/SubmissionForm.test.tsx -import { fireEvent, render, within } from "@testing-library/react"; - +import { fireEvent, within } from "@testing-library/react"; +import { renderWithProviders } from "../../__test-utils__/helpers"; import { VisitInput } from "./VisitInput"; it("should render visit field", () => { - const { getByTestId } = render( {}} />); + const { getByTestId } = renderWithProviders( + {}} />, + ); expect(getByTestId("visit-field")).toBeInTheDocument(); }); it("should render submit button by default", () => { - const { getByTestId } = render( {}} />); + const { getByTestId } = renderWithProviders( + {}} />, + ); expect(getByTestId("submit-button")).toBeInTheDocument(); }); it("should render submit button", () => { - const { getByTestId } = render( + const { getByTestId } = renderWithProviders( {}} submitButton={true} />, ); expect(getByTestId("submit-button")).toBeInTheDocument(); }); it("should render visit field without submit func", () => { - const { getByTestId } = render(); + const { getByTestId } = renderWithProviders(); expect(getByTestId("visit-field")).toBeInTheDocument(); }); it("should not render submit button by default", () => { - const { queryByTestId } = render(); + const { queryByTestId } = renderWithProviders(); expect(queryByTestId("submit-button")).not.toBeInTheDocument(); }); it("should not render submit button", () => { - const { queryByTestId } = render( + const { queryByTestId } = renderWithProviders( {}} submitButton={false} />, ); expect(queryByTestId("submit-button")).not.toBeInTheDocument(); @@ -39,7 +43,7 @@ it("should not render submit button", () => { it("should produce visit and parameters on submit button click", () => { const onSubmit = vi.fn(); - const { getByTestId } = render( + const { getByTestId } = renderWithProviders( , ); const visitField = within(getByTestId("visit-field")).getByRole("textbox"); @@ -60,7 +64,9 @@ it("should produce visit and parameters on submit button click", () => { it("should produce visit on submit button click", () => { const onSubmit = vi.fn(); - const { getByTestId } = render(); + const { getByTestId } = renderWithProviders( + , + ); const visitField = within(getByTestId("visit-field")).getByRole("textbox"); fireEvent.change(visitField, { target: { value: "zz12345-7" } }); const submitButton = getByTestId("submit-button"); @@ -77,7 +83,7 @@ it("should produce visit on submit button click", () => { it("should not produce parsed visit", () => { const onSubmit = vi.fn(); - const { getByTestId } = render( + const { getByTestId } = renderWithProviders( , ); const visitField = within(getByTestId("visit-field")).getByRole("textbox"); @@ -93,7 +99,9 @@ it("should not produce parsed visit", () => { it("should produce visit on enter key down by default", () => { const onSubmit = vi.fn(); - const { getByTestId } = render(); + const { getByTestId } = renderWithProviders( + , + ); const visitField = within(getByTestId("visit-field")).getByRole("textbox"); fireEvent.change(visitField, { target: { value: "zz12345-7" } }); fireEvent.keyDown(visitField, { @@ -114,7 +122,7 @@ it("should produce visit on enter key down by default", () => { it("should produce visit on enter key down without submit button", () => { const onSubmit = vi.fn(); - const { getByTestId } = render( + const { getByTestId } = renderWithProviders( , ); const visitField = within(getByTestId("visit-field")).getByRole("textbox"); @@ -137,7 +145,7 @@ it("should produce visit on enter key down without submit button", () => { it("should not produce visit on enter key down", () => { const onSubmit = vi.fn(); - const { getByTestId } = render( + const { getByTestId } = renderWithProviders( , ); const visitField = within(getByTestId("visit-field")).getByRole("textbox"); @@ -160,7 +168,7 @@ it("should not produce visit on enter key down", () => { it("should not produce visit on enter key down", () => { const onSubmit = vi.fn(); - const { getByTestId } = render( + const { getByTestId } = renderWithProviders( , ); const visitField = within(getByTestId("visit-field")).getByRole("textbox"); @@ -183,7 +191,7 @@ it("should not produce visit on enter key down", () => { it("should not produce visit on enter key down with no onSubmit", () => { const onSubmit = vi.fn(); - const { getByTestId } = render(); + const { getByTestId } = renderWithProviders(); const visitField = within(getByTestId("visit-field")).getByRole("textbox"); fireEvent.change(visitField, { target: { value: "zz12345-7" } }); fireEvent.keyDown(visitField, { @@ -204,7 +212,7 @@ it("should not produce visit on enter key down with no onSubmit", () => { it("should update visit on submit", () => { const onSubmit = vi.fn(); - const { getByTestId } = render( + const { getByTestId } = renderWithProviders( ( + <> + + + + + + ), + parameters: { + docs: { + description: { + story: + "Breadcrumbs are subtle by default but can adapt to different surfaces when needed.", + }, }, }, }; diff --git a/src/components/navigation/Breadcrumbs.tsx b/src/components/navigation/Breadcrumbs.tsx index 086f9333..3c454daf 100644 --- a/src/components/navigation/Breadcrumbs.tsx +++ b/src/components/navigation/Breadcrumbs.tsx @@ -3,7 +3,6 @@ import { Breadcrumbs as MuiBreadcrumbs, BreadcrumbsProps as MuiBreadcrumbsProps, Link as MuiLink, - styled, Typography, } from "@mui/material"; import HomeIcon from "@mui/icons-material/Home"; @@ -12,11 +11,11 @@ import { CustomLink } from "types/links"; import { Bar, BarProps } from "../controls/Bar"; -interface BreadcrumbsProps extends BarProps { +type BreadcrumbsProps = BarProps & { path: string | string[] | CustomLink[]; linkComponent?: React.ElementType; muiBreadcrumbsProps?: MuiBreadcrumbsProps; -} +}; /** * Create CrumbData from crumb parts with links @@ -53,18 +52,10 @@ export function getCrumbs( }); } -const BarStyled = styled(Bar)(({ theme }) => ({ - backgroundColor: theme.vars.palette.primary.light, -})); - -const MuiBreadcrumbsStyled = styled(MuiBreadcrumbs)( - ({ theme }) => ({ - color: theme.vars.palette.primary.contrastText, - padding: 0, - }), -); - const Breadcrumbs = ({ + surface = "surface", + variant = "container", + elevation, path, linkComponent, muiBreadcrumbsProps, @@ -73,17 +64,22 @@ const Breadcrumbs = ({ const crumbs: CustomLink[] = getCrumbs(path); return ( - - + } + sx={{ + color: "inherit", + "&, & *": { + color: "inherit !important", // required to use Bar colour for adequate text contrast + }, + }} {...muiBreadcrumbsProps} > {crumb.name} ); } })} - - + +
); }; diff --git a/src/components/navigation/Footer.stories.tsx b/src/components/navigation/Footer.stories.tsx index 6bf7f57e..c80de258 100644 --- a/src/components/navigation/Footer.stories.tsx +++ b/src/components/navigation/Footer.stories.tsx @@ -1,6 +1,7 @@ import { Meta, StoryObj } from "@storybook/react"; import { Footer, FooterLink, FooterLinks } from "./Footer"; import { MockLink } from "../../utils/MockLink"; +import { Typography } from "../../components/MUI/MuiWrapped"; const meta: Meta = { title: "Components/Navigation/Footer", @@ -56,6 +57,17 @@ const staticFooterLinks = ( ); +const rightSlotLinks = ( + + + The Moon + + + Phobos + + +); + export const All: Story = { args: { logo: "theme", @@ -103,16 +115,58 @@ export const RightSlot: Story = { args: { logo: "theme", copyright: "Company", - rightSlot: ( - - - The Moon - - - Phobos - - - ), + rightSlot: rightSlotLinks, + }, +}; + +export const FooterVariants: Story = { + render: (_args) => ( + <> +