diff --git a/.storybook/main.ts b/.storybook/main.ts index 73f49764c9..7b56d6be3d 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -39,6 +39,7 @@ const config: StorybookConfig = { stories: [ "./welcome-page/welcome.stories.jsx", + "./theme-preview/*.stories.tsx", "../docs/*.mdx", "../docs/*.stories.tsx", ...getStories(), diff --git a/.storybook/preview.ts b/.storybook/preview.ts index e7c97573fa..f2d8d99bf2 100644 --- a/.storybook/preview.ts +++ b/.storybook/preview.ts @@ -142,6 +142,19 @@ const globalTypes = { showName: true, }, }, + "override-tokens": { + name: "Override tokens", + description: "Apply custom token overrides", + defaultValue: "off", + toolbar: { + title: "Override tokens", + icon: "beaker", + items: [ + { value: "on", title: "With overrides" }, + { value: "off", title: "Without overrides" }, + ], + }, + }, ...(globalThemeProvider as object), } as Preview["globalTypes"]; diff --git a/.storybook/theme-preview/theme-preview.component.tsx b/.storybook/theme-preview/theme-preview.component.tsx new file mode 100644 index 0000000000..b4140ea66d --- /dev/null +++ b/.storybook/theme-preview/theme-preview.component.tsx @@ -0,0 +1,583 @@ +import React, { useState } from "react"; +import styled from "styled-components"; + +import TokensWrapper from "../../src/components/tokens-wrapper"; +import Button from "../../src/components/button/__next__"; +import Link from "../../src/components/link"; +import Box from "../../src/components/box"; +import type { BrandColors } from "../../src/components/tokens-wrapper/__internal__/utils/build-override-tokens"; + +interface ThemePreviewState { + lightPrimary: string; + lightPrimaryHover: string; + lightPrimaryActive: string; + lightOnPrimary: string; + lightInversePrimary: string; + lightInversePrimaryHover: string; + lightInversePrimaryActive: string; + lightInverseOnPrimary: string; + darkPrimary: string; + darkPrimaryHover: string; + darkPrimaryActive: string; + darkOnPrimary: string; + darkInversePrimary: string; + darkInversePrimaryHover: string; + darkInversePrimaryActive: string; + darkInverseOnPrimary: string; +} + +const Container = styled.div` + display: flex; + height: 100vh; + gap: 0; +`; + +const Sidebar = styled.div<{ $isDarkMode: boolean }>` + width: 340px; + background: ${({ $isDarkMode }) => ($isDarkMode ? "#2a2a2a" : "white")}; + border-right: 1px solid + ${({ $isDarkMode }) => ($isDarkMode ? "#4a4a4a" : "#e0e0e0")}; + padding: 24px; + overflow-y: auto; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + + h2 { + font-size: 18px; + font-weight: 600; + margin-bottom: 24px; + color: ${({ $isDarkMode }) => ($isDarkMode ? "#f5f5f5" : "#161616")}; + } +`; + +const ColorGroup = styled.div<{ $isDarkMode: boolean }>` + margin-bottom: 28px; + + label { + display: block; + font-size: 12px; + font-weight: 500; + margin-bottom: 8px; + color: ${({ $isDarkMode }) => ($isDarkMode ? "#d0d0d0" : "#525252")}; + text-transform: uppercase; + letter-spacing: 0.5px; + } +`; + +const ColorInputWrapper = styled.div<{ $isDarkMode: boolean }>` + display: flex; + gap: 12px; + align-items: center; + margin-bottom: 8px; + + input[type="color"] { + width: 48px; + height: 48px; + border: none; + border-radius: 4px; + cursor: pointer; + } + + input[type="text"] { + flex: 1; + padding: 8px 12px; + border: 1px solid + ${({ $isDarkMode }) => ($isDarkMode ? "#5a5a5a" : "#e0e0e0")}; + background: ${({ $isDarkMode }) => ($isDarkMode ? "#1f1f1f" : "white")}; + border-radius: 4px; + font-size: 12px; + font-family: "Monaco", "Courier New", monospace; + text-transform: uppercase; + color: ${({ $isDarkMode }) => ($isDarkMode ? "#f5f5f5" : "#161616")}; + } +`; + +const PreviewArea = styled.div<{ $isDarkMode: boolean }>` + flex: 1; + padding: 40px; + overflow-y: auto; + background: ${({ $isDarkMode }) => + $isDarkMode + ? "linear-gradient(135deg, #202020 0%, #2b2b2b 100%)" + : "linear-gradient(135deg, #fafafa 0%, #f0f0f0 100%)"}; +`; + +const PreviewHeader = styled.div<{ $isDarkMode: boolean }>` + margin-bottom: 40px; + + h2 { + font-size: 24px; + font-weight: 600; + color: ${({ $isDarkMode }) => ($isDarkMode ? "#f5f5f5" : "#161616")}; + margin-bottom: 8px; + } + + p { + font-size: 14px; + color: ${({ $isDarkMode }) => ($isDarkMode ? "#c2c2c2" : "#8d8d8d")}; + } +`; + +const ComponentsGrid = styled.div` + display: grid; + grid-template-columns: repeat(auto-fit, minmax(420px, 1fr)); + gap: 24px; +`; + +const ComponentCard = styled(Box)<{ $isDarkMode: boolean }>` + background: ${({ $isDarkMode }) => ($isDarkMode ? "#303030" : "white")}; + border-radius: 8px; + padding: 24px; + border: 1px solid + ${({ $isDarkMode }) => ($isDarkMode ? "#4a4a4a" : "#e0e0e0")}; + + h3 { + font-size: 12px; + font-weight: 600; + color: ${({ $isDarkMode }) => ($isDarkMode ? "#e0e0e0" : "#525252")}; + text-transform: uppercase; + letter-spacing: 0.5px; + margin-bottom: 16px; + } +`; + +const ComponentRow = styled.div` + display: flex; + gap: 12px; + flex-wrap: wrap; + align-items: center; + margin-bottom: 12px; + + &:last-child { + margin-bottom: 0; + } +`; + +const ResetButton = styled.button<{ $isDarkMode: boolean }>` + width: 100%; + padding: 10px 16px; + background: ${({ $isDarkMode }) => ($isDarkMode ? "#3a3a3a" : "#f5f5f5")}; + border: 1px solid + ${({ $isDarkMode }) => ($isDarkMode ? "#5a5a5a" : "#e0e0e0")}; + border-radius: 4px; + font-size: 13px; + font-weight: 500; + color: ${({ $isDarkMode }) => ($isDarkMode ? "#f5f5f5" : "#161616")}; + cursor: pointer; + margin-top: 24px; + transition: all 0.2s ease; + + &:hover { + background: ${({ $isDarkMode }) => ($isDarkMode ? "#474747" : "#e8e8e8")}; + } +`; + +const SwatchBox = styled.div` + padding: 16px; + border-radius: 4px; + font-weight: 600; + flex: 1; +`; + +const InversePanel = styled.div<{ $isDarkMode: boolean }>` + background: ${({ $isDarkMode }) => ($isDarkMode ? "#f5f5f5" : "#1f1f1f")}; + border-radius: 8px; + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; +`; + +interface ThemePreviewProps { + modeOverride?: "light" | "dark"; +} + +const ThemePreview: React.FC = ({ + modeOverride = "light", +}) => { + const isDarkMode = modeOverride === "dark"; + + const [state, setState] = useState({ + lightPrimary: "#0043ce", + lightPrimaryHover: "#0039b8", + lightPrimaryActive: "#002aa8", + lightOnPrimary: "#ffffff", + lightInversePrimary: "#9eea00", + lightInversePrimaryHover: "#b8f255", + lightInversePrimaryActive: "#85c700", + lightInverseOnPrimary: "#161616", + darkPrimary: "#4589ff", + darkPrimaryHover: "#66b3ff", + darkPrimaryActive: "#82c5ff", + darkOnPrimary: "#161616", + darkInversePrimary: "#2f7d32", + darkInversePrimaryHover: "#3a9540", + darkInversePrimaryActive: "#246428", + darkInverseOnPrimary: "#ffffff", + }); + + const buildOverrides = (): BrandColors => ({ + light: { + primary: state.lightPrimary, + primaryHover: state.lightPrimaryHover, + primaryActive: state.lightPrimaryActive, + onPrimary: state.lightOnPrimary, + inverse: { + primary: state.lightInversePrimary, + primaryHover: state.lightInversePrimaryHover, + primaryActive: state.lightInversePrimaryActive, + onPrimary: state.lightInverseOnPrimary, + }, + }, + dark: { + primary: state.darkPrimary, + primaryHover: state.darkPrimaryHover, + primaryActive: state.darkPrimaryActive, + onPrimary: state.darkOnPrimary, + inverse: { + primary: state.darkInversePrimary, + primaryHover: state.darkInversePrimaryHover, + primaryActive: state.darkInversePrimaryActive, + onPrimary: state.darkInverseOnPrimary, + }, + }, + }); + + const handleColorChange = (key: keyof ThemePreviewState, value: string) => { + setState((prev) => ({ ...prev, [key]: value })); + }; + + const resetColors = () => { + setState({ + lightPrimary: "#0043ce", + lightPrimaryHover: "#0039b8", + lightPrimaryActive: "#002aa8", + lightOnPrimary: "#ffffff", + lightInversePrimary: "#9eea00", + lightInversePrimaryHover: "#b8f255", + lightInversePrimaryActive: "#85c700", + lightInverseOnPrimary: "#161616", + darkPrimary: "#4589ff", + darkPrimaryHover: "#66b3ff", + darkPrimaryActive: "#82c5ff", + darkOnPrimary: "#161616", + darkInversePrimary: "#2f7d32", + darkInversePrimaryHover: "#3a9540", + darkInversePrimaryActive: "#246428", + darkInverseOnPrimary: "#ffffff", + }); + }; + + const renderColorInput = ( + label: string, + key: keyof ThemePreviewState, + value: string, + ) => ( + + + + handleColorChange(key, e.target.value)} + /> + handleColorChange(key, e.target.value)} + placeholder="#000000" + /> + + + ); + + return ( + + + {/* Sidebar */} + +

Brand Colors

+ +

+ Light Mode +

+ {renderColorInput("Primary", "lightPrimary", state.lightPrimary)} + {renderColorInput( + "Primary Hover", + "lightPrimaryHover", + state.lightPrimaryHover, + )} + {renderColorInput( + "Primary Active", + "lightPrimaryActive", + state.lightPrimaryActive, + )} + {renderColorInput( + "Text on Primary", + "lightOnPrimary", + state.lightOnPrimary, + )} + +

+ Light Mode Inverse +

+ {renderColorInput( + "Primary", + "lightInversePrimary", + state.lightInversePrimary, + )} + {renderColorInput( + "Primary Hover", + "lightInversePrimaryHover", + state.lightInversePrimaryHover, + )} + {renderColorInput( + "Primary Active", + "lightInversePrimaryActive", + state.lightInversePrimaryActive, + )} + {renderColorInput( + "Text on Primary", + "lightInverseOnPrimary", + state.lightInverseOnPrimary, + )} + +

+ Dark Mode +

+ {renderColorInput("Primary", "darkPrimary", state.darkPrimary)} + {renderColorInput( + "Primary Hover", + "darkPrimaryHover", + state.darkPrimaryHover, + )} + {renderColorInput( + "Primary Active", + "darkPrimaryActive", + state.darkPrimaryActive, + )} + {renderColorInput( + "Text on Primary", + "darkOnPrimary", + state.darkOnPrimary, + )} + +

+ Dark Mode Inverse +

+ {renderColorInput( + "Primary", + "darkInversePrimary", + state.darkInversePrimary, + )} + {renderColorInput( + "Primary Hover", + "darkInversePrimaryHover", + state.darkInversePrimaryHover, + )} + {renderColorInput( + "Primary Active", + "darkInversePrimaryActive", + state.darkInversePrimaryActive, + )} + {renderColorInput( + "Text on Primary", + "darkInverseOnPrimary", + state.darkInverseOnPrimary, + )} + + + Reset to Defaults + +
+ + {/* Main Preview */} + + +

Live Component Preview

+

Only components that currently react to brand token overrides

+
+ + + {/* Next Buttons */} + +

Buttons (Next)

+ + + + + + + + + + + + +
+ + {/* Next Buttons Inverse */} + +

Buttons (Inverse)

+ + + + + + + + + + +
+ + {/* Links */} + +

Links

+ + + Typical Link + + + Subtle Link + + + Negative Link + + + + + Bold Link + + + Large Link + + +
+ + {/* Links Inverse */} + +

Links (Inverse)

+ + + + Typical Inverse + + + Subtle Inverse + + + Negative Inverse + + + +
+ + {/* Token Swatches */} + +

Brand Token Swatches

+ + + Primary + + + + + Hover State + + + + + Active State + + +
+ + {/* Button Sizes */} + +

Button Sizes

+ + + + + +
+
+
+
+
+ ); +}; + +export default ThemePreview; diff --git a/.storybook/theme-preview/theme-preview.stories.tsx b/.storybook/theme-preview/theme-preview.stories.tsx new file mode 100644 index 0000000000..843a61e653 --- /dev/null +++ b/.storybook/theme-preview/theme-preview.stories.tsx @@ -0,0 +1,49 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; + +import ThemePreview from "./theme-preview.component"; + +const meta: Meta = { + title: "Theme Customization/White Label Preview", + component: ThemePreview, + parameters: { + layout: "fullscreen", + docs: { + description: { + component: + "Interactive theme preview tool for white-label customization. Adjust brand colors to see real-time updates across all Carbon components. The overrides use the TokensWrapper component to apply brand colors globally.", + }, + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +export const LightMode: Story = { + args: { + modeOverride: "light", + }, + parameters: { + docs: { + description: { + story: + "Preview and customize brand colors for light mode. All components will update in real-time as you adjust the primary color and its variants.", + }, + }, + }, +}; + +export const DarkMode: Story = { + args: { + modeOverride: "dark", + }, + parameters: { + docs: { + description: { + story: + "Preview and customize brand colors for dark mode. Adjust the dark mode primary colors and see how they affect component styling across the entire design system.", + }, + }, + }, +}; diff --git a/.storybook/tokens-wrapper-demo/tokens-wrapper-demo.component.tsx b/.storybook/tokens-wrapper-demo/tokens-wrapper-demo.component.tsx index 1ac2ec276a..82560fc72c 100644 --- a/.storybook/tokens-wrapper-demo/tokens-wrapper-demo.component.tsx +++ b/.storybook/tokens-wrapper-demo/tokens-wrapper-demo.component.tsx @@ -2,29 +2,52 @@ import React from "react"; import TokensWrapper from "../../src/components/tokens-wrapper"; -import useModeSwitcher from "../../src/components/tokens-wrapper/__internal__/hooks"; interface TokensWrapperDemoProps { children?: React.ReactNode; modeOverride?: "light" | "dark"; + allowOverrides?: boolean; } +const overrides = { + light: { + primary: "#db004e", + primaryHover: "#c50046", + primaryActive: "#af003e", + onPrimary: "#ffffff", + inverse: { + primary: "#ff69b4", + primaryHover: "#ff82c4", + primaryActive: "#ff9bd4", + onPrimary: "#000000", + }, + }, + dark: { + primary: "#ff69b4", + primaryHover: "#ff82c4", + primaryActive: "#ff9bd4", + onPrimary: "#000000", + inverse: { + primary: "#db004e", + primaryHover: "#c50046", + primaryActive: "#af003e", + onPrimary: "#ffffff", + }, + }, +}; + const TokensWrapperDemo = ({ children, modeOverride, + allowOverrides, }: TokensWrapperDemoProps) => { - const modePreference = useModeSwitcher(modeOverride); - return ( - -
- {children} -
+ + {children} ); }; diff --git a/.storybook/with-fusion-tokens.tsx b/.storybook/with-fusion-tokens.tsx index 658f0e429b..72964bfbd7 100644 --- a/.storybook/with-fusion-tokens.tsx +++ b/.storybook/with-fusion-tokens.tsx @@ -3,7 +3,10 @@ import { Decorator } from "@storybook/react"; import TokensWrapper from "./tokens-wrapper-demo"; const withFusionTokens: Decorator = (Story, context) => ( - + ); diff --git a/scripts/generate_tokens/generate_tokens.mjs b/scripts/generate_tokens/generate_tokens.mjs index 0d62271d8c..b051956bba 100644 --- a/scripts/generate_tokens/generate_tokens.mjs +++ b/scripts/generate_tokens/generate_tokens.mjs @@ -127,7 +127,7 @@ if (includeDarkMode) { staticTokensCSS = ` ${lightCSS} ${zIndexTokens} -.carbon-dark-mode, [data-carbon-theme="dark"] { +&.carbon-dark-mode, &[data-carbon-theme="dark"] { ${darkCSS} } `; diff --git a/src/__internal__/popover/popover.component.tsx b/src/__internal__/popover/popover.component.tsx index 157c0962b8..c59cf258e3 100644 --- a/src/__internal__/popover/popover.component.tsx +++ b/src/__internal__/popover/popover.component.tsx @@ -1,4 +1,10 @@ -import React, { MutableRefObject, useContext, useRef, RefObject } from "react"; +import React, { + MutableRefObject, + useContext, + useRef, + RefObject, + useEffect, +} from "react"; import { createPortal } from "react-dom"; import { flip, Placement, Middleware } from "@floating-ui/dom"; @@ -115,6 +121,25 @@ const Popover = ({ disablePortal, ...props }: PopoverProps) => { const { isInModal } = useContext(ModalContext); const closestDialog = props.reference.current?.closest("[role='dialog']"); + const [mode, setMode] = React.useState(); + + useEffect(() => { + const wrapper = props.reference.current?.closest("[data-carbon-theme]"); + if (!wrapper) return; + + setMode(wrapper.getAttribute("data-carbon-theme") ?? undefined); + + const observer = new MutationObserver(() => { + setMode(wrapper.getAttribute("data-carbon-theme") ?? undefined); + }); + + observer.observe(wrapper, { + attributes: true, + attributeFilter: ["data-carbon-theme"], + }); + + return () => observer.disconnect(); + }, [props.reference]); if (disablePortal) { return ; @@ -125,7 +150,10 @@ const Popover = ({ disablePortal, ...props }: PopoverProps) => { } return createPortal( - + , isInModal && closestDialog ? closestDialog : document.body, diff --git a/src/components/button/__next__/button.stories.tsx b/src/components/button/__next__/button.stories.tsx index a335c14b0c..94780f518d 100644 --- a/src/components/button/__next__/button.stories.tsx +++ b/src/components/button/__next__/button.stories.tsx @@ -142,32 +142,35 @@ export const FullWidth: Story = () => { }; FullWidth.storyName = "Full-Width"; -export const Inverse: Story = () => { - return ( - - - - - - - ); +export const Inverse: Story = { + name: "Inverse", + render: (_args, { globals }) => { + const isDark = (globals.mode as "light" | "dark" | undefined) === "dark"; + return ( + + + + + + + ); + }, }; -Inverse.storyName = "Inverse"; export const Loading: Story = () => { return ( diff --git a/src/components/popover-container/popover-container-test.stories.tsx b/src/components/popover-container/popover-container-test.stories.tsx index 067db8a008..bf1959e41d 100644 --- a/src/components/popover-container/popover-container-test.stories.tsx +++ b/src/components/popover-container/popover-container-test.stories.tsx @@ -1,6 +1,8 @@ import React, { useState } from "react"; -import Button from "../button"; +import Button from "../button/"; +import ButtonNext from "../button/__next__"; + import Box from "../box"; import PopoverContainer, { PopoverContainerProps, @@ -113,6 +115,7 @@ export const InAScrollableBlock = () => { + FOO { /> )} > - + View all notifications diff --git a/src/components/portal/portal.tsx b/src/components/portal/portal.tsx index d653e00176..12ad223117 100644 --- a/src/components/portal/portal.tsx +++ b/src/components/portal/portal.tsx @@ -47,6 +47,7 @@ export const Portal = ({ }: PortalProps) => { const { current: internalId } = useRef(guid()); const id = externalId ?? internalId; + const entranceRef = useRef(null); useEffect(() => { if (onReposition) { @@ -61,10 +62,20 @@ export const Portal = ({ }; }, [onReposition]); + useEffect(() => { + const wrapper = entranceRef.current?.closest("[data-carbon-theme]"); + const currentMode = wrapper?.getAttribute("data-carbon-theme"); + if (!currentMode) return; + + const portalExit = document.querySelector(`[data-portal-exit="${id}"]`); + portalExit?.setAttribute("data-carbon-theme", currentMode); + }); + return ( { - const [modePreference, setModePreference] = useState<"light" | "dark">( - modeOverride || "light", +export default (modeOverride?: "light" | "dark" | "auto") => { + let override: "light" | "dark" | undefined; + if (modeOverride && modeOverride !== "auto") { + override = modeOverride; + } + const [systemPreference, setSystemPreference] = useState<"light" | "dark">( + "light", ); useEffect(() => { - if (modeOverride) { - setModePreference(modeOverride); + if (override) { return; } @@ -17,10 +20,10 @@ export default (modeOverride?: "light" | "dark") => { /* istanbul ignore if */ if (!mediaQuery) return; - setModePreference(mediaQuery.matches ? "dark" : "light"); + setSystemPreference(mediaQuery.matches ? "dark" : "light"); const handleChange = (e: MediaQueryListEvent) => { - setModePreference(e.matches ? "dark" : "light"); + setSystemPreference(e.matches ? "dark" : "light"); }; mediaQuery.addEventListener("change", handleChange); @@ -31,7 +34,7 @@ export default (modeOverride?: "light" | "dark") => { mediaQuery.removeEventListener("change", handleChange); }; - }, [modeOverride]); + }, [override]); - return modePreference; + return override ?? systemPreference; }; diff --git a/src/components/tokens-wrapper/__internal__/static-tokens/__snapshots__/static-tokens.test.ts.snap b/src/components/tokens-wrapper/__internal__/static-tokens/__snapshots__/static-tokens.test.ts.snap index 80a47f4e32..4e55c025b8 100644 --- a/src/components/tokens-wrapper/__internal__/static-tokens/__snapshots__/static-tokens.test.ts.snap +++ b/src/components/tokens-wrapper/__internal__/static-tokens/__snapshots__/static-tokens.test.ts.snap @@ -1284,7 +1284,7 @@ exports[`builds the static tokens as expected 1`] = ` --carbon-zindex-notification: 7000; --carbon-zindex-above-all: 9999; -.carbon-dark-mode, [data-carbon-theme="dark"] { +&.carbon-dark-mode, &[data-carbon-theme="dark"] { --mode-color-ai-stop-1: #00d639; --mode-color-ai-stop-2: #00d6de; --mode-color-ai-stop-3: #9d60ff; diff --git a/src/components/tokens-wrapper/__internal__/utils/build-override-tokens.ts b/src/components/tokens-wrapper/__internal__/utils/build-override-tokens.ts new file mode 100644 index 0000000000..388429246f --- /dev/null +++ b/src/components/tokens-wrapper/__internal__/utils/build-override-tokens.ts @@ -0,0 +1,107 @@ +interface Colors { + primary: string; + primaryHover?: string; + primaryActive?: string; + onPrimary?: string; + onPrimaryHover?: string; + onPrimaryActive?: string; +} + +export interface BrandColors { + light: Colors & { inverse?: Colors }; + dark?: Colors & { inverse?: Colors }; +} + +// Utility to convert a hex colour to an rgba/hex with opacity +// (for the derived alt tokens) +function withOpacity(hex: string, opacity: number): string { + const alpha = Math.round(opacity * 255) + .toString(16) + .padStart(2, "0"); + return `${hex}${alpha}`; +} + +const DEFAULT_ALT_OPACITY = 0.8; +const DEFAULT_ALT3_OPACITY = 0.03; +const HOVER_ALT_OPACITY = 0.15; +const ACTIVE_ALT_OPACITY = 0.3; + +function buildActionRules(colors: Colors, prefix: string): string[] { + const rules: string[] = []; + const tokenPrefix = `--mode-color-action-main-${prefix}`; + + const { + primary, + primaryHover, + primaryActive, + onPrimary, + onPrimaryHover, + onPrimaryActive, + } = colors; + + rules.push(`${tokenPrefix}default: ${primary};`); + rules.push( + `${tokenPrefix}default-alt: ${withOpacity(primary, DEFAULT_ALT_OPACITY)};`, + ); + rules.push( + `${tokenPrefix}default-alt3: ${withOpacity(primary, DEFAULT_ALT3_OPACITY)};`, + ); + + if (primaryHover) { + rules.push(`${tokenPrefix}hover: ${primaryHover};`); + rules.push( + `${tokenPrefix}hover-alt: ${withOpacity(primaryHover, HOVER_ALT_OPACITY)};`, + ); + rules.push(`${tokenPrefix}default-alt2: ${primaryHover};`); + } + + if (primaryActive) { + rules.push(`${tokenPrefix}active: ${primaryActive};`); + rules.push( + `${tokenPrefix}active-alt: ${withOpacity(primaryActive, ACTIVE_ALT_OPACITY)};`, + ); + rules.push(`${tokenPrefix}hover-alt2: ${primaryActive};`); + } + + if (onPrimary) { + rules.push(`${tokenPrefix}with-default: ${onPrimary};`); + rules.push(`${tokenPrefix}with-hover: ${onPrimaryHover ?? onPrimary};`); + rules.push(`${tokenPrefix}with-active: ${onPrimaryActive ?? onPrimary};`); + } else { + if (onPrimaryHover) { + rules.push(`${tokenPrefix}with-hover: ${onPrimaryHover};`); + } + if (onPrimaryActive) { + rules.push(`${tokenPrefix}with-active: ${onPrimaryActive};`); + } + } + return rules; +} + +export const buildOverrideTokens = (overrides: BrandColors): string => { + const lightRules: string[] = []; + const darkRules: string[] = []; + + const { light, dark } = overrides; + + if (light) { + const { inverse } = light; + lightRules.push(...buildActionRules(light, "")); + if (inverse) { + lightRules.push(...buildActionRules(inverse, "inverse-")); + } + } + if (dark) { + const { inverse } = dark; + darkRules.push(...buildActionRules(dark, "")); + if (inverse) { + darkRules.push(...buildActionRules(inverse, "inverse-")); + } + } + + const darkBlock = darkRules.length + ? `&.carbon-dark-mode, &[data-carbon-theme="dark"] { ${darkRules.join(" ")} }` + : ""; + + return [...lightRules, darkBlock].join("\n "); +}; diff --git a/src/components/tokens-wrapper/tokens-wrapper.component.tsx b/src/components/tokens-wrapper/tokens-wrapper.component.tsx index 7672a0b84b..1a37a3fb0f 100644 --- a/src/components/tokens-wrapper/tokens-wrapper.component.tsx +++ b/src/components/tokens-wrapper/tokens-wrapper.component.tsx @@ -2,35 +2,60 @@ import React from "react"; import styled, { createGlobalStyle } from "styled-components"; import STATIC_TOKENS_CSS from "./__internal__/static-tokens"; +import useModeSwitcher from "./__internal__/hooks/useModeSwitcher"; +import { + buildOverrideTokens, + type BrandColors, +} from "./__internal__/utils/build-override-tokens"; export interface TokenWrapperProps { children: React.ReactNode; height?: string; + modeSupportOptIn?: boolean; + modeOverride?: "light" | "dark" | "auto"; + overrides?: BrandColors; } const StyledTokensWrapper = styled.div<{ $height?: string }>` height: ${({ $height }) => $height}; `; -const TokensGlobalStyle = createGlobalStyle` +const TokensGlobalStyle = createGlobalStyle<{ + $overrides?: TokenWrapperProps["overrides"]; +}>` [data-component="tokens-wrapper"], [class*="carbon-portal"] { ${STATIC_TOKENS_CSS} + ${({ $overrides }) => ($overrides ? buildOverrideTokens($overrides) : "")} } `; export const TokensWrapper = ({ children, height = "auto", -}: TokenWrapperProps) => ( - - - {children} - -); + modeSupportOptIn = false, + modeOverride, + overrides, +}: TokenWrapperProps) => { + const modePreference = useModeSwitcher(modeOverride); + const modeProps = modeSupportOptIn + ? { + className: `carbon-${modePreference}-mode`, + "data-carbon-theme": modePreference === "dark" ? "dark" : "light", + } + : {}; + + return ( + + + {children} + + ); +}; export default TokensWrapper;