diff --git a/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx b/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx index a4e3a1d56b..a8a6dd1247 100644 --- a/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx +++ b/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx @@ -618,7 +618,7 @@ export function ComponentDetailPage({ return `import { Icon, SegmentedControl } from "@openbitfun/ui";\n\n, label: "${t("components.preview.segmentedChat")}", value: "chat" },\n { label: "${t("components.preview.segmentedAgent")}", value: "agent" },\n ]}\n/>`; } if (component.name === "StatusPill") { - return `import { Icon, StatusPill } from "@openbitfun/ui";\n\n} tone="${previewState}">\n Ask\n`; + return `import { Icon, StatusPill } from "@openbitfun/ui";\n\n} tone="${previewState}">\n Ask\n`; } if (component.name === "Disclosure") { const stateProps = previewState === "open" ? " defaultOpen" : previewState === "disabled" ? " disabled" : ""; @@ -1149,6 +1149,7 @@ export function ComponentDetailPage({ if (component.name === "StatusPill") { return ( } tone={state as StatusPillTone} > diff --git a/design-system/apps/design-lab/src/pages/PatternsPage.tsx b/design-system/apps/design-lab/src/pages/PatternsPage.tsx index 38264a4061..0c065e54c3 100644 --- a/design-system/apps/design-lab/src/pages/PatternsPage.tsx +++ b/design-system/apps/design-lab/src/pages/PatternsPage.tsx @@ -35,7 +35,7 @@ import { type TokenOverrides, } from "@openbitfun/ui"; import { useI18n, type MessageKey } from "../i18n"; -import { FormTypographyPattern, NestedMenuPattern, ProviderConfigurationPattern, SceneToolbarPattern, WorkspaceConfigurationPattern } from "./ReferencePatterns"; +import { IndicatorsPattern, FormTypographyPattern, NestedMenuPattern, ProviderConfigurationPattern, SceneToolbarPattern, WorkspaceConfigurationPattern } from "./ReferencePatterns"; interface PatternsPageProps { colorScheme: ColorScheme; @@ -80,6 +80,8 @@ export function PatternsPage({ colorScheme, contrast, density, tokenOverrides }:

{t("patterns.description")}

+ + diff --git a/design-system/apps/design-lab/src/pages/ReferencePatterns.tsx b/design-system/apps/design-lab/src/pages/ReferencePatterns.tsx index e0fb144cb4..d27dde18aa 100644 --- a/design-system/apps/design-lab/src/pages/ReferencePatterns.tsx +++ b/design-system/apps/design-lab/src/pages/ReferencePatterns.tsx @@ -16,6 +16,8 @@ import { MenuPopover, MultiSelect, NumberInput, + NumberBadge, + LauncherButton, PageHeader, SearchField, Select, @@ -36,6 +38,17 @@ import { } from "@openbitfun/ui"; import { useI18n } from "../i18n"; +export function IndicatorsPattern() { + const { t } = useI18n(); + const [enabled, setEnabled] = useState(false); + return + + K}>K + }>Ask{t("patterns.device.online")} + }>Hello + } />; +} + export function FormTypographyPattern() { const { t } = useI18n(); return diff --git a/design-system/packages/design-tokens/src/system.tokens.json b/design-system/packages/design-tokens/src/system.tokens.json index ad30d7bbf3..9857d65301 100644 --- a/design-system/packages/design-tokens/src/system.tokens.json +++ b/design-system/packages/design-tokens/src/system.tokens.json @@ -180,6 +180,10 @@ "lineHeight": { "$value": "{lineHeight.tight}" }, "letterSpacing": { "$value": "{letterSpacing.normal}" } }, + "statusPill": { + "$description": "Short status leading: 14px at 11px, scaling with typography.", + "lineHeight": { "$type": "number", "$value": 1.2727272727272727 } + }, "fieldHelper": { "$description": "Field helper leading: 16px at the default 11px type size, scaling with user typography.", "lineHeight": { "$type": "number", "$value": 1.4545454545454546 } @@ -291,6 +295,13 @@ } }, "code": { + "meta": { + "fontFamily": { "$value": "{font.family.mono}" }, + "fontSize": { "$value": "{font.size.meta}" }, + "fontWeight": { "$value": "{font.weight.regular}" }, + "lineHeight": { "$value": "{lineHeight.tight}" }, + "letterSpacing": { "$value": "{letterSpacing.normal}" } + }, "sm": { "fontFamily": { "$value": "{font.family.mono}" }, "fontSize": { "$value": "{font.size.xs}" }, @@ -618,11 +629,11 @@ "launcherButton": { "$description": "Geometry for a shell-edge launcher with an icon and short label.", "$type": "dimension", - "minInlineSize": { "$value": "104px" }, + "minInlineSize": { "$value": "72px" }, "blockSize": { "$value": "{control.height.md}" }, - "paddingInline": { "$value": "14px" }, - "gap": { "$value": "{space.2}" }, - "iconSize": { "$value": "16px" }, + "paddingInline": { "$value": "10px" }, + "gap": { "$value": "{space.1}" }, + "iconSize": { "$value": "12px" }, "radius": { "$value": "{radius.lg}" } }, "changeCount": { @@ -635,7 +646,7 @@ "statusPill": { "$type": "dimension", "gap": { "$value": "2px" }, - "paddingBlock": { "$value": "3px" }, + "paddingBlock": { "$value": "2px" }, "paddingInline": { "$value": "6px" }, "radius": { "$value": "{radius.pill}" }, "iconSize": { "$value": "14px" } diff --git a/design-system/packages/design-tokens/tests/contract.test.mjs b/design-system/packages/design-tokens/tests/contract.test.mjs index bacbf51dfa..045a34bc9e 100644 --- a/design-system/packages/design-tokens/tests/contract.test.mjs +++ b/design-system/packages/design-tokens/tests/contract.test.mjs @@ -114,7 +114,7 @@ test("StatusPill geometry preserves compact semantic status anatomy", async () = const systemDocument = await readSource("system.tokens.json"); assert.equal(tokens["control.statusPill.gap"], "2px"); - assert.equal(tokens["control.statusPill.paddingBlock"], "3px"); + assert.equal(tokens["control.statusPill.paddingBlock"], "2px"); assert.equal(tokens["control.statusPill.paddingInline"], "6px"); assert.equal(tokens["control.statusPill.iconSize"], "14px"); assert.equal(systemDocument.control.statusPill.radius.$value, "{radius.pill}"); @@ -145,11 +145,11 @@ test("ActionCard geometry preserves compact and descriptive entry compositions", test("LauncherButton geometry preserves the shell-edge action contract", async () => { const systemDocument = await readSource("system.tokens.json"); - assert.equal(tokens["control.launcherButton.minInlineSize"], "104px"); + assert.equal(tokens["control.launcherButton.minInlineSize"], "72px"); assert.equal(tokens["control.launcherButton.blockSize"], "40px"); - assert.equal(tokens["control.launcherButton.paddingInline"], "14px"); - assert.equal(tokens["control.launcherButton.gap"], "8px"); - assert.equal(tokens["control.launcherButton.iconSize"], "16px"); + assert.equal(tokens["control.launcherButton.paddingInline"], "10px"); + assert.equal(tokens["control.launcherButton.gap"], "4px"); + assert.equal(tokens["control.launcherButton.iconSize"], "12px"); assert.equal( systemDocument.control.launcherButton.radius.$value, "{radius.lg}", diff --git a/design-system/packages/theme-openbitfun/README.md b/design-system/packages/theme-openbitfun/README.md index 7f7f458db8..1c6173179d 100644 --- a/design-system/packages/theme-openbitfun/README.md +++ b/design-system/packages/theme-openbitfun/README.md @@ -79,3 +79,5 @@ in both root and chrome; generic palette projection previously reduced product menu labels to secondary text (60%). Explicit imported action colors still win. Action cards own `color.actionCard.background`: the light entry surface is black at 3% opacity. `surface.subtle` is a transient navy tint and `field.groupBackground` belongs to form groups, so neither represents this persistent action surface. Other modes retain their neutral action surface. Product Appearance preserves explicit legacy neutral-surface overrides in root and chrome. + +Compact indicators own `color.numberBadge.background` and `color.keyHint.content` so reference light values (8% fill and 60% text) can coexist with existing dark/contrast values and explicit legacy Appearance overrides. The light Switch off track and resting scrollbar thumb use 10% black; KeyHint and Launcher resting fills use 8%. Long status labels keep content colors; StatusPill emphasis is opt-in. diff --git a/design-system/packages/theme-openbitfun/src/dark.tokens.json b/design-system/packages/theme-openbitfun/src/dark.tokens.json index 52195b1bbc..70f0bae823 100644 --- a/design-system/packages/theme-openbitfun/src/dark.tokens.json +++ b/design-system/packages/theme-openbitfun/src/dark.tokens.json @@ -19,6 +19,9 @@ } }, "color": { + "numberBadge": { + "background": { "$type": "color", "$value": "{color.action.neutral.surface}" } + }, "actionCard": { "background": { "$type": "color", "$value": "{color.action.neutral.surface}" } }, @@ -60,6 +63,7 @@ } }, "keyHint": { + "content": { "$type": "color", "$value": "{color.content.muted}" }, "background": { "$type": "color", "$value": "{color.action.neutral.surface}" } }, "scrollbar": { diff --git a/design-system/packages/theme-openbitfun/src/high-contrast-dark.tokens.json b/design-system/packages/theme-openbitfun/src/high-contrast-dark.tokens.json index 40a32423cd..76117f9738 100644 --- a/design-system/packages/theme-openbitfun/src/high-contrast-dark.tokens.json +++ b/design-system/packages/theme-openbitfun/src/high-contrast-dark.tokens.json @@ -19,6 +19,12 @@ } }, "color": { + "keyHint": { + "content": { "$type": "color", "$value": "{color.content.muted}" } + }, + "numberBadge": { + "background": { "$type": "color", "$value": "{color.action.neutral.surface}" } + }, "actionCard": { "background": { "$type": "color", "$value": "{color.action.neutral.surface}" } }, diff --git a/design-system/packages/theme-openbitfun/src/high-contrast-light.tokens.json b/design-system/packages/theme-openbitfun/src/high-contrast-light.tokens.json index a542e03701..d5ee484751 100644 --- a/design-system/packages/theme-openbitfun/src/high-contrast-light.tokens.json +++ b/design-system/packages/theme-openbitfun/src/high-contrast-light.tokens.json @@ -19,6 +19,12 @@ } }, "color": { + "keyHint": { + "content": { "$type": "color", "$value": "{color.content.muted}" } + }, + "numberBadge": { + "background": { "$type": "color", "$value": "{color.action.neutral.surface}" } + }, "actionCard": { "background": { "$type": "color", "$value": "{color.action.neutral.surface}" } }, diff --git a/design-system/packages/theme-openbitfun/src/light.tokens.json b/design-system/packages/theme-openbitfun/src/light.tokens.json index 7d7ae605e2..cb6047f6d0 100644 --- a/design-system/packages/theme-openbitfun/src/light.tokens.json +++ b/design-system/packages/theme-openbitfun/src/light.tokens.json @@ -19,6 +19,9 @@ } }, "color": { + "numberBadge": { + "background": { "$type": "color", "$value": "{color.action.neutral.surfaceHover}" } + }, "actionCard": { "background": { "$type": "color", "$value": "rgba(0, 0, 0, 0.03)" } }, @@ -60,10 +63,11 @@ } }, "keyHint": { - "background": { "$type": "color", "$value": "{color.action.neutral.surface}" } + "content": { "$type": "color", "$value": "{color.content.secondary}" }, + "background": { "$type": "color", "$value": "{color.action.neutral.surfaceHover}" } }, "scrollbar": { - "thumb": { "$type": "color", "$value": "{color.action.neutral.surfaceHover}" }, + "thumb": { "$type": "color", "$value": "{color.action.neutral.surfacePressed}" }, "thumbHover": { "$type": "color", "$value": "{color.action.neutral.surfacePressed}" } }, "content": { @@ -196,7 +200,7 @@ }, "launcher": { "$description": "Shell-edge launcher states: neutral at rest, then cyan-tinted with cyan content on hover and press.", - "background": { "$type": "color", "$value": "{color.action.neutral.surfacePressed}" }, + "background": { "$type": "color", "$value": "{color.action.neutral.surfaceHover}" }, "backgroundHover": { "$type": "color", "$value": "color-mix(in srgb, {color.control.highlight.background} 20%, transparent)" }, "backgroundPressed": { "$type": "color", "$value": "color-mix(in srgb, {color.control.highlight.background} 30%, transparent)" }, "content": { "$type": "color", "$value": "{color.action.neutral.content}" }, @@ -205,7 +209,7 @@ "contentDisabled": { "$type": "color", "$value": "{color.action.neutral.contentDisabled}" } }, "switch": { - "track": { "$type": "color", "$value": "{ref.color.neutral.250}" }, + "track": { "$type": "color", "$value": "{color.action.neutral.surfacePressed}" }, "trackChecked": { "$type": "color", "$value": "{color.control.highlight.background}" }, "thumb": { "$type": "color", "$value": "{ref.color.neutral.0}" } } diff --git a/design-system/packages/theme-openbitfun/tests/theme-contract.test.mjs b/design-system/packages/theme-openbitfun/tests/theme-contract.test.mjs index cbffd84c40..ee0647988e 100644 --- a/design-system/packages/theme-openbitfun/tests/theme-contract.test.mjs +++ b/design-system/packages/theme-openbitfun/tests/theme-contract.test.mjs @@ -295,13 +295,13 @@ test("default modes preserve the built-in Appearance anchor values", () => { assert.equal(themes.light["color.selection.surface"], "rgba(0, 0, 0, 0.08)"); assert.equal(themes.light["color.surface.chrome"], "#f8f8f9"); assert.equal(themes.light["color.surface.tertiary"], "#f7f7f7"); - assert.equal(themes.light["color.scrollbar.thumb"], "rgba(0, 0, 0, 0.08)"); + assert.equal(themes.light["color.scrollbar.thumb"], "rgba(0, 0, 0, 0.10)"); assert.equal(themes.light["color.scrollbar.thumbHover"], "rgba(0, 0, 0, 0.10)"); - assert.equal(themes.light["color.keyHint.background"], "rgba(0, 0, 0, 0.05)"); + assert.equal(themes.light["color.keyHint.background"], "rgba(0, 0, 0, 0.08)"); assert.equal(themes.light["color.control.highlight.background"], "#059cb0"); assert.equal(themes.light["color.control.highlight.content"], "#000000"); assert.equal(themes.light["color.content.requiredIndicator"], "#059cb0"); - assert.equal(themes.light["color.control.launcher.background"], "rgba(0, 0, 0, 0.10)"); + assert.equal(themes.light["color.control.launcher.background"], "rgba(0, 0, 0, 0.08)"); assert.equal( themes.light["color.control.launcher.backgroundHover"], "color-mix(in srgb, #059cb0 20%, transparent)", @@ -313,7 +313,7 @@ test("default modes preserve the built-in Appearance anchor values", () => { assert.equal(themes.light["color.control.launcher.content"], "rgba(0, 0, 0, 0.80)"); assert.equal(themes.light["color.control.launcher.contentHover"], "#059cb0"); assert.equal(themes.light["color.control.launcher.contentPressed"], "#059cb0"); - assert.equal(themes.light["color.control.switch.track"], "#dddddd"); + assert.equal(themes.light["color.control.switch.track"], "rgba(0, 0, 0, 0.10)"); assert.equal(themes.light["color.control.switch.trackChecked"], "#059cb0"); assert.equal(themes.light["color.control.switch.thumb"], "#ffffff"); assert.equal(themes.light["color.identity.harness.minimal"], "#b434ef"); diff --git a/design-system/packages/ui/README.md b/design-system/packages/ui/README.md index 259bf1ce46..5b3b420a09 100644 --- a/design-system/packages/ui/README.md +++ b/design-system/packages/ui/README.md @@ -259,7 +259,7 @@ Use `canonicalIconNames` for galleries and pickers. `iconNames` also keeps the legacy `download`, `circle` and `turn` entries for compatibility; prefer `arrow-down`, `unselected` and `` respectively. `turn` is only the old empty background, not a complete numbered marker. -`NumberBadge` owns a 24px slot, a 20px surface and 11px medium text; longer +`NumberBadge` owns a 24px filled surface and 11px regular text; longer values grow horizontally. Callers supply formatted values and contextual accessible labels. `ToolbarBadge` delegates to the same anatomy. @@ -421,3 +421,5 @@ Dialog titles use 24px bold type with their own 29px line box and normal trackin Extra-large (`xl`) dialogs have an 800px maximum width and continue shrinking within the viewport gutter. Provider editing uses the floating footer; small workspace creation retains its attached footer and existing button/input sizes. The Lab workspace pattern uses local sample paths and callbacks only. PageHeader `md` uses the settings title with a primary 15px description; `display` uses the welcome heading and medium 17px introduction with a 12px gap. ActionCard uses 12px padding, section-heading typography (15px semibold), and a primary 13px single-line action description. Its inset outline does not inflate the 62px medium minimum height; longer content keeps the independent sibling actions and OverflowText behavior. + +KeyHint uses 10px text on a 10px line and 2px block padding. StatusPill uses a 14px line with 2px block padding; opt into `emphasis` for short mode labels such as Ask, while ordinary status descriptions retain readable content colors. LauncherButton owns a 72px minimum width, 40px height, 10px side padding, 4px gap, 12px icon and 11px monospace text. Product shells may retain a deliberate compact greeting that expands into this geometry. diff --git a/design-system/packages/ui/src/components/KeyHint/KeyHint.meta.ts b/design-system/packages/ui/src/components/KeyHint/KeyHint.meta.ts index 6b2ef77c68..dbbe23653d 100644 --- a/design-system/packages/ui/src/components/KeyHint/KeyHint.meta.ts +++ b/design-system/packages/ui/src/components/KeyHint/KeyHint.meta.ts @@ -11,9 +11,10 @@ export const keyHintMeta = { ], states: ["default"], tokens: [ - "color.content.muted", + "color.keyHint.content", "color.keyHint.background", "type.micro.fontSize", + "type.modifier.leading.none.lineHeight", "radius.xs", ], } as const satisfies ComponentMeta; diff --git a/design-system/packages/ui/src/components/KeyHint/KeyHint.module.css b/design-system/packages/ui/src/components/KeyHint/KeyHint.module.css index 25e459a11f..d6c92d2ae2 100644 --- a/design-system/packages/ui/src/components/KeyHint/KeyHint.module.css +++ b/design-system/packages/ui/src/components/KeyHint/KeyHint.module.css @@ -11,18 +11,22 @@ padding-inline: var(--openbitfun-space-1); border: 0; border-radius: var(--openbitfun-radius-xs); - color: var(--openbitfun-color-content-muted); + color: var(--openbitfun-color-key-hint-content); background: var(--openbitfun-color-key-hint-background); font-family: var(--openbitfun-type-micro-font-family); font-size: var(--openbitfun-type-micro-font-size); font-weight: var(--openbitfun-type-micro-font-weight); letter-spacing: var(--openbitfun-type-micro-letter-spacing); - line-height: var(--openbitfun-type-micro-line-height); + line-height: var(--openbitfun-type-modifier-leading-none-line-height); text-decoration: none; vertical-align: middle; white-space: nowrap; } + .icon { + --openbitfun-opacity-icon-artwork: 1; + } + .icon, .label { display: inline-flex; diff --git a/design-system/packages/ui/src/components/LauncherButton/LauncherButton.meta.ts b/design-system/packages/ui/src/components/LauncherButton/LauncherButton.meta.ts index 28a0c5cd0a..2f47247363 100644 --- a/design-system/packages/ui/src/components/LauncherButton/LauncherButton.meta.ts +++ b/design-system/packages/ui/src/components/LauncherButton/LauncherButton.meta.ts @@ -26,6 +26,6 @@ export const launcherButtonMeta = { "control.launcherButton.gap", "control.launcherButton.iconSize", "control.launcherButton.radius", - "type.code.md.fontSize", + "type.code.meta.fontSize", ], } as const satisfies ComponentMeta; diff --git a/design-system/packages/ui/src/components/LauncherButton/LauncherButton.module.css b/design-system/packages/ui/src/components/LauncherButton/LauncherButton.module.css index 4090d8b299..5c0ac181f7 100644 --- a/design-system/packages/ui/src/components/LauncherButton/LauncherButton.module.css +++ b/design-system/packages/ui/src/components/LauncherButton/LauncherButton.module.css @@ -19,12 +19,12 @@ border-radius: var(--openbitfun-control-launcher-button-radius) 0 0 0; color: var(--openbitfun-color-control-launcher-content); background: var(--openbitfun-color-control-launcher-background); - font-family: var(--openbitfun-type-code-md-font-family); - font-size: var(--openbitfun-type-code-md-font-size); - font-weight: var(--openbitfun-type-code-md-font-weight); + font-family: var(--openbitfun-type-code-meta-font-family); + font-size: var(--openbitfun-type-code-meta-font-size); + font-weight: var(--openbitfun-type-code-meta-font-weight); font-synthesis: none; - letter-spacing: var(--openbitfun-type-code-md-letter-spacing); - line-height: var(--openbitfun-type-code-md-line-height); + letter-spacing: var(--openbitfun-type-code-meta-letter-spacing); + line-height: var(--openbitfun-type-code-meta-line-height); white-space: nowrap; cursor: pointer; transition: diff --git a/design-system/packages/ui/src/components/NumberBadge/NumberBadge.meta.ts b/design-system/packages/ui/src/components/NumberBadge/NumberBadge.meta.ts index f4942f8bf4..8e5d29c2a6 100644 --- a/design-system/packages/ui/src/components/NumberBadge/NumberBadge.meta.ts +++ b/design-system/packages/ui/src/components/NumberBadge/NumberBadge.meta.ts @@ -2,7 +2,7 @@ import type { ComponentMeta } from "../../registry.types"; export const numberBadgeMeta = { category: "primitive", - description: "A compact numeric marker with a 24px slot, 20px surface and caller-owned value. Long values grow without clipping.", + description: "A compact numeric marker with a 24px filled surface and caller-owned value. Long values grow without clipping.", maturity: "stable", name: "NumberBadge", props: [ @@ -11,7 +11,7 @@ export const numberBadgeMeta = { ], states: ["default"], tokens: [ - "space.1", "space.5", "space.6", "type.meta.fontSize", "radius.pill", - "color.action.neutral.content", "color.action.neutral.surface", + "space.1", "space.6", "type.meta.fontSize", "radius.pill", + "color.action.neutral.content", "color.numberBadge.background", ], } as const satisfies ComponentMeta; diff --git a/design-system/packages/ui/src/components/NumberBadge/NumberBadge.module.css b/design-system/packages/ui/src/components/NumberBadge/NumberBadge.module.css index 7be94bc242..b0205bef10 100644 --- a/design-system/packages/ui/src/components/NumberBadge/NumberBadge.module.css +++ b/design-system/packages/ui/src/components/NumberBadge/NumberBadge.module.css @@ -7,7 +7,7 @@ justify-content: center; min-inline-size: var(--openbitfun-space-6); min-block-size: var(--openbitfun-space-6); - padding: calc(var(--openbitfun-space-1) / 2); + padding: 0; vertical-align: middle; color: var(--openbitfun-color-action-neutral-content); font-family: var(--openbitfun-type-meta-font-family); @@ -24,11 +24,11 @@ display: inline-flex; align-items: center; justify-content: center; - min-inline-size: var(--openbitfun-space-5); - min-block-size: var(--openbitfun-space-5); + min-inline-size: var(--openbitfun-space-6); + min-block-size: var(--openbitfun-space-6); padding-inline: calc(var(--openbitfun-space-1) / 2); border-radius: var(--openbitfun-radius-pill); - background: var(--openbitfun-color-action-neutral-surface); + background: var(--openbitfun-color-number-badge-background); } @media (forced-colors: active) { diff --git a/design-system/packages/ui/src/components/StatusPill/StatusPill.meta.ts b/design-system/packages/ui/src/components/StatusPill/StatusPill.meta.ts index 7e4a865c71..24c69d8e64 100644 --- a/design-system/packages/ui/src/components/StatusPill/StatusPill.meta.ts +++ b/design-system/packages/ui/src/components/StatusPill/StatusPill.meta.ts @@ -8,11 +8,17 @@ export const statusPillMeta = { props: [ { name: "children", type: "ReactNode" }, { name: "leading", type: "ReactNode" }, + { defaultValue: "false", name: "emphasis", type: "boolean" }, { defaultValue: "success", name: "tone", type: "neutral | accent | info | success | warning | danger" }, ], states: ["neutral", "accent", "info", "success", "warning", "danger"], tokens: [ - "color.surface.subtle", + "color.action.neutral.surface", + "color.status.info.emphasis", + "color.status.success.emphasis", + "color.status.warning.emphasis", + "color.status.danger.emphasis", + "type.statusPill.lineHeight", "color.content.secondary", "color.accent.default", "color.status.info.content", diff --git a/design-system/packages/ui/src/components/StatusPill/StatusPill.module.css b/design-system/packages/ui/src/components/StatusPill/StatusPill.module.css index 1db4c954f2..e17183a099 100644 --- a/design-system/packages/ui/src/components/StatusPill/StatusPill.module.css +++ b/design-system/packages/ui/src/components/StatusPill/StatusPill.module.css @@ -18,7 +18,7 @@ font-family: var(--openbitfun-type-meta-font-family); font-size: var(--openbitfun-type-meta-font-size); font-weight: var(--openbitfun-type-meta-font-weight); - line-height: var(--openbitfun-type-meta-line-height); + line-height: var(--openbitfun-type-status-pill-line-height); letter-spacing: var(--openbitfun-type-meta-letter-spacing); white-space: nowrap; } @@ -85,7 +85,28 @@ color: var(--openbitfun-color-status-danger-emphasis); } + .root[data-tone="info"][data-emphasis="true"] { + color: var(--openbitfun-color-status-info-emphasis); + } + + .root[data-tone="success"][data-emphasis="true"] { + color: var(--openbitfun-color-status-success-emphasis); + } + + .root[data-tone="warning"][data-emphasis="true"] { + color: var(--openbitfun-color-status-warning-emphasis); + } + + .root[data-tone="danger"][data-emphasis="true"] { + color: var(--openbitfun-color-status-danger-emphasis); + } + + .root[data-emphasis="true"] .leading { + --openbitfun-opacity-icon-artwork: 1; + } + @media (forced-colors: active) { + .root[data-tone][data-emphasis] { color: CanvasText; } .root[data-tone] .leading { color: CanvasText; } .root { diff --git a/design-system/packages/ui/src/components/StatusPill/StatusPill.tsx b/design-system/packages/ui/src/components/StatusPill/StatusPill.tsx index 1f385aa096..09cfc98393 100644 --- a/design-system/packages/ui/src/components/StatusPill/StatusPill.tsx +++ b/design-system/packages/ui/src/components/StatusPill/StatusPill.tsx @@ -20,6 +20,8 @@ export interface StatusPillProps children: ReactNode; leading?: ReactNode; tone?: StatusPillTone; + /** Strong color for short labels such as chat modes; prose keeps the default. */ + emphasis?: boolean; } export const StatusPill = forwardRef( @@ -28,6 +30,7 @@ export const StatusPill = forwardRef( className, leading, tone = "success", + emphasis = false, ...props }, ref) { return ( @@ -36,6 +39,7 @@ export const StatusPill = forwardRef( className={classNames(styles.root, className)} data-openbitfun-component="status-pill" data-tone={tone} + data-emphasis={emphasis ? "true" : "false"} ref={ref} > {leading !== undefined && leading !== null && ( diff --git a/design-system/packages/ui/tests/key-hint.test.mjs b/design-system/packages/ui/tests/key-hint.test.mjs index 0c14a2d556..aeada378f5 100644 --- a/design-system/packages/ui/tests/key-hint.test.mjs +++ b/design-system/packages/ui/tests/key-hint.test.mjs @@ -34,11 +34,11 @@ test("KeyHint styles use the shared micro typography role", async () => { assert.match(styles, /--openbitfun-color-key-hint-background/); assert.match(styles, /--openbitfun-color-action-neutral-surface/); - assert.match(styles, /--openbitfun-color-content-muted/); + assert.match(styles, /--openbitfun-color-key-hint-content/); assert.match(styles, /--openbitfun-type-micro-font-family/); assert.match(styles, /--openbitfun-type-micro-font-size/); assert.match(styles, /--openbitfun-type-micro-letter-spacing/); - assert.match(styles, /--openbitfun-type-micro-line-height/); + assert.match(styles, /--openbitfun-type-modifier-leading-none-line-height/); assert.match(styles, /--openbitfun-radius-xs/); assert.match(styles, /--openbitfun-radius-sm/); assert.match(styles, /flex:\s*0 0 auto/); diff --git a/design-system/packages/ui/tests/number-badge.test.mjs b/design-system/packages/ui/tests/number-badge.test.mjs index 00fdaaaacb..bfa209b960 100644 --- a/design-system/packages/ui/tests/number-badge.test.mjs +++ b/design-system/packages/ui/tests/number-badge.test.mjs @@ -23,9 +23,9 @@ test("ToolbarBadge preserves its slot hook and delegates to NumberBadge", () => assert.match(markup, /data-openbitfun-part="value">18<\/span>/); }); -test("NumberBadge uses a 24px slot, 20px surface and 11px control typography without clipping", async () => { +test("NumberBadge uses a 24px filled surface and 11px control typography without clipping", async () => { const source = await readFile(new URL("../src/components/NumberBadge/NumberBadge.module.css", import.meta.url), "utf8"); - for (const token of ["space-6", "space-5", "type-meta-font-size", "type-meta-font-family", "type-meta-font-weight", "color-action-neutral-content", "color-action-neutral-surface"]) assert.ok(source.includes(`--openbitfun-${token}`), token); + for (const token of ["space-6", "type-meta-font-size", "type-meta-font-family", "type-meta-font-weight", "color-action-neutral-content", "color-number-badge-background"]) assert.ok(source.includes(`--openbitfun-${token}`), token); assert.doesNotMatch(source, /overflow:\s*(hidden|clip)|text-overflow:\s*ellipsis/); assert.match(source, /forced-colors: active/); }); diff --git a/design-system/packages/ui/tests/status-pill.test.mjs b/design-system/packages/ui/tests/status-pill.test.mjs index ab39ef0fc1..68a4ef2b7f 100644 --- a/design-system/packages/ui/tests/status-pill.test.mjs +++ b/design-system/packages/ui/tests/status-pill.test.mjs @@ -10,6 +10,7 @@ test("StatusPill exposes semantic tone and independent label anatomy", () => { assert.match(markup, /data-openbitfun-component="status-pill"/); assert.match(markup, /data-tone="warning"/); + assert.match(markup, /data-emphasis="false"/); assert.match(markup, /data-openbitfun-part="label"/); assert.match(markup, /data-overflow-behavior="marquee"/); assert.match(markup, />Review { test("StatusPill keeps its optional leading indicator decorative", () => { const markup = renderToStaticMarkup(createElement( StatusPill, - { leading: createElement(Icon, { name: "circle" }) }, + { emphasis: true, leading: createElement(Icon, { name: "circle" }) }, "Ask", )); assert.match(markup, /data-tone="success"/); + assert.match(markup, /data-emphasis="true"/); assert.match(markup, /data-openbitfun-part="leading"/); assert.match(markup, /aria-hidden="true"/); }); diff --git a/src/apps/data-migrator/ui/generated/design-system.css b/src/apps/data-migrator/ui/generated/design-system.css index 56ba65ca00..baafeb7b33 100644 --- a/src/apps/data-migrator/ui/generated/design-system.css +++ b/src/apps/data-migrator/ui/generated/design-system.css @@ -98,10 +98,10 @@ --openbitfun-control-icon-button-xs-icon-size: 14px; --openbitfun-control-icon-button-xs-size: 22px; --openbitfun-control-launcher-button-block-size: var(--openbitfun-control-height-md); - --openbitfun-control-launcher-button-gap: var(--openbitfun-space-2); - --openbitfun-control-launcher-button-icon-size: 16px; - --openbitfun-control-launcher-button-min-inline-size: 104px; - --openbitfun-control-launcher-button-padding-inline: 14px; + --openbitfun-control-launcher-button-gap: var(--openbitfun-space-1); + --openbitfun-control-launcher-button-icon-size: 12px; + --openbitfun-control-launcher-button-min-inline-size: 72px; + --openbitfun-control-launcher-button-padding-inline: 10px; --openbitfun-control-launcher-button-radius: var(--openbitfun-radius-lg); --openbitfun-control-segmented-control-gap: var(--openbitfun-space-1); --openbitfun-control-segmented-control-icon-size: 12px; @@ -125,7 +125,7 @@ --openbitfun-control-select-trailing-inset: var(--openbitfun-space-3); --openbitfun-control-status-pill-gap: 2px; --openbitfun-control-status-pill-icon-size: 14px; - --openbitfun-control-status-pill-padding-block: 3px; + --openbitfun-control-status-pill-padding-block: 2px; --openbitfun-control-status-pill-padding-inline: 6px; --openbitfun-control-status-pill-radius: var(--openbitfun-radius-pill); --openbitfun-control-switch-thumb-inset: 2px; @@ -427,6 +427,11 @@ --openbitfun-type-code-md-font-weight: var(--openbitfun-font-weight-regular); --openbitfun-type-code-md-letter-spacing: var(--openbitfun-letter-spacing-normal); --openbitfun-type-code-md-line-height: var(--openbitfun-line-height-code); + --openbitfun-type-code-meta-font-family: var(--openbitfun-font-family-mono); + --openbitfun-type-code-meta-font-size: var(--openbitfun-font-size-meta); + --openbitfun-type-code-meta-font-weight: var(--openbitfun-font-weight-regular); + --openbitfun-type-code-meta-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-code-meta-line-height: var(--openbitfun-line-height-tight); --openbitfun-type-code-sm-font-family: var(--openbitfun-font-family-mono); --openbitfun-type-code-sm-font-size: var(--openbitfun-font-size-xs); --openbitfun-type-code-sm-font-weight: var(--openbitfun-font-weight-regular); @@ -629,6 +634,7 @@ --openbitfun-type-page-intro-font-weight: var(--openbitfun-font-weight-medium); --openbitfun-type-page-intro-letter-spacing: var(--openbitfun-letter-spacing-normal); --openbitfun-type-page-intro-line-height: var(--openbitfun-line-height-tight); + --openbitfun-type-status-pill-line-height: 1.2727272727272727; --openbitfun-type-support-font-family: var(--openbitfun-font-family-control); --openbitfun-type-support-font-size: var(--openbitfun-font-size-meta); --openbitfun-type-support-font-weight: var(--openbitfun-font-weight-regular); @@ -730,7 +736,7 @@ --openbitfun-color-content-secondary: rgba(0, 0, 0, 0.60); --openbitfun-color-control-highlight-background: #059cb0; --openbitfun-color-control-highlight-content: #000000; - --openbitfun-color-control-launcher-background: rgba(0, 0, 0, 0.10); + --openbitfun-color-control-launcher-background: rgba(0, 0, 0, 0.08); --openbitfun-color-control-launcher-background-hover: color-mix(in srgb, #059cb0 20%, transparent); --openbitfun-color-control-launcher-background-pressed: color-mix(in srgb, #059cb0 30%, transparent); --openbitfun-color-control-launcher-content: rgba(0, 0, 0, 0.80); @@ -738,7 +744,7 @@ --openbitfun-color-control-launcher-content-hover: #059cb0; --openbitfun-color-control-launcher-content-pressed: #059cb0; --openbitfun-color-control-switch-thumb: #ffffff; - --openbitfun-color-control-switch-track: #dddddd; + --openbitfun-color-control-switch-track: rgba(0, 0, 0, 0.10); --openbitfun-color-control-switch-track-checked: #059cb0; --openbitfun-color-field-background: #ffffff; --openbitfun-color-field-background-hover: #ffffff; @@ -764,11 +770,13 @@ --openbitfun-color-identity-harness-minimal: #b434ef; --openbitfun-color-identity-harness-standard: #1aa73e; --openbitfun-color-identity-harness-ultimate: #ff8c00; - --openbitfun-color-key-hint-background: rgba(0, 0, 0, 0.05); + --openbitfun-color-key-hint-background: rgba(0, 0, 0, 0.08); + --openbitfun-color-key-hint-content: rgba(0, 0, 0, 0.60); --openbitfun-color-link-default: #2563eb; --openbitfun-color-link-hover: #005fcc; + --openbitfun-color-number-badge-background: rgba(0, 0, 0, 0.08); --openbitfun-color-overlay-scrim: rgba(16, 26, 39, 0.20); - --openbitfun-color-scrollbar-thumb: rgba(0, 0, 0, 0.08); + --openbitfun-color-scrollbar-thumb: rgba(0, 0, 0, 0.10); --openbitfun-color-scrollbar-thumb-hover: rgba(0, 0, 0, 0.10); --openbitfun-color-selection-surface: rgba(0, 0, 0, 0.08); --openbitfun-color-status-danger-border: rgba(236, 34, 31, 0.3); @@ -915,8 +923,10 @@ --openbitfun-color-identity-harness-standard: #1aa73e; --openbitfun-color-identity-harness-ultimate: #ff8c00; --openbitfun-color-key-hint-background: rgba(255, 255, 255, 0.1); + --openbitfun-color-key-hint-content: #858585; --openbitfun-color-link-default: #60a5fa; --openbitfun-color-link-hover: #93c5fd; + --openbitfun-color-number-badge-background: rgba(255, 255, 255, 0.1); --openbitfun-color-overlay-scrim: rgba(0, 0, 0, 0.56); --openbitfun-color-scrollbar-thumb: rgba(255, 255, 255, 0.12); --openbitfun-color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.15); @@ -1004,7 +1014,7 @@ --openbitfun-color-content-secondary: #161616; --openbitfun-color-control-highlight-background: #005fcc; --openbitfun-color-control-highlight-content: #ffffff; - --openbitfun-color-control-launcher-background: #b3b3b3; + --openbitfun-color-control-launcher-background: #cccccc; --openbitfun-color-control-launcher-background-hover: color-mix(in srgb, #005fcc 20%, transparent); --openbitfun-color-control-launcher-background-pressed: color-mix(in srgb, #005fcc 30%, transparent); --openbitfun-color-control-launcher-content: #000000; @@ -1021,8 +1031,10 @@ --openbitfun-color-field-placeholder: #343434; --openbitfun-color-focus-ring: #005fcc; --openbitfun-color-identity-global-search-open-terminal: #000000; - --openbitfun-color-key-hint-background: #e6e6e6; - --openbitfun-color-scrollbar-thumb: #cccccc; + --openbitfun-color-key-hint-background: #cccccc; + --openbitfun-color-key-hint-content: #343434; + --openbitfun-color-number-badge-background: #e6e6e6; + --openbitfun-color-scrollbar-thumb: #b3b3b3; --openbitfun-color-scrollbar-thumb-hover: #b3b3b3; --openbitfun-color-selection-surface: #cccccc; --openbitfun-color-status-danger-content: #8e1413; @@ -1086,6 +1098,8 @@ --openbitfun-color-focus-ring: #ffcc00; --openbitfun-color-identity-global-search-open-terminal: #ffffff; --openbitfun-color-key-hint-background: #343434; + --openbitfun-color-key-hint-content: #d0d0d0; + --openbitfun-color-number-badge-background: #343434; --openbitfun-color-scrollbar-thumb: #505050; --openbitfun-color-scrollbar-thumb-hover: #6b6b6b; --openbitfun-color-selection-surface: #505050; diff --git a/src/apps/desktop/src/generated/bootstrap_theme.css b/src/apps/desktop/src/generated/bootstrap_theme.css index 829478a4f1..2fba242395 100644 --- a/src/apps/desktop/src/generated/bootstrap_theme.css +++ b/src/apps/desktop/src/generated/bootstrap_theme.css @@ -84,8 +84,10 @@ --openbitfun-color-identity-harness-standard: #1aa73e; --openbitfun-color-identity-harness-ultimate: #ff8c00; --openbitfun-color-key-hint-background: rgba(255, 255, 255, 0.1); + --openbitfun-color-key-hint-content: #858585; --openbitfun-color-link-default: #60a5fa; --openbitfun-color-link-hover: #93c5fd; + --openbitfun-color-number-badge-background: rgba(255, 255, 255, 0.1); --openbitfun-color-overlay-scrim: rgba(0, 0, 0, 0.56); --openbitfun-color-scrollbar-thumb: rgba(255, 255, 255, 0.12); --openbitfun-color-scrollbar-thumb-hover: rgba(255, 255, 255, 0.15); diff --git a/src/crates/contracts/product-domains/src/miniapp/generated/default_appearance_style.html b/src/crates/contracts/product-domains/src/miniapp/generated/default_appearance_style.html index e3002891a2..894bc7821c 100644 --- a/src/crates/contracts/product-domains/src/miniapp/generated/default_appearance_style.html +++ b/src/crates/contracts/product-domains/src/miniapp/generated/default_appearance_style.html @@ -66,7 +66,7 @@ --openbitfun-field-bg: #ffffff; --openbitfun-field-border: rgba(0, 0, 0, 0.08); --openbitfun-field-border-focus: #858585; - --openbitfun-scrollbar-thumb: rgba(0, 0, 0, 0.08); + --openbitfun-scrollbar-thumb: rgba(0, 0, 0, 0.10); --openbitfun-scrollbar-thumb-hover: rgba(0, 0, 0, 0.10); --openbitfun-overlay-scrim: rgba(16, 26, 39, 0.20); --openbitfun-shadow-xs: 0 1px 2px rgba(16, 26, 39, 0.04); diff --git a/src/web-ui/src/infrastructure/appearance/builtins/appearancePresetOutput.test.ts b/src/web-ui/src/infrastructure/appearance/builtins/appearancePresetOutput.test.ts index 9e0a070ad4..608a89ea50 100644 --- a/src/web-ui/src/infrastructure/appearance/builtins/appearancePresetOutput.test.ts +++ b/src/web-ui/src/infrastructure/appearance/builtins/appearancePresetOutput.test.ts @@ -52,6 +52,9 @@ describe('builtin appearance preset output', () => { if (name.startsWith('color.field.') || name === 'color.actionCard.background') expect(tokens?.[themeCssVariables[name as ThemeTokenName]]).toBe(value); } } + expect(settings?.tokens['--openbitfun-color-number-badge-background']).toBe(themes.light['color.numberBadge.background']); + expect(settings?.tokens['--openbitfun-color-key-hint-content']).toBe(themes.light['color.keyHint.content']); + expect(settings?.tokens['--openbitfun-color-scrollbar-thumb']).toBe(themes.light['color.scrollbar.thumb']); expect(settings?.tokens['--openbitfun-color-field-border']).toBe('rgba(0, 0, 0, 0.08)'); expect(settings?.tokens['--openbitfun-color-field-border-hover']).toBe('rgba(0, 0, 0, 0.20)'); expect(settings?.tokens['--openbitfun-color-field-border-active']).toBe('rgba(0, 0, 0, 0.20)'); diff --git a/src/web-ui/src/infrastructure/appearance/builtins/buildBuiltinAppearance.ts b/src/web-ui/src/infrastructure/appearance/builtins/buildBuiltinAppearance.ts index 837a3a8ce0..48d959986e 100644 --- a/src/web-ui/src/infrastructure/appearance/builtins/buildBuiltinAppearance.ts +++ b/src/web-ui/src/infrastructure/appearance/builtins/buildBuiltinAppearance.ts @@ -80,6 +80,9 @@ function themeValuesToCssTokens( // Neutral action labels are primary text in the public theme. The generic // palette's secondary text projection used to make product menus too faint. tokens['--openbitfun-color-action-neutral-content'] = String(themes.light['color.action.neutral.content']); + tokens['--openbitfun-color-scrollbar-thumb'] = String(themes.light['color.scrollbar.thumb']); + tokens['--openbitfun-color-number-badge-background'] = String(themes.light['color.numberBadge.background']); + tokens['--openbitfun-color-key-hint-content'] = String(themes.light['color.keyHint.content']); tokens['--openbitfun-color-action-card-background'] = String(themes.light['color.actionCard.background']); tokens['--openbitfun-color-content-caption'] = String(themes.light['color.content.caption']); // Default light fields use the published neutral states in both root and @@ -134,6 +137,8 @@ function createThemeTokenValues(palette: AppearancePalette): Record { schema: 'openbitfun.appearance', schemaVersion: 2, id: 'example.legacy-cards', name: 'Legacy cards', version: '1.0.0', mode: 'light', renderers: { 'theme-tokens': { version: 1, settings: { - tokens: { '--openbitfun-color-action-neutral-surface': '#123456' }, - scopes: { chrome: { '--openbitfun-color-action-neutral-surface': '#654321' } }, + tokens: { '--openbitfun-color-action-neutral-surface': '#123456', '--openbitfun-color-content-muted': '#556677' }, + scopes: { chrome: { '--openbitfun-color-action-neutral-surface': '#654321', '--openbitfun-color-content-muted': '#778899' } }, } } }, }; const payload = JSON.stringify(original); const resolved = composeAppearancePackage(JSON.parse(payload)); const settings = resolved.renderers!['theme-tokens']!.settings; expect(settings.tokens['--openbitfun-color-action-card-background']).toBe('#123456'); + expect(settings.tokens['--openbitfun-color-number-badge-background']).toBe('#123456'); + expect(settings.tokens['--openbitfun-color-key-hint-content']).toBe('#556677'); + expect(settings.scopes?.chrome?.['--openbitfun-color-number-badge-background']).toBe('#654321'); + expect(settings.scopes?.chrome?.['--openbitfun-color-key-hint-content']).toBe('#778899'); expect(settings.scopes?.chrome?.['--openbitfun-color-action-card-background']).toBe('#654321'); expect(composeAppearancePackage(JSON.parse(JSON.stringify(resolved))).renderers?.['theme-tokens']).toEqual(resolved.renderers?.['theme-tokens']); expect(JSON.stringify(original)).toBe(payload); original.renderers!['theme-tokens']!.settings.tokens['--openbitfun-color-action-card-background'] = '#112233'; original.renderers!['theme-tokens']!.settings.scopes!.chrome!['--openbitfun-color-action-card-background'] = '#334455'; + original.renderers!['theme-tokens']!.settings.tokens['--openbitfun-color-number-badge-background'] = '#aabbcc'; + original.renderers!['theme-tokens']!.settings.scopes!.chrome!['--openbitfun-color-key-hint-content'] = '#ddeeff'; const explicit = composeAppearancePackage(original).renderers!['theme-tokens']!.settings; + expect(explicit.tokens['--openbitfun-color-number-badge-background']).toBe('#aabbcc'); + expect(explicit.scopes?.chrome?.['--openbitfun-color-key-hint-content']).toBe('#ddeeff'); expect(explicit.tokens['--openbitfun-color-action-card-background']).toBe('#112233'); expect(explicit.scopes?.chrome?.['--openbitfun-color-action-card-background']).toBe('#334455'); }); diff --git a/src/web-ui/src/infrastructure/appearance/builtins/composeAppearancePackage.ts b/src/web-ui/src/infrastructure/appearance/builtins/composeAppearancePackage.ts index c961c578e8..31f54cc66e 100644 --- a/src/web-ui/src/infrastructure/appearance/builtins/composeAppearancePackage.ts +++ b/src/web-ui/src/infrastructure/appearance/builtins/composeAppearancePackage.ts @@ -16,6 +16,7 @@ import { withLegacyButtonTokens } from './buttonThemeCompatibility'; import { withLegacyFieldTokens } from './fieldThemeCompatibility'; import { withLegacyCaptionToken } from './captionThemeCompatibility'; import { withLegacyActionCardToken } from './actionCardThemeCompatibility'; +import { withLegacyIndicatorTokens } from './indicatorThemeCompatibility'; const COMPOSITION_LAYERS = Symbol('openbitfun.appearance.composition-layers'); @@ -124,12 +125,12 @@ function mergeRenderers( ...override['theme-tokens'].settings, tokens: { ...(base?.['theme-tokens']?.settings.tokens ?? {}), - ...withLegacyActionCardToken(withLegacyCaptionToken(withLegacyFieldTokens(withLegacyButtonTokens(override['theme-tokens'].settings.tokens)))), + ...withLegacyIndicatorTokens(withLegacyActionCardToken(withLegacyCaptionToken(withLegacyFieldTokens(withLegacyButtonTokens(override['theme-tokens'].settings.tokens))))), }, scopes: { chrome: { ...(base?.['theme-tokens']?.settings.scopes?.chrome ?? {}), - ...withLegacyActionCardToken(withLegacyCaptionToken(withLegacyFieldTokens(withLegacyButtonTokens(override['theme-tokens'].settings.scopes?.chrome)))), + ...withLegacyIndicatorTokens(withLegacyActionCardToken(withLegacyCaptionToken(withLegacyFieldTokens(withLegacyButtonTokens(override['theme-tokens'].settings.scopes?.chrome))))), }, }, }, diff --git a/src/web-ui/src/infrastructure/appearance/builtins/indicatorThemeCompatibility.ts b/src/web-ui/src/infrastructure/appearance/builtins/indicatorThemeCompatibility.ts new file mode 100644 index 0000000000..7c52b9962b --- /dev/null +++ b/src/web-ui/src/infrastructure/appearance/builtins/indicatorThemeCompatibility.ts @@ -0,0 +1,14 @@ +import type { AppearanceThemeTokenName } from '../types'; + +export function withLegacyIndicatorTokens( + tokens: Partial> | undefined, +): Partial> { + const result = { ...tokens }; + for (const [next, previous] of [ + ['--openbitfun-color-number-badge-background', '--openbitfun-color-action-neutral-surface'], + ['--openbitfun-color-key-hint-content', '--openbitfun-color-content-muted'], + ] as const) { + if (result[next] === undefined && tokens?.[previous] !== undefined) result[next] = tokens[previous]; + } + return result; +} diff --git a/src/web-ui/src/tools/generative-widget/appearancePayload.test.ts b/src/web-ui/src/tools/generative-widget/appearancePayload.test.ts index e0f24ff45d..45cafd9dd5 100644 --- a/src/web-ui/src/tools/generative-widget/appearancePayload.test.ts +++ b/src/web-ui/src/tools/generative-widget/appearancePayload.test.ts @@ -49,6 +49,12 @@ const CAPTION_THEME_VARIABLE_NAME = '--openbitfun-color-content-caption'; // Persistent action cards own their fill independently of transient feedback // and form groups; keep the shared contract fingerprint unchanged. const ACTION_CARD_THEME_VARIABLE_NAME = '--openbitfun-color-action-card-background'; +// Compact indicators own badge fill and key-hint contrast independently of +// shared action surfaces and muted prose; enumerate only these additions. +const INDICATOR_THEME_VARIABLE_NAMES = [ + '--openbitfun-color-key-hint-content', + '--openbitfun-color-number-badge-background', +] as const; const RETIRED_WIDGET_VARIABLE_NAMES = [ '--background-primary', '--bg-primary', @@ -93,15 +99,18 @@ describe('generated widget appearance payload contract', () => { expect(new Set(WIDGET_APPEARANCE_VAR_NAMES).size).toBe(WIDGET_APPEARANCE_VAR_NAMES.length); const buttonNames = WIDGET_APPEARANCE_VAR_NAMES.filter(name => name.startsWith('--openbitfun-component-button-')); const fieldStateNames = WIDGET_APPEARANCE_VAR_NAMES.filter(name => FIELD_STATE_THEME_VARIABLE_NAMES.some(fieldName => fieldName === name)); + const indicatorNames = WIDGET_APPEARANCE_VAR_NAMES.filter(name => INDICATOR_THEME_VARIABLE_NAMES.some(indicatorName => indicatorName === name)); const sharedNames = WIDGET_APPEARANCE_VAR_NAMES.filter(name => ( !name.startsWith('--openbitfun-component-button-') && !FIELD_STATE_THEME_VARIABLE_NAMES.some(fieldName => fieldName === name) && name !== FIELD_GROUP_THEME_VARIABLE_NAME && name !== CAPTION_THEME_VARIABLE_NAME && name !== ACTION_CARD_THEME_VARIABLE_NAME + && !INDICATOR_THEME_VARIABLE_NAMES.some(indicatorName => indicatorName === name) )); expect(buttonNames).toEqual(BUTTON_THEME_VARIABLE_NAMES); expect(fieldStateNames).toEqual(FIELD_STATE_THEME_VARIABLE_NAMES); + expect(indicatorNames).toEqual(INDICATOR_THEME_VARIABLE_NAMES); expect(WIDGET_APPEARANCE_VAR_NAMES).toContain(FIELD_GROUP_THEME_VARIABLE_NAME); expect(WIDGET_APPEARANCE_VAR_NAMES).toContain(CAPTION_THEME_VARIABLE_NAME); expect(WIDGET_APPEARANCE_VAR_NAMES).toContain(ACTION_CARD_THEME_VARIABLE_NAME); @@ -168,6 +177,8 @@ describe('generated widget appearance payload contract', () => { '--openbitfun-color-field-border-active': 'rgba(0, 0, 0, 0.20)', '--openbitfun-color-field-group-background': 'rgba(0, 0, 0, 0.03)', '--openbitfun-color-field-placeholder': 'rgba(0, 0, 0, 0.40)', + '--openbitfun-color-key-hint-content': 'rgba(0, 0, 0, 0.55)', + '--openbitfun-color-number-badge-background': 'rgba(0, 0, 0, 0.09)', '--openbitfun-color-status-danger-surface': 'rgba(200, 0, 0, 0.12)', '--openbitfun-color-status-danger-border': '#303030', '--openbitfun-shadow-raised': '0 1px 2px #404040',