From b3be1fe122639af5877daacb160baa9943929463 Mon Sep 17 00:00:00 2001 From: Yashwant Rautela Date: Tue, 21 Jul 2026 12:25:53 +0530 Subject: [PATCH 01/10] feat: add Midnight theme foundation Add Midnight as a fourth built-in theme (Light/Dark/IoC/Midnight): - New Dark Navy and Electric Blue palettes in color-palette.ts - midnight-vars.ts (dark-family vars + Midnight overrides) - midnight-theme.tsx - Wire ThemeMode.Midnight into theme-provider and barrel export - Add Midnight to the Storybook theme switcher Signed-off-by: Yashwant Rautela --- packages/open-ui-kit/.storybook/preview.ts | 12 ++ packages/open-ui-kit/src/index.ts | 1 + .../src/theme-provider/theme-provider.tsx | 6 + .../src/theme/midnight/midnight-theme.tsx | 129 ++++++++++++++++++ .../src/theme/midnight/midnight-vars.ts | 116 ++++++++++++++++ .../src/theme/style/color-palette.ts | 24 ++++ 6 files changed, 288 insertions(+) create mode 100644 packages/open-ui-kit/src/theme/midnight/midnight-theme.tsx create mode 100644 packages/open-ui-kit/src/theme/midnight/midnight-vars.ts diff --git a/packages/open-ui-kit/.storybook/preview.ts b/packages/open-ui-kit/.storybook/preview.ts index df8a41af..923871e2 100644 --- a/packages/open-ui-kit/.storybook/preview.ts +++ b/packages/open-ui-kit/.storybook/preview.ts @@ -11,6 +11,7 @@ import { withScreenshot } from "@prantlf/storycap"; import { darkTheme } from "../src/theme/dark/dark-theme"; import { iocTheme } from "../src/theme/ioc/ioc-theme"; import { lightTheme } from "../src/theme/light/light-theme"; +import { midnightTheme } from "../src/theme/midnight/midnight-theme"; const docsTheme = create({ base: "light", @@ -45,6 +46,7 @@ const muiThemeDecorator = withThemeFromJSXProvider({ light: lightTheme, dark: darkTheme, ioc: iocTheme, + midnight: midnightTheme, }, }); @@ -52,6 +54,7 @@ const themeBackgrounds = { light: "#EFF3FC", dark: "#00142B", ioc: "#07111F", + midnight: "#060A0F", }; const themeBackgroundTokens = { @@ -79,6 +82,14 @@ const themeBackgroundTokens = { previewBackground: "#07111F", text: "rgba(255, 255, 255, 0.94)", }, + midnight: { + background: themeBackgrounds.midnight, + border: "#3A4E77", + link: "#558BFF", + mutedText: "#C5C7CB", + previewBackground: "#060A0F", + text: "#E8E9EA", + }, }; const getThemeBackground = (theme?: string) => @@ -223,6 +234,7 @@ export const globalTypes = { { value: "light", icon: "sun", title: "Light" }, { value: "dark", icon: "moon", title: "Dark" }, { value: "ioc", icon: "mirror", title: "IoC" }, + { value: "midnight", icon: "starhollow", title: "Midnight" }, ], showName: false, dynamicTitle: false, diff --git a/packages/open-ui-kit/src/index.ts b/packages/open-ui-kit/src/index.ts index c8d10a4b..d2232e0e 100644 --- a/packages/open-ui-kit/src/index.ts +++ b/packages/open-ui-kit/src/index.ts @@ -17,6 +17,7 @@ export { gradientsPalette } from "./theme/style/gradients"; export { lightVars } from "./theme/light/light-vars"; export { darkVars } from "./theme/dark/dark-vars"; export { iocVars } from "./theme/ioc/ioc-vars"; +export { midnightVars } from "./theme/midnight/midnight-vars"; export type { VarsType } from "./types/vars"; export { ThemeMode, diff --git a/packages/open-ui-kit/src/theme-provider/theme-provider.tsx b/packages/open-ui-kit/src/theme-provider/theme-provider.tsx index bc2bdd6e..ae5db366 100644 --- a/packages/open-ui-kit/src/theme-provider/theme-provider.tsx +++ b/packages/open-ui-kit/src/theme-provider/theme-provider.tsx @@ -14,12 +14,14 @@ import { import { darkTheme } from "@/theme/dark/dark-theme"; import { iocTheme } from "@/theme/ioc/ioc-theme"; import { lightTheme } from "@/theme/light/light-theme"; +import { midnightTheme } from "@/theme/midnight/midnight-theme"; export { useTheme } from "@mui/material"; export enum ThemeMode { Light = "light", Dark = "dark", IoC = "ioc", + Midnight = "midnight", } export interface ThemeModeContextValue { @@ -57,6 +59,10 @@ function resolveBuiltInTheme(mode: ThemeMode): Theme { return iocTheme; } + if (mode === ThemeMode.Midnight) { + return midnightTheme; + } + return mode === ThemeMode.Dark ? darkTheme : lightTheme; } diff --git a/packages/open-ui-kit/src/theme/midnight/midnight-theme.tsx b/packages/open-ui-kit/src/theme/midnight/midnight-theme.tsx new file mode 100644 index 00000000..92fad73d --- /dev/null +++ b/packages/open-ui-kit/src/theme/midnight/midnight-theme.tsx @@ -0,0 +1,129 @@ +/* + * Copyright 2025 Cisco Systems, Inc. and its affiliates + * + * SPDX-License-Identifier: Apache-2.0 + */ +import { + bluePalette, + greenPalette, + greyPalette, + lightOrangePalette, + orangePalette, + redPalette, + surfaceDarkPalette, + darkNavy200, + darkNavy400, + darkModeCardFloating, + darkModeCardLifted, + darkModeCardRaised, + darkModeCardSubtle, + darkModeFooterBottom, + darkModeSideDrawerLeft, + darkModeSideDrawerRight, +} from "@/theme/style/color-palette"; +import { breakpoints, commonMixins } from "@/theme/style/common"; +import { typography } from "@/theme/style/typography"; +import { + createTheme, + PaletteOptions, + ThemeOptions, + Theme, + Shadows, +} from "@mui/material"; +import { midnightVars } from "./midnight-vars"; +import { + buttonComponent, + inputComponents, + snackbarComponent, +} from "@/theme/mui"; + +// Midnight reuses the Dark theme's shadow set (identical drop-shadow values). +export const shadows: Shadows = [ + `none`, + darkModeCardLifted, + darkModeCardSubtle, + darkModeCardRaised, + darkModeCardFloating, + darkModeSideDrawerRight, + darkModeSideDrawerLeft, + darkModeFooterBottom, + ...Array(17).fill("none"), +] as Shadows; + +const palette: PaletteOptions = { + mode: "dark", + primary: bluePalette, + secondary: { + ...surfaceDarkPalette, + main: surfaceDarkPalette[500], + }, + tertiary: lightOrangePalette, + error: redPalette, + warning: lightOrangePalette, + info: bluePalette, + success: greenPalette, + negative: redPalette, + orange: orangePalette, + grey: greyPalette, + vars: midnightVars, + text: { + primary: midnightVars.baseTextStrong, + secondary: midnightVars.baseTextDefault, + disabled: midnightVars.baseTextDisabled, + }, + background: { + paper: darkNavy200, + default: darkNavy400, + }, + action: { + hoverOpacity: 0.05, + selectedOpacity: 0.25, + focusOpacity: 0.18, + }, +}; + +const theme: Theme = createTheme({ + breakpoints, + palette, + typography, + mixins: commonMixins, +}); + +const midnightThemeOptions: ThemeOptions = { + shadows, + components: { + ...buttonComponent(theme), + ...inputComponents(theme), + ...snackbarComponent(theme), + MuiCssBaseline: { + styleOverrides: { + html: { + scrollbarWidth: "thin", + scrollbarColor: `${theme.palette.vars.baseTextMedium} ${theme.palette.background.default}`, + }, + "*::-webkit-scrollbar": { + width: "12px", + height: "12px", + }, + "*::-webkit-scrollbar-track": { + backgroundColor: theme.palette.background.default, + borderRadius: 8, + }, + "*::-webkit-scrollbar-thumb": { + backgroundColor: theme.palette.vars.controlIconMedium, + borderRadius: 8, + border: "2px solid transparent", + backgroundClip: "content-box", + }, + "*::-webkit-scrollbar-thumb:hover": { + backgroundColor: theme.palette.vars.baseTextMedium, + }, + "*::-webkit-scrollbar-corner": { + backgroundColor: theme.palette.background.default, + }, + }, + }, + }, +}; + +export const midnightTheme: Theme = createTheme(theme, midnightThemeOptions); diff --git a/packages/open-ui-kit/src/theme/midnight/midnight-vars.ts b/packages/open-ui-kit/src/theme/midnight/midnight-vars.ts new file mode 100644 index 00000000..d4db6fe0 --- /dev/null +++ b/packages/open-ui-kit/src/theme/midnight/midnight-vars.ts @@ -0,0 +1,116 @@ +/* + * Copyright 2025 Cisco Systems, Inc. and its affiliates + * + * SPDX-License-Identifier: Apache-2.0 + */ + +import { VarsType } from "@/types/vars"; +import { darkVars } from "@/theme/dark/dark-vars"; +import { + surfaceDarkPalette, + greyPalette, + darkNavyPalette, + electricBluePalette, + surfaceDark900Alpha10, +} from "@/theme/style/color-palette"; + +// Midnight is a dark-family theme. It shares most semantic tokens with the +// Dark theme, so it is composed as `darkVars` plus the tokens that Midnight +// redefines: deep "Dark Navy" surfaces, "Electric Blue" primary/interactive, +// and the associated border/background shifts. Values are mapped from the +// Figma "Accordion - Midnight" / Midnight token set. +export const midnightVars: VarsType = { + ...darkVars, + + // Base — Dark Navy surfaces + baseBackgroundStrong: darkNavyPalette[400], // #060a0f canvas + baseBackgroundMedium: darkNavyPalette[200], // #0a141f + baseBackgroundWeak: darkNavyPalette[100], // #1e293b + baseBackgroundHover: darkNavyPalette[300], // #0f1623 + baseBorderDefault: surfaceDarkPalette[300], // #263b62 + baseBorderStrong: surfaceDarkPalette[100], // #3a4e77 + baseBorderMedium: surfaceDarkPalette[200], // #31466e + baseBorderWeak: surfaceDarkPalette[500], // #0d274d + + // Control + controlBackgroundDefault: darkNavyPalette[200], + controlBackgroundWeak: darkNavyPalette[100], + controlBackgroundMedium: darkNavyPalette[100], + controlBackgroundDisabled: darkNavyPalette[200], + controlBorderDefault: surfaceDarkPalette[100], // #3a4e77 + controlBorderStrong: surfaceDarkPalette[200], // #31466e + controlBorderMedium: surfaceDarkPalette[300], // #263b62 + controlBorderWeak: surfaceDarkPalette[400], // #183056 + controlBorderHover: electricBluePalette[700], // #1469cc + controlBorderActive: electricBluePalette[700], + controlBorderDisabled: surfaceDarkPalette[400], + controlIconHover: electricBluePalette[700], + controlIconActive: electricBluePalette[700], + + // Interactive — Primary (Electric Blue) + interactivePrimaryDefaultDefault: electricBluePalette[500], // #558bff + interactivePrimaryDefaultHover: electricBluePalette[300], // #0ab6ff + interactivePrimaryDefaultActive: electricBluePalette[700], // #1469cc + interactivePrimaryDefaultDisabled: electricBluePalette[500], + interactivePrimaryWeakDefault: darkNavyPalette[200], + interactivePrimaryWeakHover: darkNavyPalette[100], + interactivePrimaryWeakActive: darkNavyPalette[100], + interactivePrimaryWeakDisabled: surfaceDark900Alpha10, + + // Interactive — Secondary + interactiveSecondaryDefaultDefault: greyPalette[50], + interactiveSecondaryDefaultHover: greyPalette[0], + interactiveSecondaryDefaultActive: greyPalette[100], + interactiveSecondaryDefaultDisabled: greyPalette[0], + interactiveSecondaryWeakDefault: darkNavyPalette[200], + interactiveSecondaryWeakHover: darkNavyPalette[100], + interactiveSecondaryWeakActive: greyPalette[800], + interactiveSecondaryWeakDisabled: surfaceDarkPalette[900], + + // Interactive — Inverse + interactiveInverseBackgroundDefault: greyPalette[200], + interactiveInverseBackgroundHover: greyPalette[100], + interactiveInverseBackgroundActive: greyPalette[200], + interactiveInverseBackgroundDisabled: greyPalette[200], + interactiveInverseTextHover: surfaceDarkPalette[600], + + // Excellent — Electric Blue + excellentBackgroundDefault: electricBluePalette[700], + excellentBackgroundWeak: electricBluePalette[300], + excellentBackgroundDisabled: electricBluePalette[500], + excellentBackgroundHover: electricBluePalette[500], + excellentBackgroundActive: electricBluePalette[700], + excellentTextDefault: electricBluePalette[300], + excellentTextHover: electricBluePalette[300], + excellentTextActive: electricBluePalette[300], + excellentTextInDefault: electricBluePalette[300], + excellentTextInDisabled: electricBluePalette[300], + excellentBorderDefault: electricBluePalette[500], + excellentBorderHover: electricBluePalette[300], + excellentBorderActive: electricBluePalette[700], + excellentBorderDisabled: electricBluePalette[500], + excellentBorderWeak: electricBluePalette[500], + excellentIconDefault: electricBluePalette[500], + excellentIconHover: electricBluePalette[300], + excellentIconActive: electricBluePalette[700], + excellentIconDisabled: electricBluePalette[500], + excellentIconInDefault: electricBluePalette[300], + excellentIconInHover: electricBluePalette[300], + excellentIconInActive: electricBluePalette[300], + excellentIconInDisabled: electricBluePalette[300], + + // Brand — Electric Blue + Dark Navy + brandIconPrimaryDefault: electricBluePalette[500], + brandIconPrimaryStrong: electricBluePalette[700], + brandIconPrimaryMedium: electricBluePalette[300], + brandIconPrimaryWeak: electricBluePalette[300], + brandIconSecondaryDefault: electricBluePalette[300], + brandIconSecondaryWeak: greyPalette[100], + brandIconSecondaryMedium: greyPalette[200], + brandBackgroundPrimaryDefault: darkNavyPalette[200], + brandBackgroundPrimaryWeak: darkNavyPalette[100], + brandBackgroundPrimaryMedium: surfaceDarkPalette[400], + brandBackgroundSecondaryDefault: darkNavyPalette[100], + brandTextPrimary: electricBluePalette[500], + brandTextSecondary: greyPalette[0], +}; diff --git a/packages/open-ui-kit/src/theme/style/color-palette.ts b/packages/open-ui-kit/src/theme/style/color-palette.ts index d3d9c301..0871a05b 100644 --- a/packages/open-ui-kit/src/theme/style/color-palette.ts +++ b/packages/open-ui-kit/src/theme/style/color-palette.ts @@ -626,3 +626,27 @@ export const OS_LIGHT_COLORS = { sunset: sunsetPalette, brand: brandColors, }; + +// Dark Navy (Midnight deep surfaces) +export const darkNavy100 = "#1e293b"; +export const darkNavy200 = "#0a141f"; +export const darkNavy300 = "#0f1623"; +export const darkNavy400 = "#060a0f"; + +export const darkNavyPalette = { + 100: darkNavy100, + 200: darkNavy200, + 300: darkNavy300, + 400: darkNavy400, +}; + +// Electric Blue (Midnight primary / interactive) +export const electricBlue300 = "#0ab6ff"; +export const electricBlue500 = "#558bff"; +export const electricBlue700 = "#1469cc"; + +export const electricBluePalette = { + 300: electricBlue300, + 500: electricBlue500, + 700: electricBlue700, +}; From 85589d4e9d5e04653e4133d18b61c22055327896 Mon Sep 17 00:00:00 2001 From: Yashwant Rautela Date: Tue, 28 Jul 2026 17:20:17 +0530 Subject: [PATCH 02/10] fix(theme): correct Midnight excellent/interactive disabled token opacity Align 9 Midnight semantic tokens with the Agntcy Midnight design spec. The excellent Weak/Disabled and interactive disabled tokens were mapped to solid Electric Blue values instead of the intended low-opacity variants, causing the excellent Banner to render as solid bright cyan and disabled states to appear fully opaque. Add electricBlueAlpha10/40 palette variants and repoint the affected tokens to them (mirroring the Dark theme's lightBluePalette alpha usage). Signed-off-by: Yashwant Rautela --- .../src/theme/midnight/midnight-vars.ts | 19 ++++++++++--------- .../src/theme/style/color-palette.ts | 4 ++++ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/open-ui-kit/src/theme/midnight/midnight-vars.ts b/packages/open-ui-kit/src/theme/midnight/midnight-vars.ts index d4db6fe0..20198350 100644 --- a/packages/open-ui-kit/src/theme/midnight/midnight-vars.ts +++ b/packages/open-ui-kit/src/theme/midnight/midnight-vars.ts @@ -11,7 +11,7 @@ import { greyPalette, darkNavyPalette, electricBluePalette, - surfaceDark900Alpha10, + surfaceDark900, } from "@/theme/style/color-palette"; // Midnight is a dark-family theme. It shares most semantic tokens with the @@ -51,11 +51,11 @@ export const midnightVars: VarsType = { interactivePrimaryDefaultDefault: electricBluePalette[500], // #558bff interactivePrimaryDefaultHover: electricBluePalette[300], // #0ab6ff interactivePrimaryDefaultActive: electricBluePalette[700], // #1469cc - interactivePrimaryDefaultDisabled: electricBluePalette[500], + interactivePrimaryDefaultDisabled: electricBluePalette["alpha40"], interactivePrimaryWeakDefault: darkNavyPalette[200], interactivePrimaryWeakHover: darkNavyPalette[100], interactivePrimaryWeakActive: darkNavyPalette[100], - interactivePrimaryWeakDisabled: surfaceDark900Alpha10, + interactivePrimaryWeakDisabled: electricBluePalette["alpha10"], // Interactive — Secondary interactiveSecondaryDefaultDefault: greyPalette[50], @@ -73,31 +73,32 @@ export const midnightVars: VarsType = { interactiveInverseBackgroundActive: greyPalette[200], interactiveInverseBackgroundDisabled: greyPalette[200], interactiveInverseTextHover: surfaceDarkPalette[600], + interactiveInverseTextDisabled: surfaceDark900, // Excellent — Electric Blue excellentBackgroundDefault: electricBluePalette[700], - excellentBackgroundWeak: electricBluePalette[300], - excellentBackgroundDisabled: electricBluePalette[500], + excellentBackgroundWeak: electricBluePalette["alpha10"], + excellentBackgroundDisabled: electricBluePalette["alpha40"], excellentBackgroundHover: electricBluePalette[500], excellentBackgroundActive: electricBluePalette[700], excellentTextDefault: electricBluePalette[300], excellentTextHover: electricBluePalette[300], excellentTextActive: electricBluePalette[300], excellentTextInDefault: electricBluePalette[300], - excellentTextInDisabled: electricBluePalette[300], + excellentTextInDisabled: electricBluePalette["alpha10"], excellentBorderDefault: electricBluePalette[500], excellentBorderHover: electricBluePalette[300], excellentBorderActive: electricBluePalette[700], - excellentBorderDisabled: electricBluePalette[500], + excellentBorderDisabled: electricBluePalette["alpha40"], excellentBorderWeak: electricBluePalette[500], excellentIconDefault: electricBluePalette[500], excellentIconHover: electricBluePalette[300], excellentIconActive: electricBluePalette[700], - excellentIconDisabled: electricBluePalette[500], + excellentIconDisabled: electricBluePalette["alpha40"], excellentIconInDefault: electricBluePalette[300], excellentIconInHover: electricBluePalette[300], excellentIconInActive: electricBluePalette[300], - excellentIconInDisabled: electricBluePalette[300], + excellentIconInDisabled: electricBluePalette["alpha10"], // Brand — Electric Blue + Dark Navy brandIconPrimaryDefault: electricBluePalette[500], diff --git a/packages/open-ui-kit/src/theme/style/color-palette.ts b/packages/open-ui-kit/src/theme/style/color-palette.ts index 0871a05b..189f8ab8 100644 --- a/packages/open-ui-kit/src/theme/style/color-palette.ts +++ b/packages/open-ui-kit/src/theme/style/color-palette.ts @@ -644,9 +644,13 @@ export const darkNavyPalette = { export const electricBlue300 = "#0ab6ff"; export const electricBlue500 = "#558bff"; export const electricBlue700 = "#1469cc"; +export const electricBlueAlpha40 = "#558bff66"; +export const electricBlueAlpha10 = "#558bff19"; export const electricBluePalette = { 300: electricBlue300, 500: electricBlue500, 700: electricBlue700, + alpha40: electricBlueAlpha40, + alpha10: electricBlueAlpha10, }; From 27646e7f22b6db5a635cd9b2d69d23f1c2a6c440 Mon Sep 17 00:00:00 2001 From: Yashwant Rautela Date: Thu, 6 Aug 2026 13:16:56 +0530 Subject: [PATCH 03/10] Midnight Theme Fixes (#12) Design token corrections for accordion, activity-timeline, checkbox, code-block, dialog, floating-button, header, message, navigation, and popover components, plus code-block syntax highlighting via refractor. Signed-off-by: Yashwant Rautela --- DESIGN-TOKEN-CHANGES.md | 357 ++++++++++++++++++ .../open-ui-kit/.storybook/css/preview.css | 23 +- packages/open-ui-kit/jest.config.js | 25 +- packages/open-ui-kit/package.json | 1 + .../accordion/components/accordion.tsx | 8 +- .../accordion/components/elements.tsx | 10 +- .../src/components/accordion/types/index.ts | 2 + .../components/activity-timeline.tsx | 4 + .../checkbox/stories/checkbox.stories.tsx | 4 +- .../code-block/__tests__/code-block.test.tsx | 168 ++++++++- .../code-block/components/code-block.tsx | 6 +- .../components/code-block/prism-grammar.ts | 56 +++ .../src/components/code-block/styles/index.ts | 156 ++++++-- .../components/dialog/components/elements.tsx | 6 +- .../__tests__/floating-button.test.tsx | 2 +- .../floating-button/styles/index.ts | 7 +- .../footer/stories/footer.stories.tsx | 9 +- .../header/__tests__/header.test.tsx | 4 +- .../src/components/header/styles/index.ts | 4 +- .../message/__tests__/message.test.tsx | 4 +- .../src/components/message/styles/index.ts | 2 +- .../src/components/navigation/styles/index.ts | 2 +- .../popover/__tests__/popover.test.tsx | 18 +- .../components/popover/components/popover.tsx | 2 +- .../src/components/popover/styles/index.ts | 4 +- .../src/custom-icons/brand-logos.tsx | 4 +- .../test-utils/react-syntax-highlighter.js | 9 +- 27 files changed, 810 insertions(+), 87 deletions(-) create mode 100644 DESIGN-TOKEN-CHANGES.md create mode 100644 packages/open-ui-kit/src/components/code-block/prism-grammar.ts diff --git a/DESIGN-TOKEN-CHANGES.md b/DESIGN-TOKEN-CHANGES.md new file mode 100644 index 00000000..edd625fe --- /dev/null +++ b/DESIGN-TOKEN-CHANGES.md @@ -0,0 +1,357 @@ +# Design token alignment — change summary + +Branch: `gauge-chart/gradient` +Package: `packages/open-ui-kit` +Figma source: [Outshift Spark Component Library](https://www.figma.com/design/o6t5UKJGaw75ZAiLfijAxq/Outshift-Spark-Component-Library) + +Work done: audited several components against the Figma token set, corrected the +tokens that were wrong, and fixed a Storybook CSS bug that was making the Docs +tab display the wrong colors for every component. + +All values below were read from the Figma variable definitions or verified by +rendering the component and reading its computed style — not inferred from the +source alone. Two of the changes exist specifically because the source code +looked correct but rendered incorrectly. + +## Two kinds of change in this branch + +It is worth separating these, because they carry different risk and want +different review attention. + +**Spec alignment (sections 1–3).** The code did what it said it did; it simply +pointed at the wrong token, or shipped stale artwork. These are low-risk, and +the thing to review is whether the chosen token is the *right* one — the diff +itself is obviously correct. + +**Latent rendering bugs (sections 4–5).** In these the source code read as +correct, and a reviewer grepping for the token would have concluded everything +was fine — but the browser rendered something else. Neither would have been +caught by reading the code, and section 5 in particular had been quietly +corrupting the way the team verifies token work. These deserve the closer look. + +The distinction also explains why several of the checks below render the +component and read `getComputedStyle` rather than asserting on a style object: +after section 4, asserting that a style function *returns* the right token was +no longer sufficient evidence that the token reaches the screen. + +--- + +## 1. Header — profile dropdown background + +**Token:** `baseBackgroundMedium` → `controlBackgroundWeak` +**Figma row:** Control / Background / Weak + +The Profile / Log out dropdown paper and its menu items were painted with +`baseBackgroundMedium`. Figma specifies Control/Background/Weak for this surface. + +### Why this was needed + +The two tokens are not interchangeable — they belong to different scales. +`baseBackgroundMedium` is the *base* surface scale, shared with SideDrawer, +Toast, CodeBlock gutters and chart tooltips. `controlBackgroundWeak` is the +*control* surface scale. A dropdown is a control, so it should track the control +scale: if the control palette is ever retuned, the dropdown should move with the +other controls rather than with drawers and toasts. + +Beyond the semantics, the two resolve to visibly different colors — in midnight +`#0a141f` versus `#1e293b` — so the dropdown was rendering noticeably darker than +the design. Leaving it on the base token would also have made the mismatch +invisible to future audits, since the code looked deliberate. + +| File | Change | +| --- | --- | +| `src/components/header/styles/index.ts` | `getStoryMenuPaperStyles` and `getStoryMenuItemStyles` background | +| `src/components/header/__tests__/header.test.tsx` | Two assertions that pinned the old token | + +Resolved values for the new token: + +| Theme | Value | +| --- | --- | +| Light | `#f5f8fd` (Surface Light/100) | +| Dark | `#0d274d` (Surface Dark/500) | +| Midnight | `#1e293b` (Dark Navy/100) | +| IoC | `rgba(255,255,255,0.09)` (deliberate deviation, see note in `ioc-vars.ts:93`) | + +In midnight the dropdown moves from `#0a141f` to `#1e293b`, so it now reads +slightly lighter against the page. + +> **Follow-up:** the item hover state still uses `baseBackgroundHover` +> (`#0f1623` in midnight), which is now *darker* than the resting background — +> previously hover was lighter. If that inversion is unintended, the consistent +> fix is switching hover to `controlBackgroundHover`. + +--- + +## 2. FloatingButton — primary variant background + +**Token:** primary variant now uses `baseBackgroundWeak`; secondary is unchanged. + +Both variants shared `controlBackgroundDefault`. The background is now selected +per variant, mirroring how the border color was already chosen. + +### Why this was needed + +The `variant` prop was only half-implemented. It already switched the border +color between primary and secondary, but the background was a single hard-wired +token, so the two variants were distinguishable by outline alone. Figma gives the +primary variant Base/Background/Weak, i.e. a distinct fill — the component could +not express that at all. + +The fix follows the shape of the code that was already there rather than adding a +new mechanism: a `backgroundColor` constant chosen by variant, directly parallel +to the existing `borderColor` constant. That keeps the two variant-dependent +values side by side, so the next person adding a variant sees both. + +| File | Change | +| --- | --- | +| `src/components/floating-button/styles/index.ts` | Added `backgroundColor` branch on `variant` | +| `src/components/floating-button/__tests__/floating-button.test.tsx` | Primary-variant assertion | + +```ts +const backgroundColor = + variant === "primary" + ? theme.palette.vars.baseBackgroundWeak + : theme.palette.vars.controlBackgroundDefault; +``` + +Midnight renders the primary (blue-bordered) button at `#1e293b`. Hover, active +and disabled behavior are untouched. + +--- + +## 3. Footer — AGNTCY brand icon + +The icon did not match the Figma component. The mark was re-exported from Figma +node `179577-1004`, where it is flattened into a single "powered by AGNTCY" +group — the AGNTCY path was extracted and the viewBox cropped to its measured +bounds. + +### Why this was needed + +Two separate problems, one visual and one structural. + +The **artwork was stale**: the committed path was an older AGNTCY lockup with +different letterforms and proportions from the one in the current Figma +component. No amount of resizing would have reconciled them — the geometry had to +be replaced. + +The **color was untokenized**: the story rendered `` with no color, +so the icon inherited ambient `currentColor` from the surrounding footer text and +came out grey. Figma fills the mark with the primary blue. Because the icon +declares `fill="currentColor"`, the color is a property of whoever renders it, so +this had to be fixed at the call site rather than in the icon. + +Extracting the path was not a straight export: the Figma node is a flattened +group containing both the "powered by" text and the mark, so the mark's own +bounds had to be computed from the path data (x 72.613→138) to crop the viewBox. +That is why the viewBox has a non-zero origin rather than the usual `0 0 …`. + +| File | Change | +| --- | --- | +| `src/custom-icons/brand-logos.tsx` | `AgntcyBrand` path replaced; viewBox `0 0 105 24` → `72.613 0 65.387 14.882` | +| `src/components/footer/stories/footer.stories.tsx` | Size `64×16` → `66×15`; color now tokenized | + +The icon keeps the file's existing `fill="currentColor"` convention so it themes +like the other brand logos. The story colors it with +`interactivePrimaryDefaultDefault`, which resolves to `#558bff` in midnight — +exactly the fill Figma uses for the mark — while following each theme's primary +color elsewhere. + +> **Decision point:** if the logo is meant to be `#558bff` in *every* theme (a +> fixed brand color rather than a themed one), hard-code `electricBlue500` +> instead of the token. + +--- + +## 4. Dialog — description text color + +**File:** `src/components/dialog/components/elements.tsx` + +`StyledDialogContentText` already declared `color: baseTextDefault`, but that +color was not winning at render time. MUI's `DialogContentText` injects +`color="textSecondary"` as a Typography system prop, and system-prop styles are +emitted *after* the styled override. + +That happens to be harmless in dark and midnight, where +`palette.text.secondary === baseTextDefault`. In **light** and **IoC** the two +values differ, so the dialog description was rendering the wrong color. + +The fix wraps the declaration in a doubled `&&` selector so it outranks the +injected style in every theme: + +```ts +// MUI injects color="textSecondary" as a system prop whose styles are +// emitted after this override; the doubled selector outranks it. +"&&": { + color: theme.palette.vars.baseTextDefault, +}, +``` + +Verified with a throwaway rendered test asserting the computed color equals +`baseTextDefault` in the light theme (where the values diverge). No visual change +in midnight. + +### Why this was needed + +The code was making a promise it did not keep. `elements.tsx` plainly declared +`color: baseTextDefault`, so anyone auditing dialog colors — by reading the file +or grepping for the token — would have concluded the component was correct and +moved on. It was not: in light and IoC the dialog description rendered +`palette.text.secondary` instead. + +That makes it worse than an ordinary wrong-color bug. A visibly wrong color gets +reported; this one was invisible in the two dark themes the team looks at most, +and actively defended by source code that read as correct. It would have survived +any number of token audits. + +It also set the standard of proof for the rest of this branch. Before finding +this, checking that a style function returned the right token seemed like +sufficient verification. After it, the checks in sections 5 and in "Audited — no +change required" all render the component and read `getComputedStyle`, because +returning the right token and painting the right color turned out to be different +claims. + +--- + +## 5. Storybook Docs CSS was repainting real components + +**File:** `.storybook/css/preview.css` + +This one is not a component bug — it made the **Docs tab misreport the colors of +every component in the kit**, which is what surfaced it (midnight breadcrumbs +appeared blue instead of `#e8e9ea`). + +Four rules targeted bare HTML elements under `.sbdocs-content` with +`!important`. Storybook renders stories *inline* inside that container, so the +rules were repainting the rendered components, not just the surrounding prose. + +Each selector now carries a `:not(.sb-unstyled *)` guard. `.sb-unstyled` is the +class Storybook already applies to every inline story wrapper for exactly this +purpose, so this follows the framework's own convention. + +### Why this was needed + +This is the most consequential fix in the branch, because it was not breaking a +component — it was breaking the instrument the team uses to check components. + +The Docs tab is where token work gets verified. While these rules were in place, +the Docs tab was showing Storybook's own chrome colors on top of the real +components, so it could not be trusted for exactly the task it was being used +for. Every "does this token look right?" judgement made in Docs was potentially +answering a question about Storybook's CSS instead. + +The reach was wide. Because MUI renders `Typography` `body1`/`body2`/default as a +`

`, the muted-text rule covered ordinary body text across roughly 39 component +files — including `DialogContentText`, meaning the dialog fix in section 4 would +still have *appeared* wrong in Docs afterwards. Two independent bugs were +stacking on the same element, and only fixing both makes the component readable. + +The specific trigger was noticing midnight breadcrumbs rendering `#558BFF` when +the token resolves to `#e8e9ea`. Rendering `Breadcrumbs` under `midnightTheme` +returned the correct `rgb(232, 233, 234)`, which ruled out the component and +pointed at the environment. + +`!important` on a bare-element selector is what made it unfixable from the +component side: no Emotion class could outrank it, so no amount of correcting +component styles would have helped. The guard was applied at the source rather +than by escalating specificity in components, which would have spread the problem +across the kit. + +Rules that were leaking, and what they overrode inside stories: + +| Selector | Impact | +| --- | --- | +| `.sbdocs-content a` | Every link — Breadcrumbs, Footer, Link | +| `.sbdocs-content p`, `li` | Any `

`/`

  • `. MUI `Typography` renders `body1`/`body2`/default as `

    `, so ~39 component files were affected — including `DialogContentText` | +| `.sbdocs-content h1`–`h4` | Real headings, plus forcing Sharp Sans and `letter-spacing: 0` | +| `.sbdocs-content p code`, `li code` | Code inside those elements | + +The forced link color is theme-dependent (`preview.ts:88`); in midnight it is +`#558BFF`, which is why breadcrumbs looked blue. + +**Scope of the fix:** it only *narrows* selectors, so it can never add styling — +docs prose is unaffected. Inside story previews, elements now show their real +token colors. The Canvas tab was never affected (no `.sbdocs-content` ancestor), +so Canvas and Docs should now agree; a disagreement between the two tabs is a +useful signal of a genuine bug. + +`Typography variant="caption"` renders a `` and was never in these +selectors — the Footer copyright text, for example, was always correct. + +--- + +## Audited — no change required + +Two items were investigated and found already correct. Recording them so they +are not "fixed" into a regression later — in both cases there is a plausible +wrong answer sitting right next to the right one, and a future reader without +this note could easily talk themselves into it. + +**Breadcrumb link text** already uses `interactiveSecondaryDefaultDefault`, +resolved through `Link`'s `customizeColor` callback. Confirmed by rendering the +component and reading the computed color (`#062242` in light). The separator +chevron and collapsed "…" trigger use the same token; the collapsed dropdown's +menu items intentionally use `baseTextDefault`. + +**`interactiveSecondaryDefaultDefault` already matches the Agntcy Light / Agntcy +Dark columns** in light and dark. Read directly from the Figma variable +(`Tokens` collection, library "Outshift Spark Foundations"): + +| Figma mode | Alias → value | Codebase | +| --- | --- | --- | +| **Agntcy Light** | Surface Dark/600 → `#062242` | light: `surfaceDarkPalette[600]` ✅ | +| **Agntcy Dark** | Surface Light/300 → `#e8eefb` | dark: `surfaceLightPalette[300]` ✅ | +| Light | Surface Dark/500 → `#0d274d` | not used | +| Dark | Surface Light/300 → `#e8eefb` | not used | +| Agntcy Midnight | Grey/50 → `#e8e9ea` | midnight: `greyPalette[50]` ✅ | +| IoC | Surface Light/300 → `#e8eefb` | ioc: `iocBluePalette[500]` = `#2B82F6` ⚠️ | + +Note that the plain "Light" column is a *different* color from Agntcy Light. The +codebase deliberately follows the Agntcy columns; changing light to `#0d274d` +would move away from the intended spec. + +The hover / active / disabled states of the same family also match Agntcy +Light/Dark exactly. + +--- + +## Open questions for design + +1. **Midnight disabled state is brighter than the resting state.** + `interactiveSecondaryDefaultDisabled` is Grey/0 (`#ffffff`) while resting is + Grey/50 (`#e8e9ea`), so disabled links appear *more* prominent than enabled + ones. This was confirmed in the Figma token set — the code mirrors it + faithfully, so it is a design-side question, not a code bug. Every other + theme dims disabled with alpha. + +2. **IoC diverges from Figma on the `interactiveSecondary` family.** Figma + specifies Surface Light/300 (`#e8eefb`); the code uses `iocBluePalette[500]` + (`#2B82F6`). This resembles the deliberate IoC deviation documented in + `ioc-vars.ts:93`, but this one carries no explanatory comment. Either align it + to Figma or add a comment recording why it differs. + +3. **Header dropdown hover** — see the follow-up note in section 1. + +--- + +## Verification + +| Suite | Result | +| --- | --- | +| `header.test.tsx` | 19 passed | +| `footer.test.tsx` | 9 passed | +| `floating-button` | 12 passed | +| `dialog.test.tsx` | 7 passed | +| `tsc --noEmit` | clean | +| `prettier --check` on `preview.css` | clean | + +Storybook selector guards were verified by building the Docs DOM structure in +jsdom and running the old and new selectors against it: the old selectors matched +docs content *and* story content, the new ones match only docs content. + +Tests run with Node 22 (`~/.nvm/versions/node/v22.23.1`). The default `node` +v20.9.0 on this machine is too old for the repo's Vitest/Rolldown toolchain, and +this package's suite is Jest — `npx jest --config=jest.config.js`. + +Temporary verification tests written during this work were deleted after use; +none remain in the tree. diff --git a/packages/open-ui-kit/.storybook/css/preview.css b/packages/open-ui-kit/.storybook/css/preview.css index 6da24129..84c1327a 100644 --- a/packages/open-ui-kit/.storybook/css/preview.css +++ b/packages/open-ui-kit/.storybook/css/preview.css @@ -50,11 +50,16 @@ body { max-width: 1040px; } +/* The bare-element selectors below must exclude `.sb-unstyled` subtrees. + Storybook renders stories inline inside `.sbdocs-content`, so an unguarded + `.sbdocs-content a` (etc.) repaints the rendered components themselves and + the Docs tab stops showing their real token colors. */ + .sbdocs-title, -.sbdocs-content h1, -.sbdocs-content h2, -.sbdocs-content h3, -.sbdocs-content h4 { +.sbdocs-content h1:not(.sb-unstyled *), +.sbdocs-content h2:not(.sb-unstyled *), +.sbdocs-content h3:not(.sb-unstyled *), +.sbdocs-content h4:not(.sb-unstyled *) { color: var(--ouk-storybook-text) !important; font-family: "Sharp Sans", @@ -69,20 +74,20 @@ body { .sbdocs-subtitle, .sbdocs-p, .sbdocs-li, -.sbdocs-content p, -.sbdocs-content li { +.sbdocs-content p:not(.sb-unstyled *), +.sbdocs-content li:not(.sb-unstyled *) { color: var(--ouk-storybook-muted-text) !important; } .sbdocs-a, -.sbdocs-content a { +.sbdocs-content a:not(.sb-unstyled *) { color: var(--ouk-storybook-link) !important; } .sbdocs-p code, .sbdocs-li code, -.sbdocs-content p code, -.sbdocs-content li code { +.sbdocs-content p:not(.sb-unstyled *) code, +.sbdocs-content li:not(.sb-unstyled *) code { background: var(--ouk-storybook-preview-bg) !important; border: 1px solid var(--ouk-storybook-border) !important; border-radius: 4px !important; diff --git a/packages/open-ui-kit/jest.config.js b/packages/open-ui-kit/jest.config.js index a08cc5d7..15995433 100644 --- a/packages/open-ui-kit/jest.config.js +++ b/packages/open-ui-kit/jest.config.js @@ -20,8 +20,31 @@ module.exports = { transform: { "^.+\\.(ts|tsx|js|jsx)$": ["ts-jest", { tsconfig: "tsconfig.json" }], }, + // CodeBlock patches the refractor grammar (see components/code-block/ + // prism-grammar.ts). refractor and its hast/parse-entities dependencies are + // ESM-only, so they have to be transformed rather than skipped. transformIgnorePatterns: [ - "node_modules/(?!(lodash-es|@mui|@babel/runtime)/)", + `node_modules/(?!(${[ + "lodash-es", + "@mui", + "@babel/runtime", + // refractor and every ESM package in its dependency closure. + "refractor", + "character-entities", + "character-entities-legacy", + "character-reference-invalid", + "comma-separated-tokens", + "decode-named-character-reference", + "hast-util-parse-selector", + "hastscript", + "is-alphabetical", + "is-alphanumerical", + "is-decimal", + "is-hexadecimal", + "parse-entities", + "property-information", + "space-separated-tokens", + ].join("|")})/)`, ], modulePathIgnorePatterns: ["/dist/"], // Ignore the dist directory to avoid Haste module naming collisions testMatch: ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[tj]s?(x)"], // Ensure test files are matched diff --git a/packages/open-ui-kit/package.json b/packages/open-ui-kit/package.json index 32b093e3..6ccc7928 100644 --- a/packages/open-ui-kit/package.json +++ b/packages/open-ui-kit/package.json @@ -181,6 +181,7 @@ "react-syntax-highlighter": "^16.1.1", "react-virtuoso": "^4.18.7", "recharts": "^2.15.3", + "refractor": "^5.0.0", "sonner": "^2.0.5", "use-debounce": "^10.1.1", "zustand": "^5.0.14" diff --git a/packages/open-ui-kit/src/components/accordion/components/accordion.tsx b/packages/open-ui-kit/src/components/accordion/components/accordion.tsx index 651057a3..20b5ef4c 100644 --- a/packages/open-ui-kit/src/components/accordion/components/accordion.tsx +++ b/packages/open-ui-kit/src/components/accordion/components/accordion.tsx @@ -32,6 +32,7 @@ export const Accordion = ({ action, endSlot, showDivider, + showBorder, accordionSummaryProps, detailsContentBoxProps, children, @@ -41,9 +42,14 @@ export const Accordion = ({ const summaryTextLineHeight = size === "large" ? "24px" : "20px"; const mediumSize = size === "medium"; const shouldShowDivider = showDivider ?? (mediumSize && !contained); + const shouldShowBorder = showBorder ?? (mediumSize && !contained); return ( - + prop !== "contained" && prop !== "mediumSize", -})<{ contained?: boolean; mediumSize?: boolean }>( - ({ theme, contained, mediumSize }) => ({ + shouldForwardProp: (prop) => prop !== "contained" && prop !== "showBorder", +})<{ contained?: boolean; showBorder?: boolean }>( + ({ theme, contained, showBorder }) => ({ padding: 0, color: theme.palette.vars.baseTextStrong, backgroundColor: "transparent", @@ -36,7 +36,7 @@ export const StyledAccordion = styled(Accordion, { color: theme.palette.vars.baseTextDisabled, backgroundColor: "transparent", }, - ...(mediumSize && + ...(showBorder && !contained && { borderTop: `1px solid ${theme.palette.vars.controlBorderDefault}`, }), @@ -54,7 +54,7 @@ export const StyledAccordion = styled(Accordion, { }), }), ) as ComponentType< - AccordionProps & { contained?: boolean; mediumSize?: boolean } + AccordionProps & { contained?: boolean; showBorder?: boolean } >; export const StyledAccordionSummary = styled(AccordionSummary, { diff --git a/packages/open-ui-kit/src/components/accordion/types/index.ts b/packages/open-ui-kit/src/components/accordion/types/index.ts index b7e14b99..07c49cae 100644 --- a/packages/open-ui-kit/src/components/accordion/types/index.ts +++ b/packages/open-ui-kit/src/components/accordion/types/index.ts @@ -40,6 +40,8 @@ export interface AccordionProps extends MuiAccordionProps { endSlot?: ReactNode; /** Overrides the default summary divider visibility. Medium uncontained accordions show it by default. */ showDivider?: boolean; + /** Overrides the default top border visibility. Medium uncontained accordions show it by default. */ + showBorder?: boolean; /** Props forwarded to the internal MUI AccordionSummary. */ accordionSummaryProps?: AccordionSummaryProps; /** Props forwarded to the details content wrapper. */ diff --git a/packages/open-ui-kit/src/components/activity-timeline/components/activity-timeline.tsx b/packages/open-ui-kit/src/components/activity-timeline/components/activity-timeline.tsx index a5edc544..2ad2db9a 100644 --- a/packages/open-ui-kit/src/components/activity-timeline/components/activity-timeline.tsx +++ b/packages/open-ui-kit/src/components/activity-timeline/components/activity-timeline.tsx @@ -74,6 +74,10 @@ export const ActivityTimeline = ({ titleStartIcon={step.titleStartIcon} subTitle={step.subTitle} size={size} + // The timeline supplies its own separators, so the medium + // accordion's top border and summary divider are redundant here. + showBorder={false} + showDivider={false} > {step.content} diff --git a/packages/open-ui-kit/src/components/checkbox/stories/checkbox.stories.tsx b/packages/open-ui-kit/src/components/checkbox/stories/checkbox.stories.tsx index 640da6b7..c84f91dd 100644 --- a/packages/open-ui-kit/src/components/checkbox/stories/checkbox.stories.tsx +++ b/packages/open-ui-kit/src/components/checkbox/stories/checkbox.stories.tsx @@ -254,7 +254,9 @@ export const BareStates: Story = { {...args} {...checkboxStateProps(checkState, visualState)} /> - {stateLabel[checkState]} + + {stateLabel[checkState]} + ))} diff --git a/packages/open-ui-kit/src/components/code-block/__tests__/code-block.test.tsx b/packages/open-ui-kit/src/components/code-block/__tests__/code-block.test.tsx index 515b8db4..2a0b1edf 100644 --- a/packages/open-ui-kit/src/components/code-block/__tests__/code-block.test.tsx +++ b/packages/open-ui-kit/src/components/code-block/__tests__/code-block.test.tsx @@ -6,6 +6,7 @@ import { render, screen } from "@testing-library/react"; import "@testing-library/jest-dom"; +import { refractor } from "refractor/all"; import { ThemeMode, ThemeProvider } from "@/theme-provider/theme-provider"; import { darkTheme } from "@/theme/dark/dark-theme"; import { lightTheme } from "@/theme/light/light-theme"; @@ -13,7 +14,9 @@ import { CodeBlock } from "../components/code-block"; import { codeTextStyle, containerStackStyles, + customStyle, lineNumberStyle, + prismStyle, separatorFirstBox, } from "../styles"; import type { CodeBlockProps } from "../types"; @@ -21,6 +24,14 @@ import type { CodeBlockProps } from "../types"; const CODE = `const x = 1;\nconsole.log(x);`; const noop = jest.fn(); +/** Minimal shape of the hast tree `refractor.highlight` returns. */ +type HastNode = { + type: string; + value?: string; + properties?: { className?: string[] }; + children?: HastNode[]; +}; + const renderCodeBlock = (props: Partial = {}, dark = false) => render( @@ -91,7 +102,7 @@ describe("CodeBlock", () => { expect(containerStackStyles(lightTheme)).toEqual( expect.objectContaining({ backgroundColor: lightTheme.palette.vars.controlBackgroundDefault, - border: `1px solid ${lightTheme.palette.vars.controlBorderDefault}`, + border: `1px solid ${lightTheme.palette.vars.controlBorderWeak}`, borderRadius: "6px", }), ); @@ -154,7 +165,7 @@ describe("CodeBlock", () => { expect(containerStackStyles(darkTheme)).toEqual( expect.objectContaining({ backgroundColor: darkTheme.palette.vars.controlBackgroundDefault, - border: `1px solid ${darkTheme.palette.vars.controlBorderDefault}`, + border: `1px solid ${darkTheme.palette.vars.controlBorderWeak}`, borderRadius: "6px", }), ); @@ -170,6 +181,159 @@ describe("CodeBlock", () => { }); }); + describe("syntax colors", () => { + it("maps grammar tokens to the Figma accent ramp", () => { + const light = prismStyle(lightTheme); + + expect(light).toEqual( + expect.objectContaining({ + comment: { color: lightTheme.palette.vars.accentEDefault }, + keyword: { color: lightTheme.palette.vars.accentADefault }, + arrow: { color: lightTheme.palette.vars.accentADefault }, + "control-flow": { color: lightTheme.palette.vars.accentBDefault }, + function: { color: lightTheme.palette.vars.accentFDefault }, + "declaration-name": { + color: lightTheme.palette.vars.accentGDefault, + }, + "class-name": { color: lightTheme.palette.vars.accentJDefault }, + parameter: { color: lightTheme.palette.vars.accentHDefault }, + identifier: { color: lightTheme.palette.vars.accentHDefault }, + number: { color: lightTheme.palette.vars.successTextDefault }, + punctuation: { color: lightTheme.palette.vars.baseTextStrong }, + }), + ); + }); + + it("resolves the Figma frame's literal syntax colors in light mode", () => { + const light = prismStyle(lightTheme); + + // Values read from the Figma "Code block" frame variable definitions. + expect(light.keyword).toEqual({ color: "#5c6ddd" }); // Accent/A + expect(light["control-flow"]).toEqual({ color: "#b8428c" }); // Accent/B + expect(light.comment).toEqual({ color: "#7da11b" }); // Accent/E + expect(light.function).toEqual({ color: "#e8361a" }); // Accent/F + expect(light["declaration-name"]).toEqual({ color: "#46aace" }); // Accent/G + expect(light.parameter).toEqual({ color: "#1c2b7f" }); // Accent/H + expect(light.identifier).toEqual({ color: "#1c2b7f" }); // Accent/H + expect(light["class-name"]).toEqual({ color: "#028e99" }); // Accent/J + expect(light.number).toEqual({ color: "#00b285" }); // Success/Text/Default + }); + + it("gives control flow a different color from other keywords", () => { + // Accent/B previously sat on `regex`/`constant`, which the JavaScript + // samples never produce, so it painted nothing and `return`/`await` + // rendered as Accent/A. These must stay distinct in every theme. + for (const theme of [lightTheme, darkTheme]) { + const style = prismStyle(theme); + expect(style["control-flow"]).not.toEqual(style.keyword); + expect(style["declaration-name"]).not.toEqual(style.punctuation); + expect(style.identifier).not.toEqual(style.punctuation); + } + }); + + it("follows the active theme instead of baking in one ramp", () => { + const light = prismStyle(lightTheme); + const dark = prismStyle(darkTheme); + + expect(dark.keyword).toEqual({ + color: darkTheme.palette.vars.accentADefault, + }); + expect(dark.keyword).not.toEqual(light.keyword); + expect(customStyle(darkTheme).color).toBe( + darkTheme.palette.vars.baseTextStrong, + ); + }); + + it("hands the theme-resolved palette to the highlighter", () => { + const { container, unmount } = renderCodeBlock(); + const readPalette = () => + JSON.parse( + container.querySelector("pre")?.getAttribute("data-prism-style") ?? + "{}", + ); + + expect(readPalette()).toEqual(prismStyle(lightTheme)); + expect(container.querySelector("pre")).toHaveStyle({ + color: lightTheme.palette.vars.baseTextStrong, + }); + + unmount(); + const darkRender = renderCodeBlock({}, true); + + expect( + JSON.parse( + darkRender.container + .querySelector("pre") + ?.getAttribute("data-prism-style") ?? "{}", + ), + ).toEqual(prismStyle(darkTheme)); + }); + }); + + // `react-syntax-highlighter` is mocked in jest.config.js, so the rendered + // output has no real token spans and the palette tests above can only prove + // that a color is *assigned* to a token name. These drive the same refractor + // grammar the component uses, to prove the token names are ones the language + // actually emits — the failure mode this change exists to fix. + describe("grammar coverage for the Figma roles", () => { + const SAMPLE = [ + "function resolveAfter2Seconds(x) {", + " return new Promise((resolve) => {", + " setTimeout(() => { resolve(x); }, 2000);", + " });", + "}", + "const p1 = await resolveAfter2Seconds(20);", + "return x + p1; // done", + "console.log(p1);", + ].join("\n"); + + // refractor nests tokens — a `parameter` whose inside-grammar matched an + // `identifier` renders as . + // Collect the whole ancestor chain so either role can be asserted. + const classesFor = (text: string): string[] => { + const found: string[][] = []; + const walk = (node: HastNode, inherited: string[]) => { + for (const child of node.children ?? []) { + if (child.type === "text") { + if (child.value === text) found.push(inherited); + continue; + } + const classes = [ + ...inherited, + ...(child.properties?.className ?? []).filter((c) => c !== "token"), + ]; + walk(child, classes); + } + }; + walk(refractor.highlight(SAMPLE, "javascript") as unknown as HastNode, []); + return found[0] ?? []; + }; + + it.each([ + ["return", "control-flow"], // Accent/B + ["await", "control-flow"], // Accent/B + ["p1", "declaration-name"], // Accent/G + ["x", "identifier"], // Accent/H + ["console", "identifier"], // Accent/H + ["=>", "arrow"], // Accent/A + ["resolve", "parameter"], // Accent/H + ["Promise", "class-name"], // Accent/J + ["2000", "number"], // Success/Text/Default + ])("tokenizes %s as %s", (text, expected) => { + expect(classesFor(text)).toContain(expected); + }); + + it("keeps control flow separate from ordinary keywords", () => { + expect(classesFor("const")).not.toContain("control-flow"); + expect(classesFor("return")).toContain("control-flow"); + }); + + it("leaves a function binding on the function role, not the declaration role", () => { + // `const add = function` is a function, so Accent/F outranks Accent/G. + expect(classesFor("resolveAfter2Seconds")).toContain("function"); + }); + }); + describe("size prop", () => { it("renders size=medium without throwing", () => { expect(() => renderCodeBlock({ size: "medium" })).not.toThrow(); diff --git a/packages/open-ui-kit/src/components/code-block/components/code-block.tsx b/packages/open-ui-kit/src/components/code-block/components/code-block.tsx index 127b751c..afcdd364 100644 --- a/packages/open-ui-kit/src/components/code-block/components/code-block.tsx +++ b/packages/open-ui-kit/src/components/code-block/components/code-block.tsx @@ -16,6 +16,10 @@ import { } from "@/components/code-block/styles"; import React from "react"; import { Prism, type SyntaxHighlighterProps } from "react-syntax-highlighter"; +// Adds the `declaration-name` and `identifier` tokens that `prismStyle` paints +// with Accent/G and Accent/H. Imported for its side effect on the shared +// refractor grammar. +import "@/components/code-block/prism-grammar"; import { Separator } from "./separator"; import { CopyButton } from "@/components/copy-button"; import type { CodeBlockProps } from "../types"; @@ -83,7 +87,7 @@ export const CodeBlock = (props: CodeBlockProps) => { /> `, and `parameter` carries declaration- +// site parameters. Those only needed stylesheet keys, which live in `styles`. +// +// This mutates the refractor singleton that `react-syntax-highlighter`'s +// `Prism` export is bound to — there is one copy in the tree, and both reach it +// through the same `refractor/all` specifier. Guarded so repeated imports from +// multiple entry points apply it once. +let patched = false; + +export const patchJavaScriptGrammar = (): void => { + if (patched) return; + patched = true; + + // Inserted before `keyword` so the patterns that precede it still win: + // `function-variable` keeps `const add = function` on Accent/F, and + // `class-name` keeps `Promise` on Accent/J. + refractor.languages.insertBefore("javascript", "keyword", { + "declaration-name": { + pattern: /((?:\b(?:const|let|var)\s+))[A-Za-z_$][\w$]*/, + lookbehind: true, + }, + }); + + // `Grammar` types only the tokens prismjs ships, so reach the rest through + // an index signature rather than widening the upstream type. + const javascript = refractor.languages.javascript as Record; + + // Appended last, so it only claims identifiers no earlier pattern matched. + javascript.identifier = /\b[A-Za-z_$][\w$]*\b/; + + // The frame paints `console` as an ordinary identifier reference. refractor + // has a dedicated `console` token that nests `class-name`; since + // react-syntax-highlighter resolves overlapping classes in array order and + // `class-name` lands last, a `console` stylesheet key could not outrank it. + // Removing the token lets `console` fall through to `identifier` instead. + delete javascript.console; +}; + +patchJavaScriptGrammar(); diff --git a/packages/open-ui-kit/src/components/code-block/styles/index.ts b/packages/open-ui-kit/src/components/code-block/styles/index.ts index e5be79e3..0ed98fcb 100644 --- a/packages/open-ui-kit/src/components/code-block/styles/index.ts +++ b/packages/open-ui-kit/src/components/code-block/styles/index.ts @@ -9,7 +9,9 @@ import type { CSSProperties } from "react"; export const containerStackStyles = (theme: Theme): CSSProperties => ({ backgroundColor: theme.palette.vars.controlBackgroundDefault, - border: `1px solid ${theme.palette.vars.controlBorderDefault}`, + // Figma binds the card outline to Control/Border/Weak; Control/Border/Default + // is reserved for the controls inside it, such as the copy button. + border: `1px solid ${theme.palette.vars.controlBorderWeak}`, borderRadius: "6px", position: "relative", }); @@ -35,7 +37,9 @@ export const customStyle = ( margin: "0", backgroundColor: theme.palette.vars.controlBackgroundDefault, borderRadius: showLineNumbers ? "0 0 4px 4px" : "4px", - color: theme.palette.vars.baseTextDefault, + // Figma paints the code area of a highlighted block with Base/Text/Strong; + // unhighlighted grammar tokens inherit this color. + color: theme.palette.vars.baseTextStrong, }; }; @@ -115,42 +119,114 @@ export const headerButtonStyles = (theme: Theme): CSSProperties => ({ color: theme.palette.vars.brandIconPrimaryDefault, }); -// Syntax tokens intentionally keep Prism palette literals; no Spark semantic -// tokens exist for language grammar colors in the current design system. -export const prismStyle: { [key: string]: CSSProperties } = { - 'pre[class*="language-"]': { background: "transparent", textShadow: "none" }, - 'code[class*="language-"]': { background: "transparent", textShadow: "none" }, - comment: { color: "slategray" }, - prolog: { color: "slategray" }, - doctype: { color: "slategray" }, - cdata: { color: "slategray" }, - punctuation: { color: "#999" }, - property: { color: "#905" }, - tag: { color: "#905" }, - boolean: { color: "#905" }, - number: { color: "#905" }, - constant: { color: "#905" }, - symbol: { color: "#905" }, - deleted: { color: "#905" }, - selector: { color: "#690" }, - "attr-name": { color: "#690" }, - string: { color: "#690" }, - char: { color: "#690" }, - builtin: { color: "#690" }, - inserted: { color: "#690" }, - operator: { color: "#9a6e3a" }, - entity: { color: "#9a6e3a", cursor: "help" }, - url: { color: "#9a6e3a" }, - ".language-css .token.string": { color: "#9a6e3a" }, - ".style .token.string": { color: "#9a6e3a" }, - atrule: { color: "#07a" }, - "attr-value": { color: "#07a" }, - keyword: { color: "#07a" }, - function: { color: "#DD4A68" }, - "class-name": { color: "#DD4A68" }, - regex: { color: "#e90" }, - important: { color: "#e90", fontWeight: "bold" }, - variable: { color: "#e90" }, - bold: { fontWeight: "bold" }, - italic: { fontStyle: "italic" }, +// Syntax colors follow the Figma "Code block" frame, which paints language +// grammar with the Spark accent ramp instead of a stock Prism palette: +// +// Accent/A keywords (function, const, new, async) and the arrow `=>` +// Accent/B control-flow keywords (return, await) +// Accent/E comments +// Accent/F function names and call sites +// Accent/G declaration names (const a = ...) +// Accent/H parameters and identifier references +// Accent/J classes and constructors (Promise) +// Success/Text/Default numeric literals +// Base/Text/Strong punctuation and unclassified code +// +// The highlighter is `react-syntax-highlighter`'s full `Prism` export, which is +// bound to `refractor/all` rather than the stock prismjs grammar. refractor +// emits a richer token set, so most of the frame maps directly: `control-flow` +// splits `return`/`await` off `keyword`, `arrow` separates `=>` from the other +// operators, and `parameter` covers declaration-site parameters. +// +// Two roles have no refractor token — the binding name in a declaration and a +// bare identifier reference. `prism-grammar.ts` adds `declaration-name` and +// `identifier` for those; without that module they fall back to punctuation. +// +// Taking tokens rather than the frame's literals also fixes the theme: the +// Figma midnight frame resolves the accent ramp to its light-theme values, +// which is why parameters and punctuation are barely legible there. Reading +// through `theme.palette.vars` gives each theme its own ramp. +export const prismStyle = (theme: Theme): { [key: string]: CSSProperties } => { + const { vars } = theme.palette; + + return { + 'pre[class*="language-"]': { background: "transparent", textShadow: "none" }, + 'code[class*="language-"]': { + background: "transparent", + textShadow: "none", + }, + + // Punctuation and operators stay on the code area's own text color. + punctuation: { color: vars.baseTextStrong }, + operator: { color: vars.baseTextStrong }, + + // Comments — Accent/E + comment: { color: vars.accentEDefault }, + prolog: { color: vars.accentEDefault }, + doctype: { color: vars.accentEDefault }, + cdata: { color: vars.accentEDefault }, + + // Keywords — Accent/A. `arrow` is a sub-token of `operator`; the frame + // paints `=>` with the keyword color, and class order lets it win. + keyword: { color: vars.accentADefault }, + atrule: { color: vars.accentADefault }, + arrow: { color: vars.accentADefault }, + + // Control-flow keywords — Accent/B. refractor tags these with both + // `keyword` and `control-flow`; `control-flow` is last, so it wins. + "control-flow": { color: vars.accentBDefault }, + + // Function names and call sites — Accent/F + function: { color: vars.accentFDefault }, + "function-variable": { color: vars.accentFDefault }, + + // Classes, constructors and language builtins — Accent/J + "class-name": { color: vars.accentJDefault }, + builtin: { color: vars.accentJDefault }, + entity: { color: vars.accentJDefault, cursor: "help" }, + + // Parameters, identifiers and property names — Accent/H. + // `identifier` comes from `prism-grammar.ts` and catches bare references + // (`x`, `b`, `console`) that no other pattern claimed. `variable` carries + // refractor's `dom` token, so `document` matches `console` rather than + // rendering as attention/regex. + parameter: { color: vars.accentHDefault }, + identifier: { color: vars.accentHDefault }, + variable: { color: vars.accentHDefault }, + property: { color: vars.accentHDefault }, + "literal-property": { color: vars.accentHDefault }, + "string-property": { color: vars.accentHDefault }, + "attr-name": { color: vars.accentHDefault }, + tag: { color: vars.accentHDefault }, + selector: { color: vars.accentHDefault }, + + // Declaration binding names — Accent/G. Also from `prism-grammar.ts`. + "declaration-name": { color: vars.accentGDefault }, + + // Literals — Success/Text/Default. The Figma sample is JavaScript with no + // string in it, so it does not specify a string color; grouping strings + // with numbers keeps every literal on one token now that Accent/G carries + // declaration names. + number: { color: vars.successTextDefault }, + boolean: { color: vars.successTextDefault }, + string: { color: vars.successTextDefault }, + char: { color: vars.successTextDefault }, + "attr-value": { color: vars.successTextDefault }, + url: { color: vars.successTextDefault }, + inserted: { color: vars.successTextDefault }, + ".language-css .token.string": { color: vars.successTextDefault }, + ".style .token.string": { color: vars.successTextDefault }, + + // Regex, symbols and emphasis — Accent/B, alongside control flow above. + regex: { color: vars.accentBDefault }, + "regex-delimiter": { color: vars.accentBDefault }, + "regex-source": { color: vars.accentBDefault }, + constant: { color: vars.accentBDefault }, + symbol: { color: vars.accentBDefault }, + deleted: { color: vars.accentBDefault }, + important: { color: vars.accentBDefault, fontWeight: "bold" }, + + bold: { fontWeight: "bold" }, + italic: { fontStyle: "italic" }, + }; }; diff --git a/packages/open-ui-kit/src/components/dialog/components/elements.tsx b/packages/open-ui-kit/src/components/dialog/components/elements.tsx index e9d2146b..253f60af 100644 --- a/packages/open-ui-kit/src/components/dialog/components/elements.tsx +++ b/packages/open-ui-kit/src/components/dialog/components/elements.tsx @@ -74,5 +74,9 @@ export const StyledDialogActions: ComponentType = styled( export const StyledDialogContentText: ComponentType = styled(MuiDialogContentText)(({ theme }) => ({ ...theme.typography.body2, - color: theme.palette.vars.baseTextDefault, + // MUI injects color="textSecondary" as a system prop whose styles are + // emitted after this override; the doubled selector outranks it. + "&&": { + color: theme.palette.vars.baseTextDefault, + }, })); diff --git a/packages/open-ui-kit/src/components/floating-button/__tests__/floating-button.test.tsx b/packages/open-ui-kit/src/components/floating-button/__tests__/floating-button.test.tsx index ccd39ab1..59aef2f5 100644 --- a/packages/open-ui-kit/src/components/floating-button/__tests__/floating-button.test.tsx +++ b/packages/open-ui-kit/src/components/floating-button/__tests__/floating-button.test.tsx @@ -97,7 +97,7 @@ describe("FloatingButton", () => { describe("token styles", () => { it("uses light theme tokens for primary styling", () => { expect(getFloatingButtonStyles(lightTheme, "primary")).toMatchObject({ - background: `${lightTheme.palette.vars.controlBackgroundDefault} !important`, + background: `${lightTheme.palette.vars.baseBackgroundWeak} !important`, border: `2px solid ${lightTheme.palette.vars.interactivePrimaryDefaultDefault} !important`, color: `${lightTheme.palette.vars.baseTextStrong} !important`, boxShadow: lightTheme.shadows[4], diff --git a/packages/open-ui-kit/src/components/floating-button/styles/index.ts b/packages/open-ui-kit/src/components/floating-button/styles/index.ts index 04dcbd18..f8c0cc9b 100644 --- a/packages/open-ui-kit/src/components/floating-button/styles/index.ts +++ b/packages/open-ui-kit/src/components/floating-button/styles/index.ts @@ -16,10 +16,15 @@ export const getFloatingButtonStyles = ( ? theme.palette.vars.interactivePrimaryDefaultDefault : theme.palette.vars.controlBorderDefault; + const backgroundColor = + variant === "primary" + ? theme.palette.vars.baseBackgroundWeak + : theme.palette.vars.controlBackgroundDefault; + return { borderRadius: "100px", boxShadow: theme.shadows[4], - background: `${theme.palette.vars.controlBackgroundDefault} !important`, + background: `${backgroundColor} !important`, color: `${theme.palette.vars.baseTextStrong} !important`, border: `2px solid ${borderColor} !important`, letterSpacing: "0.1px", diff --git a/packages/open-ui-kit/src/components/footer/stories/footer.stories.tsx b/packages/open-ui-kit/src/components/footer/stories/footer.stories.tsx index e5c2e516..e13d3f7d 100644 --- a/packages/open-ui-kit/src/components/footer/stories/footer.stories.tsx +++ b/packages/open-ui-kit/src/components/footer/stories/footer.stories.tsx @@ -28,7 +28,14 @@ const ProductNode = () => ( > {`© ${new Date().getFullYear()} Cisco Systems Inc. • powered by`} - + ({ + width: 66, + height: 15, + flexShrink: 0, + color: theme.palette.vars.interactivePrimaryDefaultDefault, + })} + /> ({ diff --git a/packages/open-ui-kit/src/components/header/__tests__/header.test.tsx b/packages/open-ui-kit/src/components/header/__tests__/header.test.tsx index a9d8271a..6aefea04 100644 --- a/packages/open-ui-kit/src/components/header/__tests__/header.test.tsx +++ b/packages/open-ui-kit/src/components/header/__tests__/header.test.tsx @@ -210,7 +210,7 @@ describe("Header", () => { height: "96px", borderRadius: "8px", border: `2px solid ${lightTheme.palette.vars.interactivePrimaryDefaultActive}`, - backgroundColor: lightTheme.palette.vars.baseBackgroundMedium, + backgroundColor: lightTheme.palette.vars.controlBackgroundWeak, boxShadow: lightTheme.shadows[2], padding: "8px 0", }), @@ -225,7 +225,7 @@ describe("Header", () => { height: "40px", padding: "8px 16px", color: darkTheme.palette.vars.baseTextDefault, - backgroundColor: darkTheme.palette.vars.baseBackgroundMedium, + backgroundColor: darkTheme.palette.vars.controlBackgroundWeak, "&:hover": { backgroundColor: darkTheme.palette.vars.baseBackgroundHover, }, diff --git a/packages/open-ui-kit/src/components/header/styles/index.ts b/packages/open-ui-kit/src/components/header/styles/index.ts index 167b4cdd..54a46c63 100644 --- a/packages/open-ui-kit/src/components/header/styles/index.ts +++ b/packages/open-ui-kit/src/components/header/styles/index.ts @@ -153,7 +153,7 @@ export const getStoryMenuPaperStyles = (theme: Theme) => ({ boxSizing: "border-box", borderRadius: "8px", border: `2px solid ${theme.palette.vars.interactivePrimaryDefaultActive}`, - backgroundColor: theme.palette.vars.baseBackgroundMedium, + backgroundColor: theme.palette.vars.controlBackgroundWeak, boxShadow: theme.shadows[2], padding: "8px 0", }, @@ -166,7 +166,7 @@ export const getStoryMenuItemStyles = (theme: Theme) => ({ height: "40px", padding: "8px 16px", color: theme.palette.vars.baseTextDefault, - backgroundColor: theme.palette.vars.baseBackgroundMedium, + backgroundColor: theme.palette.vars.controlBackgroundWeak, "&:hover": { backgroundColor: theme.palette.vars.baseBackgroundHover, }, diff --git a/packages/open-ui-kit/src/components/message/__tests__/message.test.tsx b/packages/open-ui-kit/src/components/message/__tests__/message.test.tsx index 644e5180..8bedc058 100644 --- a/packages/open-ui-kit/src/components/message/__tests__/message.test.tsx +++ b/packages/open-ui-kit/src/components/message/__tests__/message.test.tsx @@ -116,7 +116,7 @@ describe("Message", () => { height: "auto", padding: "12px 16px", gap: "12px", - background: lightTheme.palette.vars.baseBackgroundWeak, + background: lightTheme.palette.vars.controlBackgroundDefault, color: lightTheme.palette.vars.baseTextDefault, borderWidth: "1px 1px 1px 4px", borderRadius: "4px", @@ -206,7 +206,7 @@ describe("Message", () => { ).toMatchObject({ width: "480px", height: "auto", - background: darkTheme.palette.vars.baseBackgroundWeak, + background: darkTheme.palette.vars.controlBackgroundDefault, }); expect(getMessageActionStyles(darkTheme)).toMatchObject({ color: darkTheme.palette.vars.interactivePrimaryDefaultDefault, diff --git a/packages/open-ui-kit/src/components/message/styles/index.ts b/packages/open-ui-kit/src/components/message/styles/index.ts index d5750971..247e880c 100644 --- a/packages/open-ui-kit/src/components/message/styles/index.ts +++ b/packages/open-ui-kit/src/components/message/styles/index.ts @@ -59,7 +59,7 @@ export const getMessageRootStyles = ( height: "auto", padding: "12px 16px", gap: "12px", - background: theme.palette.vars.baseBackgroundWeak, + background: theme.palette.vars.controlBackgroundDefault, borderStyle: "solid", borderColor: getMessageStatusColor(theme, type), borderWidth: "1px 1px 1px 4px", diff --git a/packages/open-ui-kit/src/components/navigation/styles/index.ts b/packages/open-ui-kit/src/components/navigation/styles/index.ts index 8cfb7a3e..5a93b370 100644 --- a/packages/open-ui-kit/src/components/navigation/styles/index.ts +++ b/packages/open-ui-kit/src/components/navigation/styles/index.ts @@ -325,7 +325,7 @@ export const getNavigationDrawerItemStyles = ( "&:hover": { backgroundColor: selected ? theme.palette.vars.interactivePrimaryWeakHover - : theme.palette.vars.baseBackgroundHover, + : theme.palette.vars.brandBackgroundPrimaryMedium, }, }); diff --git a/packages/open-ui-kit/src/components/popover/__tests__/popover.test.tsx b/packages/open-ui-kit/src/components/popover/__tests__/popover.test.tsx index f800dc1c..664187d6 100644 --- a/packages/open-ui-kit/src/components/popover/__tests__/popover.test.tsx +++ b/packages/open-ui-kit/src/components/popover/__tests__/popover.test.tsx @@ -127,7 +127,7 @@ describe("Popover", () => { }); it("positions bottom-side popovers with arrows on the top edge", () => { - const bg = lightTheme.palette.vars.controlBackgroundDefault; + const bg = lightTheme.palette.vars.baseBackgroundWeak; expect(getArrowStyles(PopoverPlacement.BottomStart, bg)).toMatchObject({ top: "-8px", @@ -144,7 +144,7 @@ describe("Popover", () => { }); it("positions top-side popovers with arrows on the bottom edge", () => { - const bg = lightTheme.palette.vars.controlBackgroundDefault; + const bg = lightTheme.palette.vars.baseBackgroundWeak; expect(getArrowStyles(PopoverPlacement.TopStart, bg)).toMatchObject({ bottom: "-8px", @@ -205,7 +205,7 @@ describe("Popover", () => { }); it("positions left and right arrows outside the side edges", () => { - const bg = lightTheme.palette.vars.controlBackgroundDefault; + const bg = lightTheme.palette.vars.baseBackgroundWeak; expect(getArrowStyles(PopoverPlacement.Left, bg)).toMatchObject({ right: "-8px", @@ -260,7 +260,7 @@ describe("Popover", () => { width: "228px", minWidth: "228px", maxWidth: "228px", - background: lightTheme.palette.vars.controlBackgroundDefault, + background: lightTheme.palette.vars.baseBackgroundWeak, borderRadius: "6px", boxShadow: "none", overflow: "visible", @@ -277,7 +277,7 @@ describe("Popover", () => { maxWidth: "360px", }); expect(getPopoverContentStyles(lightTheme)).toMatchObject({ - background: lightTheme.palette.vars.controlBackgroundDefault, + background: lightTheme.palette.vars.baseBackgroundWeak, border: "0px solid transparent", borderRadius: "6px", gap: "16px", @@ -324,7 +324,7 @@ describe("Popover", () => { ).toMatchObject({ background: lightTheme.palette.vars.controlBorderActive, }); - expect(lightTheme.palette.vars.controlBackgroundDefault).toBe("#fbfcfe"); + expect(lightTheme.palette.vars.baseBackgroundWeak).toBe("#fbfcfe"); expect(lightTheme.palette.vars.controlBorderActive).toBe("#0051af"); expect(lightTheme.palette.vars.baseTextStrong).toBe("#00142b"); expect(lightTheme.palette.vars.baseTextDefault).toBe("#3c4551"); @@ -332,10 +332,10 @@ describe("Popover", () => { it("uses dark mode design tokens", () => { expect(getPopoverPaperStyles(darkTheme)).toMatchObject({ - background: darkTheme.palette.vars.controlBackgroundDefault, + background: darkTheme.palette.vars.baseBackgroundWeak, }); expect(getPopoverContentStyles(darkTheme, true)).toMatchObject({ - background: darkTheme.palette.vars.controlBackgroundDefault, + background: darkTheme.palette.vars.baseBackgroundWeak, border: `2px solid ${darkTheme.palette.vars.controlBorderActive}`, }); expect(popoverTitleStyles(darkTheme)).toMatchObject({ @@ -344,7 +344,7 @@ describe("Popover", () => { expect(popoverBodyStyles(darkTheme)).toMatchObject({ color: darkTheme.palette.vars.baseTextDefault, }); - expect(darkTheme.palette.vars.controlBackgroundDefault).toBe("#183056"); + expect(darkTheme.palette.vars.baseBackgroundWeak).toBe("#183056"); expect(darkTheme.palette.vars.controlBorderActive).toBe("#12c1ff"); expect(darkTheme.palette.vars.baseTextStrong).toBe("#ffffff"); expect(darkTheme.palette.vars.baseTextDefault).toBe("#e8e9ea"); diff --git a/packages/open-ui-kit/src/components/popover/components/popover.tsx b/packages/open-ui-kit/src/components/popover/components/popover.tsx index 68050498..0a7bf572 100644 --- a/packages/open-ui-kit/src/components/popover/components/popover.tsx +++ b/packages/open-ui-kit/src/components/popover/components/popover.tsx @@ -76,7 +76,7 @@ export const Popover = ({ }); const bg = featureHighlight ? theme.palette.vars.controlBorderActive - : theme.palette.vars.controlBackgroundDefault; + : theme.palette.vars.baseBackgroundWeak; return ( { return ( - + diff --git a/packages/open-ui-kit/test-utils/react-syntax-highlighter.js b/packages/open-ui-kit/test-utils/react-syntax-highlighter.js index 0bb57452..ca3d4b12 100644 --- a/packages/open-ui-kit/test-utils/react-syntax-highlighter.js +++ b/packages/open-ui-kit/test-utils/react-syntax-highlighter.js @@ -13,13 +13,20 @@ const SyntaxHighlighter = ({ lineNumberStyle, showLineNumbers, startingLineNumber = 1, + style, }) => { const lines = String(children ?? "").split("\n"); const { style: codeStyle, ...restCodeTagProps } = codeTagProps; return React.createElement( "pre", - { style: customStyle }, + { + style: customStyle, + // The real highlighter turns `style` into per-token colors. The mock + // cannot tokenize, so it records the map instead — otherwise a wrong or + // missing syntax palette would be invisible to every test. + "data-prism-style": style ? JSON.stringify(style) : undefined, + }, showLineNumbers ? React.createElement( "span", From b4e18dab7db60d8fb196cac4b42067c3f77c61a8 Mon Sep 17 00:00:00 2001 From: Yashwant Rautela Date: Thu, 6 Aug 2026 13:18:18 +0530 Subject: [PATCH 04/10] Fixes for Yarn Lock file Signed-off-by: Yashwant Rautela --- yarn.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/yarn.lock b/yarn.lock index f6fe598e..5e1427fb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5269,6 +5269,7 @@ __metadata: react-syntax-highlighter: "npm:^16.1.1" react-virtuoso: "npm:^4.18.7" recharts: "npm:^2.15.3" + refractor: "npm:^5.0.0" rollup: "npm:^4.62.2" rollup-plugin-circular-dependencies: "npm:^2.0.1" rollup-plugin-cleanup: "npm:^3.2.1" From a6f359004438c124f04be6ba1a287d184056b30a Mon Sep 17 00:00:00 2001 From: Yashwant Rautela Date: Thu, 6 Aug 2026 13:18:46 +0530 Subject: [PATCH 05/10] fixes for avatar token (#14) Signed-off-by: Yashwant Rautela --- .../src/components/avatar/__tests__/avatar.test.tsx | 10 ++++++---- .../src/components/avatar/components/elements.tsx | 6 +++--- .../src/components/avatar/stories/avatar.stories.tsx | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/open-ui-kit/src/components/avatar/__tests__/avatar.test.tsx b/packages/open-ui-kit/src/components/avatar/__tests__/avatar.test.tsx index 42f371df..20da7384 100644 --- a/packages/open-ui-kit/src/components/avatar/__tests__/avatar.test.tsx +++ b/packages/open-ui-kit/src/components/avatar/__tests__/avatar.test.tsx @@ -108,10 +108,10 @@ describe("Avatar", () => { expect(styles.fontWeight).toBe("600"); expect(styles.lineHeight).toBe("133%"); expect(styles.letterSpacing).toBe("0.15px"); - expect(lightTheme.palette.vars.brandBackgroundPrimaryWeak).toBe( + expect(lightTheme.palette.vars.interactivePrimaryWeakDefault).toBe( "#e8f1ff", ); - expect(lightTheme.palette.vars.brandBackgroundPrimaryMedium).toBe( + expect(lightTheme.palette.vars.interactivePrimaryWeakHover).toBe( "#9bcaff", ); expect(lightTheme.palette.vars.brandIconPrimaryDefault).toBe("#187adc"); @@ -148,8 +148,10 @@ describe("Avatar", () => { expect(styles.backgroundColor).toBe("rgb(6, 34, 66)"); expect(styles.color).toBe("rgb(27, 205, 255)"); - expect(darkTheme.palette.vars.brandBackgroundPrimaryWeak).toBe("#062242"); - expect(darkTheme.palette.vars.brandBackgroundPrimaryMedium).toBe( + expect(darkTheme.palette.vars.interactivePrimaryWeakDefault).toBe( + "#062242", + ); + expect(darkTheme.palette.vars.interactivePrimaryWeakHover).toBe( "#263b62", ); expect(darkTheme.palette.vars.brandIconPrimaryDefault).toBe("#1bcdff"); diff --git a/packages/open-ui-kit/src/components/avatar/components/elements.tsx b/packages/open-ui-kit/src/components/avatar/components/elements.tsx index 3ea6a307..702adedd 100644 --- a/packages/open-ui-kit/src/components/avatar/components/elements.tsx +++ b/packages/open-ui-kit/src/components/avatar/components/elements.tsx @@ -24,7 +24,7 @@ export const StyledAvatar = styled(MuiAvatar, { borderRadius: "50px", backgroundColor: hasImage ? "transparent" - : theme.palette.vars.brandBackgroundPrimaryWeak, + : theme.palette.vars.interactivePrimaryWeakDefault, color: theme.palette.vars.brandIconPrimaryDefault, fontSize: avatarSize === "L" ? "16px" : "12px", fontWeight: 600, @@ -49,7 +49,7 @@ export const StyledAvatar = styled(MuiAvatar, { "&:hover": { backgroundColor: hasImage ? "transparent" - : theme.palette.vars.brandBackgroundPrimaryMedium, + : theme.palette.vars.interactivePrimaryWeakHover, color: theme.palette.vars.brandIconPrimaryStrong, "& .MuiSvgIcon-root": { @@ -81,7 +81,7 @@ export const StyledAvatarGroup = styled(MuiAvatarGroup, { width: avatarSize === "L" ? 40 : 32, height: avatarSize === "L" ? 40 : 32, borderRadius: "50px", - backgroundColor: theme.palette.vars.brandBackgroundPrimaryWeak, + backgroundColor: theme.palette.vars.interactivePrimaryWeakDefault, border: `2px solid ${ theme.palette.mode === "dark" ? theme.palette.vars.baseBorderWeak diff --git a/packages/open-ui-kit/src/components/avatar/stories/avatar.stories.tsx b/packages/open-ui-kit/src/components/avatar/stories/avatar.stories.tsx index b40d89a8..96832118 100644 --- a/packages/open-ui-kit/src/components/avatar/stories/avatar.stories.tsx +++ b/packages/open-ui-kit/src/components/avatar/stories/avatar.stories.tsx @@ -71,7 +71,7 @@ const HoverState = ({ children }: { children: ReactNode }) => ( ({ "& .MuiAvatar-root": { - bgcolor: theme.palette.vars.brandBackgroundPrimaryMedium, + bgcolor: theme.palette.vars.interactivePrimaryWeakHover, color: theme.palette.vars.brandIconPrimaryStrong, }, "& .MuiSvgIcon-root": { From 7487b93d137527f768037668334f22857406a3a5 Mon Sep 17 00:00:00 2001 From: Yashwant Rautela Date: Thu, 6 Aug 2026 13:19:00 +0530 Subject: [PATCH 06/10] fix: Navigation and Header Component (#15) Signed-off-by: Yashwant Rautela --- .../header/stories/header.stories.tsx | 33 +-- .../navigation/__tests__/navigation.test.tsx | 25 ++- .../navigation/components/navigation.tsx | 5 +- .../src/components/navigation/styles/index.ts | 50 +++-- .../src/custom-icons/brand-logos.tsx | 60 ++++-- .../src/custom-icons/navigation/dashboard.tsx | 199 ++++++++---------- .../src/theme/midnight/midnight-vars.ts | 7 +- 7 files changed, 193 insertions(+), 186 deletions(-) diff --git a/packages/open-ui-kit/src/components/header/stories/header.stories.tsx b/packages/open-ui-kit/src/components/header/stories/header.stories.tsx index f7eac0cc..994f9127 100644 --- a/packages/open-ui-kit/src/components/header/stories/header.stories.tsx +++ b/packages/open-ui-kit/src/components/header/stories/header.stories.tsx @@ -6,7 +6,7 @@ import { Meta, StoryObj } from "@storybook/react-vite"; import { useState } from "react"; -import { Box, Stack, Typography } from "@/components"; +import { Badge, Box, Stack, Typography } from "@/components"; import { AccountCircleOutlined, ExpandMore, @@ -193,32 +193,11 @@ const defaultActions: HeaderAction[] = [ { id: "notifications", icon: ( - ({ - alignItems: "center", - backgroundColor: theme.palette.vars.excellentBackgroundDefault, - color: theme.palette.vars.baseTextInverse, - borderRadius: "64px", - content: '"1"', - display: "flex", - fontSize: "10px", - lineHeight: "16px", - height: "16px", - justifyContent: "center", - minWidth: "19px", - padding: "0 6.5px", - position: "absolute", - right: "-6px", - top: "-8px", - boxSizing: "border-box", - }), - }} - > - - + } + /> ), tooltip: "Notifications", "aria-label": "notifications", diff --git a/packages/open-ui-kit/src/components/navigation/__tests__/navigation.test.tsx b/packages/open-ui-kit/src/components/navigation/__tests__/navigation.test.tsx index efa8b3e8..f45baf28 100644 --- a/packages/open-ui-kit/src/components/navigation/__tests__/navigation.test.tsx +++ b/packages/open-ui-kit/src/components/navigation/__tests__/navigation.test.tsx @@ -168,15 +168,26 @@ describe("Navigation", () => { ).toMatchObject({ height: "40px", padding: "8px", - backgroundColor: lightTheme.palette.vars.interactivePrimaryWeakDefault, - color: lightTheme.palette.vars.interactivePrimaryDefaultActive, + backgroundColor: lightTheme.palette.vars.brandBackgroundPrimaryWeak, + color: lightTheme.palette.vars.brandTextPrimary, + // Figma binds no border on selected; only the open-submenu state has one. + border: "1px solid transparent", + borderRadius: "8px", }); expect(getNavigationItemStyles(lightTheme, "open", false)).toMatchObject({ - backgroundColor: lightTheme.palette.vars.interactivePrimaryWeakDefault, - color: lightTheme.palette.vars.interactivePrimaryDefaultActive, - border: `1px solid ${lightTheme.palette.vars.controlBorderStrong}`, + backgroundColor: lightTheme.palette.vars.brandBackgroundPrimaryWeak, + // Open submenu keeps the secondary label color and is distinguished by + // its border, unlike selected. + color: lightTheme.palette.vars.brandTextSecondary, + border: `1px solid ${lightTheme.palette.vars.baseBorderStrong}`, borderRightWidth: 0, }); + expect( + getNavigationItemStyles(lightTheme, "default", false), + ).toMatchObject({ + backgroundColor: "transparent", + color: lightTheme.palette.vars.brandTextSecondary, + }); expect( getNavigationCollapseButtonStyles(lightTheme, false), ).toMatchObject({ @@ -208,8 +219,8 @@ describe("Navigation", () => { expect( getNavigationItemStyles(darkTheme, "selected", false), ).toMatchObject({ - backgroundColor: darkTheme.palette.vars.brandBackgroundSecondaryDefault, - color: darkTheme.palette.vars.brandIconPrimaryDefault, + backgroundColor: darkTheme.palette.vars.brandBackgroundPrimaryWeak, + color: darkTheme.palette.vars.brandTextPrimary, }); expect(getNavigationCollapseButtonStyles(darkTheme, true)).toMatchObject({ marginTop: "auto", diff --git a/packages/open-ui-kit/src/components/navigation/components/navigation.tsx b/packages/open-ui-kit/src/components/navigation/components/navigation.tsx index c13af469..14500773 100644 --- a/packages/open-ui-kit/src/components/navigation/components/navigation.tsx +++ b/packages/open-ui-kit/src/components/navigation/components/navigation.tsx @@ -176,7 +176,10 @@ export const Navigation = ({ subNavigationItem?.id === item.id ? "open" : getItemState(item, selectedItemId); - const selected = state === "selected" || state === "open"; + // Only "selected" takes the primary icon ramp. An item with + // its sub-menu open keeps the secondary ramp, matching the + // Figma frame. + const selected = state === "selected"; return ( { - const selected = state === "selected" || state === "open"; + // Tokens follow the Figma "Navigation" item states (frame node 179634:5030, + // `.menu-item`). `selected` and `open` share a background but not a text + // color: the frame paints selected labels Brand/Text/Primary and open-submenu + // labels Brand/Text/Secondary, distinguishing open by its border alone. + const selected = state === "selected"; const open = state === "open"; + const active = selected || open; const disabled = state === "disabled"; return { @@ -182,19 +187,21 @@ export const getNavigationItemStyles = ( gap: "8px", position: "relative", isolation: "isolate", - border: selected - ? `1px solid ${theme.palette.vars.controlBorderStrong}` + // Only the open-submenu state binds a border; it opens on the right edge to + // merge into the sub-navigation panel. Selected is fully rounded. + border: open + ? `1px solid ${theme.palette.vars.baseBorderStrong}` : "1px solid transparent", - borderRightWidth: selected ? 0 : "1px", - borderRadius: selected ? "8px 0px 0px 8px" : "8px", - backgroundColor: selected - ? getNavigationActiveBackground(theme) + borderRightWidth: open ? 0 : "1px", + borderRadius: open ? "8px 0px 0px 8px" : "8px", + backgroundColor: active + ? theme.palette.vars.brandBackgroundPrimaryWeak : "transparent", color: disabled ? theme.palette.vars.baseTextDisabled : selected - ? getNavigationActiveColor(theme) - : theme.palette.vars.baseTextStrong, + ? theme.palette.vars.brandTextPrimary + : theme.palette.vars.brandTextSecondary, cursor: disabled ? "default" : "pointer", font: "inherit", textAlign: "left", @@ -207,18 +214,16 @@ export const getNavigationItemStyles = ( right: compact ? "-24px" : "-23px", width: compact ? "24px" : "23px", height: "40px", - backgroundColor: getNavigationActiveBackground(theme), - borderTop: `1px solid ${theme.palette.vars.controlBorderStrong}`, - borderBottom: `1px solid ${theme.palette.vars.controlBorderStrong}`, + backgroundColor: theme.palette.vars.brandBackgroundPrimaryWeak, + borderTop: `1px solid ${theme.palette.vars.baseBorderStrong}`, + borderBottom: `1px solid ${theme.palette.vars.baseBorderStrong}`, zIndex: -1, } : undefined, "&:hover": disabled ? {} : { - backgroundColor: selected - ? getNavigationActiveBackground(theme) - : theme.palette.vars.baseBackgroundHover, + backgroundColor: theme.palette.vars.brandBackgroundPrimaryWeak, }, }; }; @@ -313,19 +318,22 @@ export const getNavigationDrawerItemStyles = ( gap: "2px", border: 0, borderRadius: "6px", + // Tokens follow the Figma "Navigation" drawer item states (frame node + // 179634:5030, `.drawer-item`). Default rests on Brand/Text/Secondary with no + // fill; hover and selected are the same treatment — + // Brand/Background/Primary/Medium behind Brand/Text/Primary. backgroundColor: selected - ? theme.palette.vars.interactivePrimaryWeakHover + ? theme.palette.vars.brandBackgroundPrimaryMedium : "transparent", color: selected - ? theme.palette.vars.interactivePrimaryDefaultActive - : theme.palette.vars.baseTextStrong, + ? theme.palette.vars.brandTextPrimary + : theme.palette.vars.brandTextSecondary, cursor: "pointer", font: "inherit", textAlign: "left", "&:hover": { - backgroundColor: selected - ? theme.palette.vars.interactivePrimaryWeakHover - : theme.palette.vars.brandBackgroundPrimaryMedium, + backgroundColor: theme.palette.vars.brandBackgroundPrimaryMedium, + color: theme.palette.vars.brandTextPrimary, }, }); diff --git a/packages/open-ui-kit/src/custom-icons/brand-logos.tsx b/packages/open-ui-kit/src/custom-icons/brand-logos.tsx index 7ed4e5cc..3a00e19e 100644 --- a/packages/open-ui-kit/src/custom-icons/brand-logos.tsx +++ b/packages/open-ui-kit/src/custom-icons/brand-logos.tsx @@ -4,7 +4,13 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SvgIcon, SvgIconProps } from "@mui/material"; +import { SvgIcon, SvgIconProps, useTheme } from "@mui/material"; +import { + outshiftBlue, + outshiftLogoGreen, + outshiftLogoLightBlue, + outshiftLogoOrange, +} from "@/theme/style/color-palette"; export const AgntcyBrand = (props: SvgIconProps) => { return ( @@ -80,84 +86,98 @@ export const CiscoBrand = (props: SvgIconProps) => { ); }; +// The Figma header (179783:9025) paints the Outshift mark in four brand colors +// plus a wordmark, so the artwork cannot share one inherited `currentColor`: +// +// spark horizontal, lower-left, lower ray Outshift/Logo Light Blue +// upper-right diagonal Outshift/Logo Green +// upper-left ray Outshift/Logo Orange +// vertical ray Outshift/Blue +// by CISCO Outshift/Logo Light Blue +// outshift wordmark Brand/Logo/Secondary +// +// The brand colors are fixed in every theme; only the wordmark follows the +// theme, so it reads `brandLogoSecondary` instead of a palette constant. export const OutshiftBrand = (props: SvgIconProps) => { + const { vars } = useTheme().palette; + return ( ); diff --git a/packages/open-ui-kit/src/custom-icons/navigation/dashboard.tsx b/packages/open-ui-kit/src/custom-icons/navigation/dashboard.tsx index ee61a0a2..df94077d 100644 --- a/packages/open-ui-kit/src/custom-icons/navigation/dashboard.tsx +++ b/packages/open-ui-kit/src/custom-icons/navigation/dashboard.tsx @@ -4,121 +4,106 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SvgIcon, SvgIconProps } from "@mui/material"; +import { SvgIcon, SvgIconProps, useTheme } from "@mui/material"; + +// The Figma navigation item (frame node 179634:5030) paints the Dashboard mark +// with three tones rather than a single flat fill, and swaps the whole ramp +// between the unselected and selected states: +// +// unselected tall block Brand/Icon/Secondary/Weak +// bars Brand/Icon/Secondary/Medium +// square Brand/Icon/Secondary/Default +// selected tall block Brand/Icon/Primary/Default +// bars Brand/Icon/Primary/Default +// square Brand/Icon/Primary/Strong +// +// The shapes therefore cannot use `currentColor` — the item only carries one +// inherited color, which is why all three variants rendered identically before. +type DashboardTones = { + block: string; + bars: string; + square: string; +}; + +const DashboardMark = ({ + tones, + ...props +}: SvgIconProps & { tones: DashboardTones }) => ( + + + + + + + + +); export const DashboardSelected = (props: SvgIconProps) => { + const { vars } = useTheme().palette; + return ( - - - - - - - - + ); }; export const DashboardUnSelected = (props: SvgIconProps) => { - return ( - - - - - - - - - ); -}; + const { vars } = useTheme().palette; -export const DashboardDisabled = (props: SvgIconProps) => { return ( - - - - - - - - + ); }; + +// The disabled state has no tonal ramp in the frame — it stays flat and takes +// the item's inherited disabled text color. +export const DashboardDisabled = (props: SvgIconProps) => ( + +); diff --git a/packages/open-ui-kit/src/theme/midnight/midnight-vars.ts b/packages/open-ui-kit/src/theme/midnight/midnight-vars.ts index 20198350..0bef9bdb 100644 --- a/packages/open-ui-kit/src/theme/midnight/midnight-vars.ts +++ b/packages/open-ui-kit/src/theme/midnight/midnight-vars.ts @@ -105,9 +105,10 @@ export const midnightVars: VarsType = { brandIconPrimaryStrong: electricBluePalette[700], brandIconPrimaryMedium: electricBluePalette[300], brandIconPrimaryWeak: electricBluePalette[300], - brandIconSecondaryDefault: electricBluePalette[300], - brandIconSecondaryWeak: greyPalette[100], - brandIconSecondaryMedium: greyPalette[200], + // The Brand/Icon/Secondary ramp is not redefined for Midnight — the Figma + // Midnight mode resolves it to the same Surface values the Dark theme uses + // (Weak #e3eafa, Medium #c8d5f5, Default #4f628d), so it inherits from + // `darkVars` rather than being overridden here. brandBackgroundPrimaryDefault: darkNavyPalette[200], brandBackgroundPrimaryWeak: darkNavyPalette[100], brandBackgroundPrimaryMedium: surfaceDarkPalette[400], From 37f3b44536ea8e85b721fd4ff0935a4d47faea1f Mon Sep 17 00:00:00 2001 From: Yashwant Rautela Date: Fri, 7 Aug 2026 18:43:36 +0530 Subject: [PATCH 07/10] fix: Midnight Theme | Tokens Fixes Signed-off-by: Yashwant Rautela --- .../components/card/__tests__/card.test.tsx | 1 + .../components/card/components/elements.tsx | 23 +++++--- .../components/card/stories/card.stories.tsx | 35 +++++++++--- .../src/components/card/styles/index.ts | 4 +- .../icon/__tests__/custom-icons.test.ts | 56 +++++++++++++++---- .../navigation/__tests__/navigation.test.tsx | 44 +++++++++++++++ .../navigation/components/navigation.tsx | 4 +- .../src/custom-icons/org-switcher.tsx | 49 +++++++++++----- 8 files changed, 171 insertions(+), 45 deletions(-) diff --git a/packages/open-ui-kit/src/components/card/__tests__/card.test.tsx b/packages/open-ui-kit/src/components/card/__tests__/card.test.tsx index c99ba830..7d094b5b 100644 --- a/packages/open-ui-kit/src/components/card/__tests__/card.test.tsx +++ b/packages/open-ui-kit/src/components/card/__tests__/card.test.tsx @@ -7,6 +7,7 @@ import React from "react"; import { render, screen } from "@testing-library/react"; import "@testing-library/jest-dom"; +import type { Theme } from "@mui/material/styles"; import { ThemeMode, ThemeProvider } from "@/theme-provider/theme-provider"; import { darkTheme } from "@/theme/dark/dark-theme"; import { lightTheme } from "@/theme/light/light-theme"; diff --git a/packages/open-ui-kit/src/components/card/components/elements.tsx b/packages/open-ui-kit/src/components/card/components/elements.tsx index dbfc4d55..8cb7fe39 100644 --- a/packages/open-ui-kit/src/components/card/components/elements.tsx +++ b/packages/open-ui-kit/src/components/card/components/elements.tsx @@ -39,22 +39,27 @@ export const StyledCard = styled(MuiCard, { export const StyledCardActionArea = styled(MuiCardActionArea)(({ theme }) => ({ borderRadius: "8px", - "&:hover .MuiCard-root, &:focus-visible .MuiCard-root": { - ...cardInteractiveStyles(theme), - "& .MuiCardActionArea-focusHighlight": { - opacity: 0, - }, - }, + "&:hover .MuiCard-root, &:focus-visible .MuiCard-root": + cardInteractiveStyles(theme), "&:active .MuiCard-root": cardActiveStyles(theme), "&.Mui-disabled .MuiCard-root": cardDisabledStyles(theme), + // MUI paints a translucent overlay (`focusHighlight`) across the whole action + // area on hover and focus. It is a sibling of the card, not a descendant, so + // it cannot be reached from a `.MuiCard-root` rule, and MUI scopes its own + // rules as `&:hover .focusHighlight` / `&.Mui-focusVisible .focusHighlight`. + // A bare `& .focusHighlight` is one specificity step lower and loses to them, + // so each selector has to be matched directly to keep the overlay hidden. "& .MuiCardActionArea-focusHighlight": { opacity: 0, }, + "&:hover .MuiCardActionArea-focusHighlight": { + opacity: 0, + }, + "&.Mui-focusVisible .MuiCardActionArea-focusHighlight": { + opacity: 0, + }, "&:focus-visible": { outline: "none", - "& .MuiCardActionArea-focusHighlight": { - opacity: 0, - }, }, })) as ComponentType; diff --git a/packages/open-ui-kit/src/components/card/stories/card.stories.tsx b/packages/open-ui-kit/src/components/card/stories/card.stories.tsx index f618ec3d..fecd52fe 100644 --- a/packages/open-ui-kit/src/components/card/stories/card.stories.tsx +++ b/packages/open-ui-kit/src/components/card/stories/card.stories.tsx @@ -20,12 +20,16 @@ import { CardContent, CardDescription, CardHeader, + CardSubheader, Link, LinkType, Skeleton, Stack, + Tag, + TagStatus, Typography, } from "@/components"; +import { GeneralSize } from "@/common"; import { cardActiveStyles, cardSkeletonStyles } from "../styles"; import { DocsHeader } from "storybook/components/docs-header.stories"; @@ -83,7 +87,12 @@ const CardStats = () => ( color: theme.palette.vars.baseTextMedium, })} > - + ({ + color: theme.palette.vars.successIconDefault, + fontSize: 16, + })} + /> 10k @@ -131,23 +140,29 @@ const ImportCardContent = () => ( ({ alignItems: "center", + backgroundColor: theme.palette.vars.baseBackgroundMedium, border: `1px solid ${theme.palette.vars.controlBorderActive}`, borderRadius: "4px", + color: theme.palette.vars.controlIconDefault, display: "flex", - height: 32, + height: 40, justifyContent: "center", - width: 32, + width: 40, })} > - + Import Existing - + ({ + backgroundColor: theme.palette.vars.accentGWeak, + })} + type="info" + /> - - Upload A2A card, MCP config, or OASF file - + Upload A2A card, MCP config, or OASF file @@ -260,7 +275,9 @@ export const Metrics: Story = { Headline not clickable - + + 0% + ({ }); export const cardDisabledStyles = (theme: Theme): CSSObject => ({ - backgroundColor: theme.palette.vars.controlBackgroundWeak, - border: `1px solid ${theme.palette.vars.controlBorderWeak}`, + backgroundColor: theme.palette.vars.controlBackgroundDisabled, + border: `1px solid ${theme.palette.vars.controlBorderDisabled}`, boxShadow: theme.shadows[2], color: theme.palette.vars.baseTextDisabled, pointerEvents: "none" as const, diff --git a/packages/open-ui-kit/src/components/icon/__tests__/custom-icons.test.ts b/packages/open-ui-kit/src/components/icon/__tests__/custom-icons.test.ts index fc06da1a..3a91b097 100644 --- a/packages/open-ui-kit/src/components/icon/__tests__/custom-icons.test.ts +++ b/packages/open-ui-kit/src/components/icon/__tests__/custom-icons.test.ts @@ -6,6 +6,10 @@ import fs from "node:fs"; import path from "node:path"; +// The suite only reads sources, but it still needs the jest globals to be +// typed, and `typeRoots` in tsconfig.json does not reach the hoisted +// `@types/jest`. Every other suite picks them up through this import. +import "@testing-library/jest-dom"; const customIconsDir = path.resolve(__dirname, "../../../custom-icons"); @@ -25,8 +29,48 @@ function getSource(filePath: string) { return fs.readFileSync(filePath, "utf8"); } +// A color written straight into the icon — hex, or any of the CSS color +// functions. These are what must never appear: they survive a theme switch and +// leave the mark painted for one mode only. +const LITERAL_COLOR = + /#[0-9a-f]{3,8}\b|\b(?:rgba?|hsla?|hwb|lab|lch|oklab|oklch|color)\s*\(/i; + +const QUOTED_STRING = /"[^"]*"|'[^']*'/g; + +function unquote(value: string) { + return value.slice(1, -1).trim().toLowerCase(); +} + +function isInheritedPaint(value: string) { + return value === "none" || value === "currentcolor"; +} + +// `fill`/`stroke` may either inherit (`currentColor`), paint nothing (`none`), +// or resolve through the theme — a `vars.*` token, a palette constant, or a +// prop carrying one of those. The multi-tone marks need that third form: the +// Outshift logo paints four fixed brand colors, the Dashboard navigation icon +// swaps a three-tone ramp between its selected and unselected states, and the +// OrgSwitcher mark is two-tone. A caller only carries one inherited color, so +// forcing those onto `currentColor` would flatten them. +function isAcceptablePaint(rawValue: string) { + const value = rawValue.trim(); + + if (!value.startsWith("{")) { + return isInheritedPaint(unquote(value)); + } + + const expression = value.slice(1, -1); + + return ( + !LITERAL_COLOR.test(expression) && + (expression.match(QUOTED_STRING) ?? []).every((literal) => + isInheritedPaint(unquote(literal)), + ) + ); +} + describe("custom icons", () => { - it("uses currentColor for SVG fills and strokes", () => { + it("keeps SVG fills and strokes off hardcoded colors", () => { const badPaintAttributes = iconFiles.flatMap((filePath) => { const source = getSource(filePath); const matches = [ @@ -36,15 +80,7 @@ describe("custom icons", () => { ]; return matches - .filter((match) => { - const value = match[1].toLowerCase(); - return ( - value !== '"none"' && - value !== "'none'" && - value !== '"currentcolor"' && - value !== "'currentcolor'" - ); - }) + .filter((match) => !isAcceptablePaint(match[1])) .map( (match) => `${path.relative(customIconsDir, filePath)}: ${match[0]}`, ); diff --git a/packages/open-ui-kit/src/components/navigation/__tests__/navigation.test.tsx b/packages/open-ui-kit/src/components/navigation/__tests__/navigation.test.tsx index f45baf28..c9e3fb24 100644 --- a/packages/open-ui-kit/src/components/navigation/__tests__/navigation.test.tsx +++ b/packages/open-ui-kit/src/components/navigation/__tests__/navigation.test.tsx @@ -229,4 +229,48 @@ describe("Navigation", () => { }); }); }); + + /* + * Figma `.Switcher` (179634:5059) resolves the bracket and the arrow to + * Brand/Icon/Secondary/Weak and the diamond to Brand/Icon/Secondary/Default. + * Two tones cannot ride on `currentColor`, since the switcher button passes + * down only one inherited colour — which is why the nav renders the complete + * `OrgSwitcher` rather than the single-tone `OrgSwitcherDefault`. + */ + describe("organization switcher mark", () => { + const switcherPaths = (container: HTMLElement) => + Array.from( + container.querySelectorAll( + 'button[aria-label="[Organization]"] svg path, button[aria-label="[Organization]"] path', + ), + ).map((path) => path.getAttribute("fill")); + + it.each([ + ["light", false, lightTheme], + ["dark", true, darkTheme], + ])("paints all three %s layers on the brand ramp", (_mode, dark, theme) => { + const { container } = wrap( + , + dark, + ); + const fills = switcherPaths(container); + + expect(fills).toEqual([ + theme.palette.vars.brandIconSecondaryWeak, + theme.palette.vars.brandIconSecondaryWeak, + theme.palette.vars.brandIconSecondaryDefault, + ]); + // The single-tone mark would have collapsed the layers onto one + // inherited colour. + expect(fills).not.toContain("currentColor"); + }); + + it("keeps the two tones distinguishable in both themes", () => { + for (const theme of [lightTheme, darkTheme]) { + expect(theme.palette.vars.brandIconSecondaryWeak).not.toBe( + theme.palette.vars.brandIconSecondaryDefault, + ); + } + }); + }); }); diff --git a/packages/open-ui-kit/src/components/navigation/components/navigation.tsx b/packages/open-ui-kit/src/components/navigation/components/navigation.tsx index 14500773..1c25c990 100644 --- a/packages/open-ui-kit/src/components/navigation/components/navigation.tsx +++ b/packages/open-ui-kit/src/components/navigation/components/navigation.tsx @@ -9,7 +9,7 @@ import { DashboardSelected, DashboardUnSelected, KeyboardArrowRight, - OrgSwitcherDefault, + OrgSwitcher, } from "@/custom-icons"; import { useEffect, useState, type ReactNode } from "react"; import type { @@ -153,7 +153,7 @@ export const Navigation = ({ selected={isOrganizationDrawerOpen} type="button" > - + {!isCompact ? ( <> diff --git a/packages/open-ui-kit/src/custom-icons/org-switcher.tsx b/packages/open-ui-kit/src/custom-icons/org-switcher.tsx index b29d8248..0ea199cc 100644 --- a/packages/open-ui-kit/src/custom-icons/org-switcher.tsx +++ b/packages/open-ui-kit/src/custom-icons/org-switcher.tsx @@ -4,15 +4,26 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { SvgIcon, SvgIconProps } from "@mui/material"; +import { SvgIcon, SvgIconProps, useTheme } from "@mui/material"; + +// The mark is drawn as three stacked layers, named after the +// Brand/Icon/Secondary ramp in Figma: the "Weak" bracket along the bottom, the +// "Medium" arrow on the right and the "Default" diamond on the left. The +// exports below build the partial marks by stacking those layers, so the +// geometry is declared once. +const BRACKET_PATH = + "M 10,10.583 C 10.55,10.583 11,11.033 11,11.583 L 11,13.583 L 19,13.583 C 19.55,13.583 20,14.033 20,14.583 L 20,17.583 C 20,18.143 19.55,18.583 19,18.583 C 18.45,18.583 18,18.143 18,17.583 L 18,15.583 L 11,15.583 L 11,17.583 C 11,18.143 10.55,18.583 10,18.583 C 9.45,18.583 9,18.143 9,17.583 L 9,15.583 L 2,15.583 L 2,17.583 C 2,18.143 1.55,18.583 1,18.583 C 0.45,18.583 0,18.143 0,17.583 L 0,14.583 C 0,14.033 0.45,13.583 1,13.583 L 9,13.583 L 9,11.583 C 9,11.033 9.45,10.583 10,10.583"; + +const ARROW_PATH = + "M 11.06,7.643 L 12.29,8.883 C 12.68,9.273 13.32,9.273 13.71,8.883 L 17.29,5.293 C 17.68,4.903 17.68,4.273 17.29,3.883 L 13.71,0.293 C 13.32,-0.097 12.68,-0.097 12.29,0.293 L 11.06,1.523 L 12.35,2.823 C 12.39,2.863 12.43,2.903 12.47,2.943 L 13,2.413 L 15.17,4.583 L 13,6.753 L 12.47,6.233 C 12.43,6.273 12.39,6.313 12.35,6.353 L 11.06,7.643"; + +const DIAMOND_PATH = + "M 6.29,0.293 C 6.68,-0.097 7.32,-0.097 7.71,0.293 L 11.29,3.883 C 11.68,4.273 11.68,4.903 11.29,5.293 L 7.71,8.883 C 7.32,9.273 6.68,9.273 6.29,8.883 L 2.71,5.293 C 2.32,4.903 2.32,4.273 2.71,3.883 L 6.29,0.293"; export function OrgSwitcherWeak(props: SvgIconProps) { return ( - + ); } @@ -20,10 +31,7 @@ export function OrgSwitcherWeak(props: SvgIconProps) { export function OrgSwitcherMedium(props: SvgIconProps) { return ( - + ); } @@ -31,10 +39,25 @@ export function OrgSwitcherMedium(props: SvgIconProps) { export function OrgSwitcherDefault(props: SvgIconProps) { return ( - + + + ); +} + +// The complete mark, matching the `.Switcher` frame (node 179634:5059) in the +// Outshift Spark Component Library. All three layers are painted, and the frame +// resolves the bracket and the arrow to Brand/Icon/Secondary/Weak while the +// diamond takes Brand/Icon/Secondary/Default. That two-tone ramp is why the +// layers cannot share `currentColor` — the caller only carries one inherited +// color. +export function OrgSwitcher(props: SvgIconProps) { + const { vars } = useTheme().palette; + + return ( + + + + ); } From 6c8657e2f92288b26bdbf7664df388e31c3db521 Mon Sep 17 00:00:00 2001 From: Yashwant Rautela Date: Tue, 11 Aug 2026 11:50:21 +0530 Subject: [PATCH 08/10] fix: Navigation Component UI Fix Signed-off-by: Yashwant Rautela --- .../navigation/__tests__/navigation.test.tsx | 19 +++++++++++++++++++ .../src/components/navigation/styles/index.ts | 9 ++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/open-ui-kit/src/components/navigation/__tests__/navigation.test.tsx b/packages/open-ui-kit/src/components/navigation/__tests__/navigation.test.tsx index c9e3fb24..e2c1ee05 100644 --- a/packages/open-ui-kit/src/components/navigation/__tests__/navigation.test.tsx +++ b/packages/open-ui-kit/src/components/navigation/__tests__/navigation.test.tsx @@ -12,6 +12,7 @@ import { darkTheme } from "@/theme/dark/dark-theme"; import { lightTheme } from "@/theme/light/light-theme"; import { Navigation, NavigationDrawer, NavigationSubNavigation } from ".."; import { + getNavigationDrawerItemStyles, getNavigationDrawerStyles, getNavigationCollapseButtonStyles, getNavigationItemStyles, @@ -188,6 +189,24 @@ describe("Navigation", () => { backgroundColor: "transparent", color: lightTheme.palette.vars.brandTextSecondary, }); + // Hovering a sub-menu option adds the fill but keeps the label on + // Brand/Text/Secondary; only selected holds Brand/Text/Primary. + expect(getNavigationDrawerItemStyles(lightTheme, false)).toMatchObject({ + backgroundColor: "transparent", + color: lightTheme.palette.vars.brandTextSecondary, + "&:hover": { + backgroundColor: lightTheme.palette.vars.brandBackgroundPrimaryMedium, + color: lightTheme.palette.vars.brandTextSecondary, + }, + }); + expect(getNavigationDrawerItemStyles(lightTheme, true)).toMatchObject({ + backgroundColor: lightTheme.palette.vars.brandBackgroundPrimaryMedium, + color: lightTheme.palette.vars.brandTextPrimary, + "&:hover": { + backgroundColor: lightTheme.palette.vars.brandBackgroundPrimaryMedium, + color: lightTheme.palette.vars.brandTextPrimary, + }, + }); expect( getNavigationCollapseButtonStyles(lightTheme, false), ).toMatchObject({ diff --git a/packages/open-ui-kit/src/components/navigation/styles/index.ts b/packages/open-ui-kit/src/components/navigation/styles/index.ts index 3698281c..2bd930b6 100644 --- a/packages/open-ui-kit/src/components/navigation/styles/index.ts +++ b/packages/open-ui-kit/src/components/navigation/styles/index.ts @@ -320,8 +320,9 @@ export const getNavigationDrawerItemStyles = ( borderRadius: "6px", // Tokens follow the Figma "Navigation" drawer item states (frame node // 179634:5030, `.drawer-item`). Default rests on Brand/Text/Secondary with no - // fill; hover and selected are the same treatment — - // Brand/Background/Primary/Medium behind Brand/Text/Primary. + // fill; hover adds the Brand/Background/Primary/Medium fill but keeps the + // label on Brand/Text/Secondary. Only selected paints Brand/Text/Primary, and + // it holds that ramp while hovered. backgroundColor: selected ? theme.palette.vars.brandBackgroundPrimaryMedium : "transparent", @@ -333,7 +334,9 @@ export const getNavigationDrawerItemStyles = ( textAlign: "left", "&:hover": { backgroundColor: theme.palette.vars.brandBackgroundPrimaryMedium, - color: theme.palette.vars.brandTextPrimary, + color: selected + ? theme.palette.vars.brandTextPrimary + : theme.palette.vars.brandTextSecondary, }, }); From ef44912809fbaa4b0383332b0aa0bd05a6afb3aa Mon Sep 17 00:00:00 2001 From: Yashwant Rautela Date: Tue, 11 Aug 2026 17:54:32 +0530 Subject: [PATCH 09/10] fix: Remove unused Theme import from Card tests Signed-off-by: Yashwant Rautela --- packages/open-ui-kit/src/components/card/__tests__/card.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/open-ui-kit/src/components/card/__tests__/card.test.tsx b/packages/open-ui-kit/src/components/card/__tests__/card.test.tsx index 7d094b5b..c99ba830 100644 --- a/packages/open-ui-kit/src/components/card/__tests__/card.test.tsx +++ b/packages/open-ui-kit/src/components/card/__tests__/card.test.tsx @@ -7,7 +7,6 @@ import React from "react"; import { render, screen } from "@testing-library/react"; import "@testing-library/jest-dom"; -import type { Theme } from "@mui/material/styles"; import { ThemeMode, ThemeProvider } from "@/theme-provider/theme-provider"; import { darkTheme } from "@/theme/dark/dark-theme"; import { lightTheme } from "@/theme/light/light-theme"; From 3fe8d0a76cedfae9782ec62e9352f70e4bf9fb49 Mon Sep 17 00:00:00 2001 From: Yashwant Rautela Date: Tue, 11 Aug 2026 17:55:06 +0530 Subject: [PATCH 10/10] fix: Format code for better readability in CodeBlock tests and styles Signed-off-by: Yashwant Rautela --- .../src/components/code-block/__tests__/code-block.test.tsx | 5 ++++- .../open-ui-kit/src/components/code-block/styles/index.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/open-ui-kit/src/components/code-block/__tests__/code-block.test.tsx b/packages/open-ui-kit/src/components/code-block/__tests__/code-block.test.tsx index 2a0b1edf..9e3cb57a 100644 --- a/packages/open-ui-kit/src/components/code-block/__tests__/code-block.test.tsx +++ b/packages/open-ui-kit/src/components/code-block/__tests__/code-block.test.tsx @@ -305,7 +305,10 @@ describe("CodeBlock", () => { walk(child, classes); } }; - walk(refractor.highlight(SAMPLE, "javascript") as unknown as HastNode, []); + walk( + refractor.highlight(SAMPLE, "javascript") as unknown as HastNode, + [], + ); return found[0] ?? []; }; diff --git a/packages/open-ui-kit/src/components/code-block/styles/index.ts b/packages/open-ui-kit/src/components/code-block/styles/index.ts index 0ed98fcb..50912db0 100644 --- a/packages/open-ui-kit/src/components/code-block/styles/index.ts +++ b/packages/open-ui-kit/src/components/code-block/styles/index.ts @@ -150,7 +150,10 @@ export const prismStyle = (theme: Theme): { [key: string]: CSSProperties } => { const { vars } = theme.palette; return { - 'pre[class*="language-"]': { background: "transparent", textShadow: "none" }, + 'pre[class*="language-"]': { + background: "transparent", + textShadow: "none", + }, 'code[class*="language-"]': { background: "transparent", textShadow: "none",