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);
@@ -24,11 +25,12 @@ test("StatusPill renders the brand accent tone", () => {
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',