From ed25259b9472700fc40adc0cd860d0970d0625b2 Mon Sep 17 00:00:00 2001 From: kev1n77 Date: Tue, 8 Sep 2026 17:50:20 +0800 Subject: [PATCH 1/3] fix(ui): align button state colors without resizing Make outline and text surfaces transparent, align fill and primary feedback with the UIKit design, and preserve variant-specific disabled content. Keep all existing button dimensions, spacing, typography and icon sizes. Publish Button-owned theme colors across light, dark and contrast modes; preserve shared action colors, branded presets and explicit legacy Appearance overrides. Show the same public buttons on two Design Lab backgrounds and refresh the generated desktop and offline theme projections. --- .../apps/design-lab/src/pages/ColorsPage.tsx | 3 +- .../src/pages/ComponentDetailPage.tsx | 63 +++++++++--------- design-system/apps/design-lab/src/styles.css | 16 +++++ .../design-lab/src/token-editor/catalog.ts | 4 +- .../vite/token-authoring-plugin.mjs | 5 +- .../packages/theme-openbitfun/README.md | 14 ++++ .../theme-openbitfun/scripts/build.mjs | 2 +- .../theme-openbitfun/src/dark.tokens.json | 18 +++++ .../src/high-contrast-dark.tokens.json | 18 +++++ .../src/high-contrast-light.tokens.json | 18 +++++ .../theme-openbitfun/src/light.tokens.json | 18 +++++ .../tests/theme-contract.test.mjs | 29 ++++++++- design-system/packages/ui/README.md | 11 ++++ .../ui/src/components/Button/Button.meta.ts | 25 +++---- .../src/components/Button/Button.module.css | 65 +++++++++++-------- .../packages/ui/tests/button.test.mjs | 39 +++++++---- .../packages/ui/tests/registry.test.mjs | 1 + scripts/audit-theme-colors.test.mjs | 2 +- scripts/theme-css-var-contract.mjs | 2 +- .../ui/generated/design-system.css | 46 +++++++++++++ .../desktop/src/generated/bootstrap_theme.css | 13 ++++ .../appearance/appearanceTokenContract.ts | 10 ++- .../builtins/appearancePresetOutput.test.ts | 28 +++++++- .../builtins/buildBuiltinAppearance.ts | 21 +++++- .../builtins/buttonThemeCompatibility.ts | 33 ++++++++++ .../builtins/composeAppearancePackage.test.ts | 50 ++++++++++++++ .../builtins/composeAppearancePackage.ts | 5 +- 27 files changed, 461 insertions(+), 98 deletions(-) create mode 100644 src/web-ui/src/infrastructure/appearance/builtins/buttonThemeCompatibility.ts diff --git a/design-system/apps/design-lab/src/pages/ColorsPage.tsx b/design-system/apps/design-lab/src/pages/ColorsPage.tsx index 9c961a6de9..83b20c17f5 100644 --- a/design-system/apps/design-lab/src/pages/ColorsPage.tsx +++ b/design-system/apps/design-lab/src/pages/ColorsPage.tsx @@ -34,7 +34,7 @@ interface ColorsPageProps { } const semanticColorTokens = themeTokenCatalog.filter( - (token): token is ThemeTokenCatalogEntry => token.category === "color", + (token): token is ThemeTokenCatalogEntry => token.type === "color", ); const semanticGroupOrder: readonly SemanticColorGroup[] = [ @@ -126,6 +126,7 @@ const referenceNameByValue = new Map( ); function getSemanticGroup(token: ThemeTokenCatalogEntry): SemanticColorGroup { + if (token.name.startsWith("component.button.")) return "action"; const segment = token.name.split(".")[1] as SemanticColorGroup | undefined; return segment && semanticGroupOrder.includes(segment) ? segment : "accent"; } diff --git a/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx b/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx index a691451490..178bce5e15 100644 --- a/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx +++ b/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx @@ -2106,38 +2106,43 @@ export function ComponentDetailPage({ ))} ) : component.name === "Button" ? ( -
- - {states.map((state, index) => ( - ( +
+

{t(surface === "plain" ? "detail.option.plain" : "detail.option.subtle")}

+
- {stateLabel(state)} - - ))} - {buttonVariants.map((matrixVariant) => ( - - - {stateLabel(matrixVariant)} - - {states.map((state) => ( -
+ {states.map((state, index) => ( + - {renderPreview(state, matrixVariant)} -
+ {stateLabel(state)} + ))} -
- ))} -
+ {buttonVariants.map((matrixVariant) => ( + + + {stateLabel(matrixVariant)} + + {states.map((state) => ( +
+ {renderPreview(state, matrixVariant)} +
+ ))} +
+ ))} +
+ + )) ) : component.name === "Icon" ? ( <> diff --git a/design-system/apps/design-lab/src/styles.css b/design-system/apps/design-lab/src/styles.css index 2e1e93c8f2..a5abfd547a 100644 --- a/design-system/apps/design-lab/src/styles.css +++ b/design-system/apps/design-lab/src/styles.css @@ -4135,6 +4135,22 @@ input.lab-force-focus { min-width: 112px; } +.button-state-surface { + width: 100%; + background: var(--openbitfun-color-surface-panel); +} + +.button-state-surface[data-surface="subtle"] { + background: var(--openbitfun-color-surface-tertiary); +} + +.button-state-surface > h3 { + margin: 0; + padding: var(--openbitfun-space-4); + color: var(--openbitfun-color-content-primary); + font-size: var(--openbitfun-type-body-sm-font-size); +} + .component-code-panel--standalone { border-top: 1px solid var(--openbitfun-color-border-subtle); background: var(--openbitfun-color-surface-panel); diff --git a/design-system/apps/design-lab/src/token-editor/catalog.ts b/design-system/apps/design-lab/src/token-editor/catalog.ts index 6dc33fdd85..390642b96c 100644 --- a/design-system/apps/design-lab/src/token-editor/catalog.ts +++ b/design-system/apps/design-lab/src/token-editor/catalog.ts @@ -32,11 +32,11 @@ export const editableTokenCatalog: readonly EditableToken[] = [ ]; export const colorTokenCatalog: readonly EditableToken[] = editableTokenCatalog.filter( - (token) => token.category === "color", + (token) => token.type === "color", ); export const nonColorTokenCatalog: readonly EditableToken[] = editableTokenCatalog.filter( - (token) => token.category !== "color", + (token) => token.type !== "color", ); const categoryLabels: Readonly> = { diff --git a/design-system/apps/design-lab/vite/token-authoring-plugin.mjs b/design-system/apps/design-lab/vite/token-authoring-plugin.mjs index 2a5b3031ea..5a2a03fd6b 100644 --- a/design-system/apps/design-lab/vite/token-authoring-plugin.mjs +++ b/design-system/apps/design-lab/vite/token-authoring-plugin.mjs @@ -197,7 +197,9 @@ async function applySourceChanges(designSystemDirectory, changes) { if (!target) { throw new Error(`Unsupported token target "${targetKey}".`); } - if (change.collection === "theme" && !change.name.startsWith("color.")) { + if (change.collection === "theme" + && !change.name.startsWith("color.") + && !change.name.startsWith("component.button.")) { throw new Error("Only public semantic color tokens can be written to theme sources."); } if (change.collection === "system" && change.name.startsWith("color.")) { @@ -348,4 +350,3 @@ export function createTokenAuthoringPlugin({ designSystemDirectory }) { }, }; } - diff --git a/design-system/packages/theme-openbitfun/README.md b/design-system/packages/theme-openbitfun/README.md index cb312dba2c..549e73f356 100644 --- a/design-system/packages/theme-openbitfun/README.md +++ b/design-system/packages/theme-openbitfun/README.md @@ -28,6 +28,20 @@ The same data is available as `@openbitfun/theme-openbitfun/reference-colors.jso ## Surface and state roles +`component.button.*` owns Button's state palette. Its light fill stays at black +8% while the shared neutral actions retain their 5/8/10% feedback; its primary +background uses black 80/60/90% and disabled content 20%. Outline and text variants +composite directly over the caller's surface. These differences cannot be +represented by changing the shared action palette without changing menus, +IconButton, and other controls. Dark and high-contrast mappings retain their +mode-specific feedback and outline contrast. Color entries remain editable in +Design Lab's Colors catalog; Button geometry is independent of this palette. + +The default Web UI appearances consume these published component values. +Branded presets and imported appearances may still supply the existing action +tokens: the Web UI inherits explicit old values only when the corresponding +component token is absent, and keeps explicit component overrides intact. + - `color.surface.scene`, `panel`, and `raised` own primary content and elevated planes. - `color.surface.chrome` owns persistent application structure such as navigation and window-control regions. - `color.surface.tertiary` is an opaque low-emphasis fill for persistent grouped content such as cards and field groups. diff --git a/design-system/packages/theme-openbitfun/scripts/build.mjs b/design-system/packages/theme-openbitfun/scripts/build.mjs index 8a3fc239df..96e20fe42a 100644 --- a/design-system/packages/theme-openbitfun/scripts/build.mjs +++ b/design-system/packages/theme-openbitfun/scripts/build.mjs @@ -38,7 +38,7 @@ const highContrastDarkTokens = resolveTokens( mergeTokenDocuments(reference, dark, highContrastDark), ); -const PUBLIC_THEME_TOKEN_PREFIXES = ["color.", "effect.", "opacity.", "shadow."]; +const PUBLIC_THEME_TOKEN_PREFIXES = ["color.", "component.button.", "effect.", "opacity.", "shadow."]; const REFERENCE_COLOR_TOKEN_PATTERN = /^ref\.color\.([a-z][a-z0-9-]*)\.(\d+)$/; function createReferenceColorArtifacts(document, tokens) { diff --git a/design-system/packages/theme-openbitfun/src/dark.tokens.json b/design-system/packages/theme-openbitfun/src/dark.tokens.json index 01d68d59d9..6e395cebee 100644 --- a/design-system/packages/theme-openbitfun/src/dark.tokens.json +++ b/design-system/packages/theme-openbitfun/src/dark.tokens.json @@ -1,5 +1,23 @@ { "$description": "OpenBitFun dark semantic theme migrated from the built-in dark Appearance.", + "component": { + "button": { + "$description": "Button-specific state colors. Keep shared action colors stable for other controls and preserve contrast-mode feedback.", + "content": { "$type": "color", "$value": "{color.action.neutral.content}" }, + "textContent": { "$type": "color", "$value": "{color.accent.default}" }, + "textContentHover": { "$type": "color", "$value": "{color.accent.hover}" }, + "textContentDisabled": { "$type": "color", "$value": "{color.accent.disabled}" }, + "outlineBorder": { "$type": "color", "$value": "{color.action.neutral.border}" }, + "outlineBorderInteractive": { "$type": "color", "$value": "{color.action.neutral.border}" }, + "fillBackground": { "$type": "color", "$value": "{color.action.neutral.surface}" }, + "fillBackgroundHover": { "$type": "color", "$value": "{color.action.neutral.surfaceHover}" }, + "fillBackgroundPressed": { "$type": "color", "$value": "{color.action.neutral.surfacePressed}" }, + "primaryBackground": { "$type": "color", "$value": "{color.action.primary.background}" }, + "primaryBackgroundHover": { "$type": "color", "$value": "{color.action.primary.hover}" }, + "primaryBackgroundPressed": { "$type": "color", "$value": "{color.action.primary.pressed}" }, + "primaryContentDisabled": { "$type": "color", "$value": "{color.action.neutral.contentDisabled}" } + } + }, "color": { "surface": { "canvas": { "$type": "color", "$value": "{ref.color.neutral.950}" }, 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 65e78d2401..fe7434f4e5 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 @@ -1,5 +1,23 @@ { "$description": "High-contrast overrides for the dark scheme.", + "component": { + "button": { + "$description": "Button-specific state colors. Keep shared action colors stable for other controls and preserve contrast-mode feedback.", + "content": { "$type": "color", "$value": "{color.action.neutral.content}" }, + "textContent": { "$type": "color", "$value": "{color.accent.default}" }, + "textContentHover": { "$type": "color", "$value": "{color.accent.hover}" }, + "textContentDisabled": { "$type": "color", "$value": "{color.accent.disabled}" }, + "outlineBorder": { "$type": "color", "$value": "{color.action.neutral.border}" }, + "outlineBorderInteractive": { "$type": "color", "$value": "{color.action.neutral.border}" }, + "fillBackground": { "$type": "color", "$value": "{color.action.neutral.surface}" }, + "fillBackgroundHover": { "$type": "color", "$value": "{color.action.neutral.surfaceHover}" }, + "fillBackgroundPressed": { "$type": "color", "$value": "{color.action.neutral.surfacePressed}" }, + "primaryBackground": { "$type": "color", "$value": "{color.action.primary.background}" }, + "primaryBackgroundHover": { "$type": "color", "$value": "{color.action.primary.hover}" }, + "primaryBackgroundPressed": { "$type": "color", "$value": "{color.action.primary.pressed}" }, + "primaryContentDisabled": { "$type": "color", "$value": "{color.action.neutral.contentDisabled}" } + } + }, "color": { "surface": { "canvas": { "$type": "color", "$value": "{ref.color.gray.1000}" }, 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 ae860587ba..a522d1e2b8 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 @@ -1,5 +1,23 @@ { "$description": "High-contrast overrides for the light scheme.", + "component": { + "button": { + "$description": "Button-specific state colors. Keep shared action colors stable for other controls and preserve contrast-mode feedback.", + "content": { "$type": "color", "$value": "{color.action.neutral.content}" }, + "textContent": { "$type": "color", "$value": "{color.accent.default}" }, + "textContentHover": { "$type": "color", "$value": "{color.accent.hover}" }, + "textContentDisabled": { "$type": "color", "$value": "{color.accent.disabled}" }, + "outlineBorder": { "$type": "color", "$value": "{color.action.neutral.border}" }, + "outlineBorderInteractive": { "$type": "color", "$value": "{color.action.neutral.border}" }, + "fillBackground": { "$type": "color", "$value": "{color.action.neutral.surface}" }, + "fillBackgroundHover": { "$type": "color", "$value": "{color.action.neutral.surfaceHover}" }, + "fillBackgroundPressed": { "$type": "color", "$value": "{color.action.neutral.surfacePressed}" }, + "primaryBackground": { "$type": "color", "$value": "{color.action.primary.background}" }, + "primaryBackgroundHover": { "$type": "color", "$value": "{color.action.primary.hover}" }, + "primaryBackgroundPressed": { "$type": "color", "$value": "{color.action.primary.pressed}" }, + "primaryContentDisabled": { "$type": "color", "$value": "{color.action.neutral.contentDisabled}" } + } + }, "color": { "surface": { "canvas": { "$type": "color", "$value": "{ref.color.gray.0}" }, diff --git a/design-system/packages/theme-openbitfun/src/light.tokens.json b/design-system/packages/theme-openbitfun/src/light.tokens.json index 34a882bb11..7f997a7fbc 100644 --- a/design-system/packages/theme-openbitfun/src/light.tokens.json +++ b/design-system/packages/theme-openbitfun/src/light.tokens.json @@ -1,5 +1,23 @@ { "$description": "OpenBitFun light semantic theme migrated from the built-in light Appearance.", + "component": { + "button": { + "$description": "Button-specific state colors. Keep shared action colors stable for other controls and preserve contrast-mode feedback.", + "content": { "$type": "color", "$value": "{color.action.neutral.content}" }, + "textContent": { "$type": "color", "$value": "{color.accent.default}" }, + "textContentHover": { "$type": "color", "$value": "{color.accent.hover}" }, + "textContentDisabled": { "$type": "color", "$value": "{color.accent.disabled}" }, + "outlineBorder": { "$type": "color", "$value": "{color.action.neutral.surfaceHover}" }, + "outlineBorderInteractive": { "$type": "color", "$value": "transparent" }, + "fillBackground": { "$type": "color", "$value": "{color.action.neutral.surfaceHover}" }, + "fillBackgroundHover": { "$type": "color", "$value": "{color.action.neutral.surfaceHover}" }, + "fillBackgroundPressed": { "$type": "color", "$value": "{color.action.neutral.surfaceHover}" }, + "primaryBackground": { "$type": "color", "$value": "rgba(0, 0, 0, 0.80)" }, + "primaryBackgroundHover": { "$type": "color", "$value": "rgba(0, 0, 0, 0.60)" }, + "primaryBackgroundPressed": { "$type": "color", "$value": "rgba(0, 0, 0, 0.90)" }, + "primaryContentDisabled": { "$type": "color", "$value": "rgba(0, 0, 0, 0.20)" } + } + }, "color": { "surface": { "canvas": { "$type": "color", "$value": "{ref.color.neutral.50}" }, 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 3bf4ff581a..c915eb0721 100644 --- a/design-system/packages/theme-openbitfun/tests/theme-contract.test.mjs +++ b/design-system/packages/theme-openbitfun/tests/theme-contract.test.mjs @@ -255,6 +255,29 @@ test("text, action, and field focus pairs meet their contrast requirements", () } }); +test("Button states have a mode-complete palette independent from shared actions", () => { + const light = themes.light; + assert.equal(light["component.button.outlineBorder"], "rgba(0, 0, 0, 0.08)"); + assert.equal(light["component.button.outlineBorderInteractive"], "transparent"); + for (const suffix of ["", "Hover", "Pressed"]) { + assert.equal(light[`component.button.fillBackground${suffix}`], "rgba(0, 0, 0, 0.08)"); + } + assert.equal(light["component.button.primaryBackground"], "rgba(0, 0, 0, 0.80)"); + assert.equal(light["component.button.primaryBackgroundHover"], "rgba(0, 0, 0, 0.60)"); + assert.equal(light["component.button.primaryBackgroundPressed"], "rgba(0, 0, 0, 0.90)"); + assert.equal(light["component.button.primaryContentDisabled"], "rgba(0, 0, 0, 0.20)"); + assert.equal(light["component.button.textContent"], "#059cb0"); + assert.equal(light["component.button.textContentDisabled"], "rgba(5, 156, 176, 0.30)"); + const names = Object.keys(light).filter(name => name.startsWith("component.button.")); + for (const [mode, values] of Object.entries(themes)) { + assert.deepEqual(Object.keys(values).filter(name => name.startsWith("component.button.")), names); + if (mode === "light") continue; + assert.equal(values["component.button.primaryBackground"], values["color.action.primary.background"]); + assert.equal(values["component.button.outlineBorderInteractive"], values["color.action.neutral.border"]); + assert.equal(values["component.button.fillBackgroundPressed"], values["color.action.neutral.surfacePressed"]); + } +}); + test("default modes preserve the built-in Appearance anchor values", () => { assert.equal(themes.light["color.surface.canvas"], "#fdfdfd"); assert.equal(themes.light["color.content.primary"], "rgba(0, 0, 0, 0.80)"); @@ -358,11 +381,13 @@ test("public theme catalog contains only semantic theme tokens for every mode", assert.equal(themeTokenCatalog.length, Object.keys(themes.light).length); for (const token of themeTokenCatalog) { assert.equal( - ["color.", "effect.", "opacity.", "shadow."].some((prefix) => token.name.startsWith(prefix)), + ["color.", "component.button.", "effect.", "opacity.", "shadow."].some((prefix) => token.name.startsWith(prefix)), true, ); assert.equal(token.name.startsWith("ref."), false); - if (token.name.startsWith("color.")) assert.equal(token.type, "color"); + if (token.name.startsWith("color.") || token.name.startsWith("component.button.")) { + assert.equal(token.type, "color"); + } assert.deepEqual(Object.keys(token.values), themeModes); } }); diff --git a/design-system/packages/ui/README.md b/design-system/packages/ui/README.md index b43b1db6b0..3e0093c28c 100644 --- a/design-system/packages/ui/README.md +++ b/design-system/packages/ui/README.md @@ -18,8 +18,19 @@ export function Example() { The package owns component anatomy, behavior, accessibility, and stable variants. It does not own theme selection persistence, product state, routes, locale resources, or platform APIs. +## Buttons + +Button outline and text variants have transparent resting surfaces. Fill and +primary state colors come from the theme's `component.button.*` contract, with +variant-specific disabled content. The secondary variant keeps its opaque +tertiary surface. All variants retain the existing xs/sm/md/lg dimensions; +text buttons keep those hit targets while omitting the visible pill background +and radius. Use native hover, pressed, focus, disabled, and loading behavior in +addition to Design Lab's state specimens. + ## Text overflow + Use `OverflowText` for single-line, non-editable labels instead of local `text-overflow: ellipsis` rules or shortening the underlying string. Plain text defaults to **fade-out truncation with an interaction marquee**: a background-independent diff --git a/design-system/packages/ui/src/components/Button/Button.meta.ts b/design-system/packages/ui/src/components/Button/Button.meta.ts index 87ae082b27..b8831e2325 100644 --- a/design-system/packages/ui/src/components/Button/Button.meta.ts +++ b/design-system/packages/ui/src/components/Button/Button.meta.ts @@ -2,7 +2,7 @@ import type { ComponentMeta } from "../../registry.types"; export const buttonMeta = { category: "action", - description: "A compact opaque pill action with outline, neutral fill, secondary, primary, and text presentation variants.", + description: "An action with transparent outline and text variants, neutral fill, secondary, and primary presentation.", maturity: "stable", name: "Button", props: [ @@ -15,22 +15,25 @@ export const buttonMeta = { ], states: ["default", "hover", "active", "disabled"], tokens: [ - "color.action.neutral.border", + "component.button.content", + "component.button.outlineBorder", + "component.button.outlineBorderInteractive", + "component.button.fillBackground", + "component.button.fillBackgroundHover", + "component.button.fillBackgroundPressed", + "component.button.primaryBackground", + "component.button.primaryBackgroundHover", + "component.button.primaryBackgroundPressed", + "component.button.primaryContentDisabled", + "component.button.textContent", + "component.button.textContentHover", + "component.button.textContentDisabled", "color.action.neutral.content", "color.action.neutral.contentDisabled", - "color.action.neutral.surface", - "color.action.neutral.surfaceHover", - "color.action.neutral.surfacePressed", - "color.action.primary.background", "color.action.primary.content", - "color.action.primary.hover", - "color.action.primary.pressed", "color.surface.tertiary", "color.border.default", "color.border.strong", - "color.accent.default", - "color.accent.hover", - "color.accent.disabled", "color.focus.ring", "color.status.danger.border", "color.status.danger.content", diff --git a/design-system/packages/ui/src/components/Button/Button.module.css b/design-system/packages/ui/src/components/Button/Button.module.css index 97f19e2862..60b12d398b 100644 --- a/design-system/packages/ui/src/components/Button/Button.module.css +++ b/design-system/packages/ui/src/components/Button/Button.module.css @@ -1,12 +1,13 @@ @layer openbitfun.components { .button { - --_button-background: var(--openbitfun-color-surface-tertiary); - --_button-background-hover: var(--openbitfun-color-action-neutral-surface); - --_button-background-active: var(--openbitfun-color-action-neutral-surface); - --_button-border: var(--openbitfun-color-action-neutral-border); - --_button-border-hover: var(--_button-border); - --_button-border-active: var(--_button-border); - --_button-content: var(--openbitfun-color-action-neutral-content); + --_button-background: transparent; + --_button-background-hover: var(--openbitfun-component-button-fill-background); + --_button-background-active: var(--openbitfun-component-button-fill-background); + --_button-border: var(--openbitfun-component-button-outline-border); + --_button-border-hover: var(--openbitfun-component-button-outline-border-interactive); + --_button-border-active: var(--openbitfun-component-button-outline-border-interactive); + --_button-content: var(--openbitfun-component-button-content); + --_button-content-disabled: var(--openbitfun-color-action-neutral-content-disabled); --_button-font-size: var(--openbitfun-type-label-md-font-size); --_button-height: var(--openbitfun-control-height-md); --_button-leading-icon-size: 16px; @@ -25,7 +26,7 @@ border: var(--openbitfun-border-width-default) solid var(--_button-border); border-radius: var(--openbitfun-radius-pill); color: var(--_button-content); - background: var(--openbitfun-color-surface-tertiary); + background: transparent; font-family: var(--openbitfun-type-label-md-font-family); font-size: var(--_button-font-size); font-weight: var(--openbitfun-type-label-md-font-weight); @@ -73,23 +74,28 @@ } .button:disabled { - color: var(--openbitfun-color-action-neutral-content-disabled); + color: var(--_button-content-disabled); cursor: not-allowed; } .button[data-openbitfun-variant="fill"] { - --_button-background: var(--openbitfun-color-action-neutral-surface); - --_button-background-hover: var(--openbitfun-color-action-neutral-surface-hover); - --_button-background-active: var(--openbitfun-color-action-neutral-surface-pressed); + --_button-background: var(--openbitfun-component-button-fill-background); + --_button-background-hover: var(--openbitfun-component-button-fill-background-hover); + --_button-background-active: var(--openbitfun-component-button-fill-background-pressed); --_button-border: transparent; + --_button-border-hover: transparent; + --_button-border-active: transparent; } .button[data-openbitfun-variant="primary"] { - --_button-background: var(--openbitfun-color-action-primary-background); - --_button-background-hover: var(--openbitfun-color-action-primary-hover); - --_button-background-active: var(--openbitfun-color-action-primary-pressed); + --_button-background: var(--openbitfun-component-button-primary-background); + --_button-background-hover: var(--openbitfun-component-button-primary-background-hover); + --_button-background-active: var(--openbitfun-component-button-primary-background-pressed); --_button-border: transparent; + --_button-border-hover: transparent; + --_button-border-active: transparent; --_button-content: var(--openbitfun-color-action-primary-content); + --_button-content-disabled: var(--openbitfun-component-button-primary-content-disabled); } .button[data-openbitfun-variant="secondary"] { @@ -103,8 +109,7 @@ } .button[data-openbitfun-variant="primary"]:disabled { - --_button-background: var(--openbitfun-color-action-neutral-surface); - --_button-content: var(--openbitfun-color-action-neutral-content-disabled); + --_button-background: var(--openbitfun-component-button-fill-background); } .button[data-openbitfun-variant="secondary"]:disabled { @@ -113,40 +118,44 @@ } .button[data-openbitfun-variant="text"] { - --_button-background: var(--openbitfun-color-surface-tertiary); - --_button-background-hover: var(--openbitfun-color-action-neutral-surface-hover); - --_button-background-active: var(--openbitfun-color-action-neutral-surface-pressed); + --_button-background: transparent; + --_button-background-hover: transparent; + --_button-background-active: transparent; --_button-border: transparent; - --_button-content: var(--openbitfun-color-accent-default); + --_button-border-hover: transparent; + --_button-border-active: transparent; + --_button-content: var(--openbitfun-component-button-text-content); + --_button-content-disabled: var(--openbitfun-component-button-text-content-disabled); + + border-radius: 0; } .button[data-openbitfun-variant="text"]:is(:hover, [data-openbitfun-preview-state="hover"]):not(:disabled) { - --_button-content: var(--openbitfun-color-accent-hover); + --_button-content: var(--openbitfun-component-button-text-content-hover); text-decoration: underline; text-underline-offset: 0.12em; } .button[data-openbitfun-variant="text"]:is(:active, [data-openbitfun-preview-state="active"]):not(:disabled) { - --_button-content: var(--openbitfun-color-accent-default); + --_button-content: var(--openbitfun-component-button-text-content); text-decoration: underline; text-underline-offset: 0.12em; } - .button[data-openbitfun-variant="text"]:disabled { - --_button-content: var(--openbitfun-color-accent-disabled); - } - .button[data-openbitfun-variant="fill"]:disabled { - --_button-background: var(--openbitfun-color-action-neutral-surface); + --_button-background: var(--openbitfun-component-button-fill-background); } .button[data-openbitfun-tone="danger"] { --_button-background-hover: var(--openbitfun-color-status-danger-surface); --_button-background-active: var(--openbitfun-color-status-danger-surface); --_button-border: var(--openbitfun-color-status-danger-border); + --_button-border-hover: var(--_button-border); + --_button-border-active: var(--_button-border); --_button-content: var(--openbitfun-color-status-danger-content); + --_button-content-disabled: var(--openbitfun-color-action-neutral-content-disabled); } .button[data-openbitfun-tone="danger"][data-openbitfun-variant="fill"] { diff --git a/design-system/packages/ui/tests/button.test.mjs b/design-system/packages/ui/tests/button.test.mjs index 1ebb066452..3de1d2c450 100644 --- a/design-system/packages/ui/tests/button.test.mjs +++ b/design-system/packages/ui/tests/button.test.mjs @@ -80,10 +80,10 @@ test("primary and text variants expose semantic emphasis without changing button assert.match(primaryMarkup, /data-openbitfun-variant="primary"/); assert.match(textMarkup, /data-openbitfun-variant="text"/); - assert.match(styles, /--openbitfun-color-action-primary-background/); + assert.match(styles, /--openbitfun-component-button-primary-background/); assert.match(styles, /--openbitfun-color-action-primary-content/); - assert.match(styles, /--openbitfun-color-accent-default/); - assert.match(styles, /--openbitfun-color-accent-disabled/); + assert.match(styles, /--openbitfun-component-button-text-content/); + assert.match(styles, /--openbitfun-component-button-text-content-disabled/); assert.match(styles, /text-decoration:underline/); }); @@ -100,7 +100,7 @@ test("secondary exposes the filled secondary-action contract", async () => { assert.match(styles, /--openbitfun-color-action-neutral-content/); }); -test("every Button variant is composited over an opaque surface", async () => { +test("outline and text composite over the caller surface without changing their hit targets", async () => { const styles = await readFile( new URL("../src/components/Button/Button.module.css", import.meta.url), "utf8", @@ -108,7 +108,7 @@ test("every Button variant is composited over an opaque surface", async () => { assert.match( styles, - /\.button\s*\{[^}]*background:\s*var\(--openbitfun-color-surface-tertiary\)/s, + /\.button\s*\{[^}]*background:\s*transparent/s, ); assert.match( styles, @@ -116,9 +116,13 @@ test("every Button variant is composited over an opaque surface", async () => { ); assert.match( styles, - /\[data-openbitfun-variant="text"\]\s*\{[^}]*--_button-background:\s*var\(--openbitfun-color-surface-tertiary\)/s, + /\[data-openbitfun-variant="text"\]\s*\{[^}]*--_button-background:\s*transparent/s, ); - assert.doesNotMatch(styles, /--_button-background(?:-hover|-active)?:\s*transparent/); + const textRule = styles.match(/\[data-openbitfun-variant="text"\]\s*\{([^}]+)\}/)?.[1] ?? ""; + assert.match(textRule, /--_button-background-hover:\s*transparent/); + assert.match(textRule, /--_button-background-active:\s*transparent/); + assert.match(textRule, /border-radius:\s*0/); + assert.doesNotMatch(textRule, /(?:block-size|padding|font-size|icon-size):/); assert.doesNotMatch(styles, /\[data-openbitfun-variant="text"\][^}]*padding-inline:\s*0/s); }); @@ -194,11 +198,24 @@ test("real and preview active states share the semibold component rule", async ( assert.equal((styles.match(/--openbitfun-type-label-selected-font-weight/g) ?? []).length, 1); }); -test("fill uses neutral semantic state colors and icons inherit content color", async () => { +test("fill uses Button state colors and icons inherit content color", async () => { const styles = await readFile(new URL("../dist/styles.css", import.meta.url), "utf8"); - assert.match(styles, /--_button-background:\s*var\(--openbitfun-color-action-neutral-surface\)/); - assert.match(styles, /--_button-background-hover:\s*var\(--openbitfun-color-action-neutral-surface-hover\)/); - assert.match(styles, /--_button-background-active:\s*var\(--openbitfun-color-action-neutral-surface-pressed\)/); + assert.match(styles, /--_button-background:\s*var\(--openbitfun-component-button-fill-background\)/); + assert.match(styles, /--_button-background-hover:\s*var\(--openbitfun-component-button-fill-background-hover\)/); + assert.match(styles, /--_button-background-active:\s*var\(--openbitfun-component-button-fill-background-pressed\)/); assert.match(styles, /color:currentColor/); }); + +test("native disabled and loading use variant-specific disabled content with a neutral danger fallback", async () => { + const styles = await readFile(new URL("../src/components/Button/Button.module.css", import.meta.url), "utf8"); + assert.match(styles, /\.button:disabled\s*\{[^}]*color:\s*var\(--_button-content-disabled\)/); + for (const variant of ["primary", "text"]) { + const rule = styles.match(new RegExp(`\\[data-openbitfun-variant="${variant}"\\]\\s*\\{([^}]+)\\}`))?.[1] ?? ""; + assert.match(rule, new RegExp(`--_button-content-disabled:\\s*var\\(--openbitfun-component-button-${variant}-content-disabled\\)`)); + const markup = renderToStaticMarkup(createElement(Button, { variant, loading: true }, "Save")); + assert.match(markup, /disabled=""/); + assert.match(markup, /aria-busy="true"/); + } + assert.match(styles, /\[data-openbitfun-tone="danger"\]\s*\{[^}]*--_button-content-disabled:\s*var\(--openbitfun-color-action-neutral-content-disabled\)/); +}); diff --git a/design-system/packages/ui/tests/registry.test.mjs b/design-system/packages/ui/tests/registry.test.mjs index 1bb4bf2525..d1e488e42c 100644 --- a/design-system/packages/ui/tests/registry.test.mjs +++ b/design-system/packages/ui/tests/registry.test.mjs @@ -122,6 +122,7 @@ test("every registered component declares states and owned tokens", () => { (token) => token.startsWith("border.") || token.startsWith("color.") || + (component.name === "Button" && token.startsWith("component.button.")) || token.startsWith("control.") || token.startsWith("effect.") || token.startsWith("font.") || diff --git a/scripts/audit-theme-colors.test.mjs b/scripts/audit-theme-colors.test.mjs index 98b9850c28..84938d4f42 100644 --- a/scripts/audit-theme-colors.test.mjs +++ b/scripts/audit-theme-colors.test.mjs @@ -162,7 +162,7 @@ test('theme CSS var contract registry is explicit and non-overlapping', () => { assert.equal( CANONICAL_THEME_COLOR_TOKENS.every(token => ( /^--openbitfun-[a-z0-9-]+$/.test(token.cssVariable) - && token.name.startsWith('color.') + && (token.name.startsWith('color.') || token.name.startsWith('component.button.')) && typeof token.value === 'string' )), true, diff --git a/scripts/theme-css-var-contract.mjs b/scripts/theme-css-var-contract.mjs index d7bf882316..808ed6e379 100644 --- a/scripts/theme-css-var-contract.mjs +++ b/scripts/theme-css-var-contract.mjs @@ -22,7 +22,7 @@ const openbitfunThemeTokens = resolveTokens(mergeTokenDocuments( readDesignSystemTokenDocument('design-system/packages/theme-openbitfun/src/reference.tokens.json'), readDesignSystemTokenDocument('design-system/packages/theme-openbitfun/src/light.tokens.json'), )); -const publicThemePrefixes = ['color.', 'effect.', 'opacity.', 'shadow.']; +const publicThemePrefixes = ['color.', 'component.button.', 'effect.', 'opacity.', 'shadow.']; export const PACKAGE_CSS_VAR_DEFINITION_CONTRACTS = Object.freeze([ Object.freeze({ diff --git a/src/apps/data-migrator/ui/generated/design-system.css b/src/apps/data-migrator/ui/generated/design-system.css index 70e3125ba3..6c3851c5e0 100644 --- a/src/apps/data-migrator/ui/generated/design-system.css +++ b/src/apps/data-migrator/ui/generated/design-system.css @@ -768,6 +768,19 @@ --openbitfun-color-surface-subtle: rgba(16, 26, 39, 0.03); --openbitfun-color-surface-tertiary: #f7f7f7; --openbitfun-color-surface-workbench: #f3f3f5; + --openbitfun-component-button-content: rgba(0, 0, 0, 0.80); + --openbitfun-component-button-fill-background: rgba(0, 0, 0, 0.08); + --openbitfun-component-button-fill-background-hover: rgba(0, 0, 0, 0.08); + --openbitfun-component-button-fill-background-pressed: rgba(0, 0, 0, 0.08); + --openbitfun-component-button-outline-border: rgba(0, 0, 0, 0.08); + --openbitfun-component-button-outline-border-interactive: transparent; + --openbitfun-component-button-primary-background: rgba(0, 0, 0, 0.80); + --openbitfun-component-button-primary-background-hover: rgba(0, 0, 0, 0.60); + --openbitfun-component-button-primary-background-pressed: rgba(0, 0, 0, 0.90); + --openbitfun-component-button-primary-content-disabled: rgba(0, 0, 0, 0.20); + --openbitfun-component-button-text-content: #059cb0; + --openbitfun-component-button-text-content-disabled: rgba(5, 156, 176, 0.30); + --openbitfun-component-button-text-content-hover: #059cb0; --openbitfun-effect-blur-base: blur(8px) saturate(1.05); --openbitfun-effect-blur-medium: blur(12px) saturate(1.2); --openbitfun-effect-blur-subtle: blur(4px) saturate(1.02); @@ -900,6 +913,19 @@ --openbitfun-color-surface-subtle: rgba(255, 255, 255, 0.06); --openbitfun-color-surface-tertiary: #0e0e10; --openbitfun-color-surface-workbench: #0e0e10; + --openbitfun-component-button-content: #b0b0b0; + --openbitfun-component-button-fill-background: rgba(255, 255, 255, 0.1); + --openbitfun-component-button-fill-background-hover: rgba(255, 255, 255, 0.12); + --openbitfun-component-button-fill-background-pressed: rgba(255, 255, 255, 0.15); + --openbitfun-component-button-outline-border: rgba(255, 255, 255, 0.18); + --openbitfun-component-button-outline-border-interactive: rgba(255, 255, 255, 0.18); + --openbitfun-component-button-primary-background: rgba(255, 255, 255, 0.16); + --openbitfun-component-button-primary-background-hover: rgba(255, 255, 255, 0.24); + --openbitfun-component-button-primary-background-pressed: rgba(255, 255, 255, 0.2); + --openbitfun-component-button-primary-content-disabled: #555555; + --openbitfun-component-button-text-content: #60a5fa; + --openbitfun-component-button-text-content-disabled: rgba(96, 165, 250, 0.30); + --openbitfun-component-button-text-content-hover: #3b82f6; --openbitfun-effect-blur-base: blur(8px) saturate(1.1); --openbitfun-effect-blur-medium: blur(12px) saturate(1.2); --openbitfun-effect-blur-subtle: blur(4px) saturate(1.05); @@ -968,6 +994,16 @@ --openbitfun-color-status-warning-content: #663800; --openbitfun-color-status-warning-surface: rgba(255, 140, 0, 0.04); --openbitfun-color-surface-canvas: #ffffff; + --openbitfun-component-button-content: #000000; + --openbitfun-component-button-fill-background: #e6e6e6; + --openbitfun-component-button-fill-background-hover: #cccccc; + --openbitfun-component-button-fill-background-pressed: #b3b3b3; + --openbitfun-component-button-outline-border: #000000; + --openbitfun-component-button-outline-border-interactive: #000000; + --openbitfun-component-button-primary-background: #000000; + --openbitfun-component-button-primary-background-hover: #262626; + --openbitfun-component-button-primary-background-pressed: #000000; + --openbitfun-component-button-primary-content-disabled: #343434; } } @@ -1022,5 +1058,15 @@ --openbitfun-color-surface-scene: #090909; --openbitfun-color-surface-tertiary: #000000; --openbitfun-color-surface-workbench: #000000; + --openbitfun-component-button-content: #ffffff; + --openbitfun-component-button-fill-background: #343434; + --openbitfun-component-button-fill-background-hover: #505050; + --openbitfun-component-button-fill-background-pressed: #6b6b6b; + --openbitfun-component-button-outline-border: #ffffff; + --openbitfun-component-button-outline-border-interactive: #ffffff; + --openbitfun-component-button-primary-background: #ffffff; + --openbitfun-component-button-primary-background-hover: #e6e6e6; + --openbitfun-component-button-primary-background-pressed: #ffffff; + --openbitfun-component-button-primary-content-disabled: #d0d0d0; } } diff --git a/src/apps/desktop/src/generated/bootstrap_theme.css b/src/apps/desktop/src/generated/bootstrap_theme.css index 21f99392e9..ebd81937ec 100644 --- a/src/apps/desktop/src/generated/bootstrap_theme.css +++ b/src/apps/desktop/src/generated/bootstrap_theme.css @@ -109,6 +109,19 @@ --openbitfun-color-surface-subtle: rgba(255, 255, 255, 0.06); --openbitfun-color-surface-tertiary: #0e0e10; --openbitfun-color-surface-workbench: #0e0e10; + --openbitfun-component-button-content: #b0b0b0; + --openbitfun-component-button-fill-background: rgba(255, 255, 255, 0.1); + --openbitfun-component-button-fill-background-hover: rgba(255, 255, 255, 0.12); + --openbitfun-component-button-fill-background-pressed: rgba(255, 255, 255, 0.15); + --openbitfun-component-button-outline-border: rgba(255, 255, 255, 0.18); + --openbitfun-component-button-outline-border-interactive: rgba(255, 255, 255, 0.18); + --openbitfun-component-button-primary-background: rgba(255, 255, 255, 0.16); + --openbitfun-component-button-primary-background-hover: rgba(255, 255, 255, 0.24); + --openbitfun-component-button-primary-background-pressed: rgba(255, 255, 255, 0.2); + --openbitfun-component-button-primary-content-disabled: #555555; + --openbitfun-component-button-text-content: #60a5fa; + --openbitfun-component-button-text-content-disabled: rgba(96, 165, 250, 0.30); + --openbitfun-component-button-text-content-hover: #3b82f6; --openbitfun-effect-blur-base: blur(8px) saturate(1.1); --openbitfun-effect-blur-medium: blur(12px) saturate(1.2); --openbitfun-effect-blur-subtle: blur(4px) saturate(1.05); diff --git a/src/web-ui/src/infrastructure/appearance/appearanceTokenContract.ts b/src/web-ui/src/infrastructure/appearance/appearanceTokenContract.ts index 6cc6ca5e82..884b68fe70 100644 --- a/src/web-ui/src/infrastructure/appearance/appearanceTokenContract.ts +++ b/src/web-ui/src/infrastructure/appearance/appearanceTokenContract.ts @@ -109,18 +109,22 @@ export const APPEARANCE_DOMAIN_TOKEN_NAMES = Object.freeze([ ]); export const APPEARANCE_COMPONENT_TOKEN_NAMES = Object.freeze( - prefix(componentTokenSuffixes, '--openbitfun-component-'), + [ + // Public component colors are theme-owned; product-only component tokens stay above. + ...Object.values(themeCssVariables).filter(name => name.startsWith('--openbitfun-component-')), + ...prefix(componentTokenSuffixes, '--openbitfun-component-'), + ], ); export const APPEARANCE_ROOT_THEME_TOKEN_NAMES = Object.freeze( Object.values(themeCssVariables) as AppearanceThemeTokenName[], ); -export const APPEARANCE_ROOT_TOKEN_NAMES = Object.freeze([ +export const APPEARANCE_ROOT_TOKEN_NAMES = Object.freeze([...new Set([ ...APPEARANCE_ROOT_THEME_TOKEN_NAMES, ...APPEARANCE_DOMAIN_TOKEN_NAMES, ...APPEARANCE_COMPONENT_TOKEN_NAMES, -]); +])]); export const APPEARANCE_SCOPED_TOKEN_NAMES = APPEARANCE_ROOT_THEME_TOKEN_NAMES; 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 418b980531..411f185970 100644 --- a/src/web-ui/src/infrastructure/appearance/builtins/appearancePresetOutput.test.ts +++ b/src/web-ui/src/infrastructure/appearance/builtins/appearancePresetOutput.test.ts @@ -1,4 +1,4 @@ -import { themes } from '@openbitfun/theme-openbitfun'; +import { themeCssVariables, themes, type ThemeTokenName } from '@openbitfun/theme-openbitfun'; import { createHash } from 'node:crypto'; import { describe, expect, it } from 'vitest'; @@ -45,6 +45,32 @@ function statusContrast(content: string, tint: string, background: string): numb } describe('builtin appearance preset output', () => { + it('uses the public Button palette in default appearances without changing shared action colors', () => { + for (const mode of ['light', 'dark'] as const) { + const appearance = getBuiltinAppearance(`openbitfun-${mode}`); + const settings = appearance?.renderers?.['theme-tokens']?.settings; + for (const [name, value] of Object.entries(themes[mode])) { + if (!name.startsWith('component.button.')) continue; + expect(settings?.tokens[themeCssVariables[name as ThemeTokenName]]).toBe(value); + } + } + const light = getBuiltinAppearanceThemeTokens('openbitfun-light'); + expect(light['--openbitfun-component-button-content']).toBe('rgba(0, 0, 0, 0.80)'); + expect(light['--openbitfun-component-button-text-content']).toBe('#059cb0'); + expect(light['--openbitfun-color-action-primary-background']).toBe('#101a27'); + expect(light['--openbitfun-color-action-neutral-content']).toBe('rgba(0, 0, 0, 0.60)'); + }); + + it('preserves the action colors of branded presets through the Button contract', () => { + for (const palette of builtinAppearancePalettes) { + if (palette.id === 'openbitfun-light' || palette.id === 'openbitfun-dark') continue; + const tokens = getBuiltinAppearanceThemeTokens(palette.id); + expect(tokens['--openbitfun-component-button-primary-background']).toBe(tokens['--openbitfun-color-action-primary-background']); + expect(tokens['--openbitfun-component-button-text-content']).toBe(tokens['--openbitfun-color-accent-default']); + expect(tokens['--openbitfun-component-button-content']).toBe(tokens['--openbitfun-color-action-neutral-content']); + } + }); + it('formats hex palette references as stable rgb strings', () => { expect(rgbFromHex('#00e6ff')).toBe('rgb(0, 230, 255)'); expect(rgbaFromHex('#00e6ff', 0.12)).toBe('rgba(0, 230, 255, 0.12)'); diff --git a/src/web-ui/src/infrastructure/appearance/builtins/buildBuiltinAppearance.ts b/src/web-ui/src/infrastructure/appearance/builtins/buildBuiltinAppearance.ts index ec79df1c57..96b2f6886f 100644 --- a/src/web-ui/src/infrastructure/appearance/builtins/buildBuiltinAppearance.ts +++ b/src/web-ui/src/infrastructure/appearance/builtins/buildBuiltinAppearance.ts @@ -1,6 +1,8 @@ import { themeCssVariables, themes, type ThemeTokenName } from '@openbitfun/theme-openbitfun'; import type { AppearancePalette } from './AppearancePalette'; +import { withLegacyButtonTokens } from './buttonThemeCompatibility'; +import { DEFAULT_DARK_APPEARANCE_ID, DEFAULT_LIGHT_APPEARANCE_ID } from './palettes'; import type { AppearanceColorValue, AppearanceDurationValue, @@ -67,11 +69,24 @@ type ThemeValue = string | number | boolean; function themeValuesToCssTokens( values: Readonly>, + palette: AppearancePalette, ): Record { - return Object.fromEntries( + const tokens = Object.fromEntries( (Object.entries(values) as [ThemeTokenName, ThemeValue][]) .map(([name, value]) => [themeCssVariables[name], String(value)]), ) as Record; + if (palette.id === DEFAULT_LIGHT_APPEARANCE_ID || palette.id === DEFAULT_DARK_APPEARANCE_ID) { + return tokens; + } + // Branded presets retain their existing action palette; the default product + // themes consume the component colors published by the design system. + const legacyTokens = Object.fromEntries( + Object.entries(tokens).filter(([name]) => !name.startsWith('--openbitfun-component-button-')), + ); + for (const [name, value] of Object.entries(withLegacyButtonTokens(legacyTokens))) { + if (value !== undefined) tokens[name as AppearanceThemeTokenName] = value; + } + return tokens; } function createThemeTokenValues(palette: AppearancePalette): Record { @@ -226,7 +241,7 @@ function createChromeThemeTokens( 'color.scrollbar.thumbHover': scrollbar.thumbHover, } satisfies Partial>); - return themeValuesToCssTokens(values); + return themeValuesToCssTokens(values, palette); } function createAppearanceOwnedTokens( @@ -247,7 +262,7 @@ function createAppearanceOwnedTokens( const configPageRowHover = configPage?.rowHover ?? (palette.type === 'dark' ? colors.element.base : colors.element.soft); return { - ...themeValuesToCssTokens(createThemeTokenValues(palette)), + ...themeValuesToCssTokens(createThemeTokenValues(palette), palette), '--openbitfun-component-config-page-section-background': configPage?.section.background ?? colors.background.tertiary, '--openbitfun-component-config-page-section-border': configPage?.section.border ?? colors.border.subtle, '--openbitfun-component-config-page-section-border-width': configPage?.section.borderWidth ?? '1px', diff --git a/src/web-ui/src/infrastructure/appearance/builtins/buttonThemeCompatibility.ts b/src/web-ui/src/infrastructure/appearance/builtins/buttonThemeCompatibility.ts new file mode 100644 index 0000000000..b43b5a6a85 --- /dev/null +++ b/src/web-ui/src/infrastructure/appearance/builtins/buttonThemeCompatibility.ts @@ -0,0 +1,33 @@ +import type { AppearanceThemeTokenName } from '../types'; + +// Existing Appearance packages may customize Button through shared action tokens. +// Only inherit explicitly supplied values; a new component override always wins. +const legacyButtonTokens = { + 'content': 'color-action-neutral-content', + 'outline-border': 'color-action-neutral-border', + 'outline-border-interactive': 'color-action-neutral-border', + 'fill-background': 'color-action-neutral-surface', + 'fill-background-hover': 'color-action-neutral-surface-hover', + 'fill-background-pressed': 'color-action-neutral-surface-pressed', + 'primary-background': 'color-action-primary-background', + 'primary-background-hover': 'color-action-primary-hover', + 'primary-background-pressed': 'color-action-primary-pressed', + 'primary-content-disabled': 'color-action-neutral-content-disabled', + 'text-content': 'color-accent-default', + 'text-content-hover': 'color-accent-hover', + 'text-content-disabled': 'color-accent-disabled', +} as const; + +export function withLegacyButtonTokens( + tokens: Partial> | undefined, +): Partial> { + const result = { ...tokens }; + for (const [suffix, legacySuffix] of Object.entries(legacyButtonTokens)) { + const name = `--openbitfun-component-button-${suffix}` as AppearanceThemeTokenName; + const legacyName = `--openbitfun-${legacySuffix}` as AppearanceThemeTokenName; + if (result[name] === undefined && tokens?.[legacyName] !== undefined) { + result[name] = tokens[legacyName]; + } + } + return result; +} diff --git a/src/web-ui/src/infrastructure/appearance/builtins/composeAppearancePackage.test.ts b/src/web-ui/src/infrastructure/appearance/builtins/composeAppearancePackage.test.ts index d51e57a99f..fac463c032 100644 --- a/src/web-ui/src/infrastructure/appearance/builtins/composeAppearancePackage.test.ts +++ b/src/web-ui/src/infrastructure/appearance/builtins/composeAppearancePackage.test.ts @@ -4,6 +4,56 @@ import { APPEARANCE_THEME_TOKEN_NAMES } from './catalog'; import { composeAppearancePackage } from './composeAppearancePackage'; describe('composeAppearancePackage', () => { + it('keeps explicit legacy Button colors through a package round trip without requiring new tokens', () => { + const original: AppearancePackage = { + schema: 'openbitfun.appearance', schemaVersion: 2, + id: 'example.legacy-button-colors', name: 'Legacy button colors', version: '1.0.0', mode: 'light', + renderers: { + 'theme-tokens': { + version: 1, + settings: { + tokens: { + '--openbitfun-color-action-primary-background': '#123456', + '--openbitfun-color-action-neutral-surface': '#eeeeee', + '--openbitfun-color-accent-default': '#007766', + }, + scopes: { chrome: { '--openbitfun-color-action-neutral-content': '#445566' } }, + }, + }, + }, + }; + const payload = JSON.stringify(original); + const resolved = composeAppearancePackage(JSON.parse(payload)); + const settings = resolved.renderers?.['theme-tokens']?.settings; + expect(settings?.tokens).toMatchObject({ + '--openbitfun-color-action-primary-background': '#123456', + '--openbitfun-component-button-primary-background': '#123456', + '--openbitfun-component-button-fill-background': '#eeeeee', + '--openbitfun-component-button-text-content': '#007766', + '--openbitfun-component-button-primary-content-disabled': 'rgba(0, 0, 0, 0.20)', + }); + expect(settings?.scopes?.chrome?.['--openbitfun-component-button-content']).toBe('#445566'); + expect(composeAppearancePackage(JSON.parse(JSON.stringify(resolved))).renderers?.['theme-tokens']).toEqual( + resolved.renderers?.['theme-tokens'], + ); + expect(JSON.stringify(original)).toBe(payload); + }); + + it('prefers explicit Button tokens over legacy aliases in both root and chrome scopes', () => { + const tokens = { + '--openbitfun-color-action-primary-background': '#123456', + '--openbitfun-component-button-primary-background': '#654321', + }; + const resolved = composeAppearancePackage({ + schema: 'openbitfun.appearance', schemaVersion: 2, + id: 'example.button-colors', name: 'Button colors', version: '1.0.0', mode: 'light', + renderers: { 'theme-tokens': { version: 1, settings: { tokens, scopes: { chrome: tokens } } } }, + }); + const settings = resolved.renderers?.['theme-tokens']?.settings; + expect(settings?.tokens['--openbitfun-component-button-primary-background']).toBe('#654321'); + expect(settings?.scopes?.chrome?.['--openbitfun-component-button-primary-background']).toBe('#654321'); + }); + it('resolves a partial imported package into a complete host appearance', () => { const pkg: AppearancePackage = { schema: 'openbitfun.appearance', diff --git a/src/web-ui/src/infrastructure/appearance/builtins/composeAppearancePackage.ts b/src/web-ui/src/infrastructure/appearance/builtins/composeAppearancePackage.ts index 03782c729f..efdbd0eec5 100644 --- a/src/web-ui/src/infrastructure/appearance/builtins/composeAppearancePackage.ts +++ b/src/web-ui/src/infrastructure/appearance/builtins/composeAppearancePackage.ts @@ -12,6 +12,7 @@ import { DEFAULT_LIGHT_APPEARANCE_ID, getBuiltinAppearance, } from './catalog'; +import { withLegacyButtonTokens } from './buttonThemeCompatibility'; const COMPOSITION_LAYERS = Symbol('openbitfun.appearance.composition-layers'); @@ -120,12 +121,12 @@ function mergeRenderers( ...override['theme-tokens'].settings, tokens: { ...(base?.['theme-tokens']?.settings.tokens ?? {}), - ...override['theme-tokens'].settings.tokens, + ...withLegacyButtonTokens(override['theme-tokens'].settings.tokens), }, scopes: { chrome: { ...(base?.['theme-tokens']?.settings.scopes?.chrome ?? {}), - ...(override['theme-tokens'].settings.scopes?.chrome ?? {}), + ...withLegacyButtonTokens(override['theme-tokens'].settings.scopes?.chrome), }, }, }, From fd7ae48444c48b4604fab68d01aebf59cd73333a Mon Sep 17 00:00:00 2001 From: kev1n77 Date: Tue, 8 Sep 2026 18:45:24 +0800 Subject: [PATCH 2/3] fix(web): use intended workspace dialog button variants --- .../src/app/components/NewProjectDialog/NewProjectDialog.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.tsx b/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.tsx index ceafc1a39e..3d7f56a7f6 100644 --- a/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.tsx +++ b/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.tsx @@ -201,10 +201,10 @@ export const NewProjectDialog: React.FC = ({ - - From fe0aea27a7c0d19e376feeae41110e4ad5904c55 Mon Sep 17 00:00:00 2001 From: kev1n77 Date: Tue, 8 Sep 2026 19:05:13 +0800 Subject: [PATCH 3/3] fix(ui): align action button roles across product surfaces Use primary for submit, save, create, and confirm actions, and fill for cancel or dismiss actions. Correct shared confirmations, approval actions, notifications, recommendations, Canvas primary mapping, and Design Lab examples. Keep danger semantics and intentional selection/utility variants. Preserve dimensions, callbacks, disabled/loading conditions, and transport logic. Document variant roles and cover shared confirmation choices, notification actions, settings actions, Canvas mapping, and AskUser output. Validation: design-system:check; check:web; 20 shared action regression tests; 110 focused editor tests. Manual visual and live remote checks remain pending. --- .../src/pages/ComponentDetailPage.tsx | 4 +- .../design-lab/src/pages/ComponentsPage.tsx | 2 +- .../design-lab/src/pages/PatternsPage.tsx | 2 +- .../src/pages/ReferencePatterns.tsx | 4 +- design-system/packages/ui/README.md | 7 +++ .../ConfirmDialog/ConfirmDialog.tsx | 2 +- .../ui/src/flow-chat/ask-user/AskUser.tsx | 2 +- .../packages/ui/tests/ask-user.test.mjs | 1 + .../components/AboutDialog/AboutDialog.tsx | 2 +- .../src/app/components/AppErrorBoundary.tsx | 2 +- .../components/InputDialog/InputDialog.tsx | 4 +- .../MCPInteractionDialog.tsx | 4 +- .../WorkspaceProjectPermissionsDialog.tsx | 4 +- .../WorkspaceRelatedPathsDialog.tsx | 4 +- .../workspaces/WorkspaceSessionBatchModal.tsx | 4 +- .../RemoteConnectDialog/AccountPanel.tsx | 6 +-- .../RemoteConnectDialog.tsx | 16 +++--- .../RemoteConnectDisclaimer.tsx | 4 +- .../RemoteNetworkConnections.tsx | 4 +- .../components/panels/BranchSelectModal.tsx | 4 +- .../panels/DiffFullscreenViewer.tsx | 4 +- .../components/panels/TerminalEditModal.tsx | 4 +- .../review-platform/ReviewPlatformPanel.tsx | 10 ++-- .../scheduled-jobs/DateTimePickerPopover.tsx | 2 +- .../scheduled-jobs/ScheduledJobsView.tsx | 6 +-- .../src/app/scenes/agents/AgentsScene.tsx | 6 +-- .../agents/components/CreateAgentPage.tsx | 10 ++-- .../agents/components/SkillGroupPicker.tsx | 4 +- .../agents/components/ToolGroupPicker.tsx | 4 +- .../EcosystemCompatibilityScene.tsx | 6 +-- src/web-ui/src/app/scenes/git/GitScene.tsx | 4 +- .../src/app/scenes/git/views/BranchesView.tsx | 2 +- .../app/scenes/git/views/WorkingCopyView.tsx | 2 +- .../components/MiniAppDetailModal.tsx | 2 +- .../customization/MiniAppCustomizePanel.tsx | 6 +-- .../MiniAppPermissionDiffDialog.tsx | 4 +- .../miniapps/views/MiniAppMarketView.tsx | 4 +- .../miniapps/views/MiniAppSubmissionsView.tsx | 2 +- .../src/app/scenes/my-agent/InsightsScene.tsx | 4 +- .../src/app/scenes/pages/PagesScene.tsx | 6 +-- .../profile/views/AssistantAvatarPicker.tsx | 2 +- .../profile/views/AssistantCard.test.tsx | 7 +-- .../scenes/profile/views/AssistantCard.tsx | 2 +- .../scenes/profile/views/NurseryGallery.tsx | 4 +- .../components/KeyboardShortcutsTab.tsx | 2 +- src/web-ui/src/app/scenes/shell/ShellNav.tsx | 2 +- .../src/app/scenes/skills/SkillsScene.tsx | 10 ++-- .../skills/components/SkillsSuiteView.tsx | 4 +- .../src/app/scenes/todos/TodosScene.tsx | 2 +- .../scenes/todos/components/TodoEditor.tsx | 4 +- .../dispatch/DispatchInstallDialog.tsx | 6 +-- .../dispatch/DispatchResultDialog.tsx | 4 +- .../market-account/MarketAccountControls.tsx | 4 +- .../relay-deploy/RelayDeployWizard.tsx | 24 ++++----- .../features/ssh-remote/RemoteFileBrowser.tsx | 8 +-- .../ssh-remote/SSHAuthPromptDialog.tsx | 4 +- .../ssh-remote/SSHConnectionDialog.tsx | 4 +- .../components/ChatInputApprovalBand.tsx | 2 +- .../components/DeepReviewConsentDialog.tsx | 4 +- .../flow_chat/components/RichTextInput.tsx | 4 +- .../BackgroundCommandInputDialog.tsx | 4 +- .../BackgroundCommandOutputPanel.tsx | 4 +- .../SmartRecommendations.tsx | 2 +- .../thread-goal/ThreadGoalDialogs.tsx | 10 ++-- .../voice/ComposerVoiceInputButton.tsx | 2 +- .../action-bar/CapacityQueueNotice.tsx | 2 +- .../action-bar/DecisionExecutionGate.tsx | 4 +- .../action-bar/ReviewActionControls.tsx | 10 ++-- .../tool-cards/AcpPermissionActions.tsx | 2 +- .../tool-cards/ComputerUseToolCard.tsx | 2 +- .../tool-cards/CreatePlanDisplay.tsx | 4 +- .../tool-cards/PagePublishToolDisplay.tsx | 2 +- .../tool-cards/ReviewSessionSummaryCard.tsx | 2 +- .../SnapshotFullscreenDiffViewer.tsx | 8 +-- .../config/components/AcpAgentsConfig.tsx | 8 +-- .../components/AppearanceMarketDialog.tsx | 4 +- .../components/AppearanceMarketWorkflows.tsx | 8 +-- .../AppearancePackageConfigSection.tsx | 2 +- .../config/components/ExternalMcpOverview.tsx | 6 +-- .../components/ExternalSourcesConfig.tsx | 26 ++++----- .../GlobalPermissionRulesDialog.tsx | 4 +- .../config/components/HooksConfig.tsx | 4 +- .../components/LocalVoiceModelsConfig.tsx | 6 +-- .../config/components/MCPResourceBrowser.tsx | 2 +- .../config/components/McpToolsConfig.tsx | 10 ++-- .../ModelSettingsDialog.presentation.test.ts | 5 +- .../config/components/ModelSettingsPage.tsx | 16 +++--- .../config/components/QuickActionsConfig.tsx | 4 +- .../components/ReasoningConfigPanel.tsx | 4 +- .../components/RuntimeSettingsPages.tsx | 6 +-- .../config/components/SkillsConfig.tsx | 10 ++-- .../VoiceInputConfig.presentation.test.ts | 2 +- .../config/components/VoiceInputConfig.tsx | 6 +-- .../components/VoiceInputDiagnostics.tsx | 2 +- .../common/ConfigActionBar.test.tsx | 4 ++ .../components/common/ConfigActionBar.tsx | 2 +- .../common/ConfigPageHeader.example.tsx | 4 +- .../form-controls/ConfigActions.tsx | 4 +- .../ConfirmDialogRenderer.test.tsx | 54 +++++++++++++++++++ .../peer-device/PeerDirectoryBrowser.tsx | 4 +- .../infrastructure/providers/CoreProvider.tsx | 2 +- .../update/UpdateAvailableDialog.tsx | 8 +-- .../update/UpdateInstallProgressModal.tsx | 2 +- .../components/AnnouncementToastItem.tsx | 2 +- .../components/FeatureModal.tsx | 6 +-- .../components/NotificationItem.test.tsx | 6 ++- .../components/NotificationItem.tsx | 2 +- .../tools/editor/components/PlanViewer.tsx | 2 +- .../components/InlineAiPreviewBlock.tsx | 4 +- .../meditor/components/TiptapEditor.tsx | 4 +- .../GenerativeWidgetPanel.tsx | 2 +- .../appearancePayload.test.ts | 34 +++++++++--- .../CreateBranchDialog/CreateBranchDialog.tsx | 4 +- .../GitBranchHistoryView.tsx | 2 +- .../components/GitDiffView/GitDiffView.tsx | 4 +- .../components/GitGraphView/GitGraphView.tsx | 2 +- .../GitSettingsView/GitSettingsView.tsx | 4 +- .../runtime/sdk/adapters.test.tsx | 13 ++++- .../runtime/sdk/adapters.tsx | 1 + 119 files changed, 377 insertions(+), 269 deletions(-) create mode 100644 src/web-ui/src/infrastructure/confirm-dialog/ConfirmDialogRenderer.test.tsx diff --git a/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx b/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx index 178bce5e15..6c386c466d 100644 --- a/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx +++ b/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx @@ -552,7 +552,7 @@ export function ComponentDetailPage({ return `import { Icon, Menu, MenuItem, MenuSection, MenuSeparator } from "@openbitfun/ui";\n\n\n \n }>${t("components.preview.menuItemOne")}\n }>${t("components.preview.menuItemTwo")}\n \n \n \n ${t("components.preview.menuDisabledItem")}\n \n`; } if (component.name === "Dialog") { - return `import { Button, Dialog, DialogBody, DialogClose, DialogFooter, DialogHeader, DialogHeading, DialogTitle } from "@openbitfun/ui";\n\n setOpen(false)} open={open} size="xl">\n \n ${t("components.preview.modalTitle")}\n \n \n \n \n \n \n \n`; + return `import { Button, Dialog, DialogBody, DialogClose, DialogFooter, DialogHeader, DialogHeading, DialogTitle } from "@openbitfun/ui";\n\n setOpen(false)} open={open} size="xl">\n \n ${t("components.preview.modalTitle")}\n \n \n \n \n \n \n \n`; } if (component.name === "Sheet") { return `import { Button, DialogBody, DialogClose, DialogFooter, DialogHeader, DialogHeading, DialogTitle, Sheet } from "@openbitfun/ui";\n\n setOpen(false)} open={open} placement="right" size="lg">\n \n ${t("components.preview.modalTitle")}\n \n \n \n \n \n \n \n`; @@ -788,7 +788,7 @@ export function ComponentDetailPage({ {renderDialogConfigurationContent()} - + ); diff --git a/design-system/apps/design-lab/src/pages/PatternsPage.tsx b/design-system/apps/design-lab/src/pages/PatternsPage.tsx index 01f7e0329e..fc8105e470 100644 --- a/design-system/apps/design-lab/src/pages/PatternsPage.tsx +++ b/design-system/apps/design-lab/src/pages/PatternsPage.tsx @@ -172,7 +172,7 @@ export function PatternsPage({ colorScheme, contrast, density, tokenOverrides }: } size="sm" variant="quiet" />} description="macOS · 127.0.0.1" leading={} title="MacBook Pro" /> } tone="success">{t("patterns.device.online")} - + diff --git a/design-system/apps/design-lab/src/pages/ReferencePatterns.tsx b/design-system/apps/design-lab/src/pages/ReferencePatterns.tsx index 1ec1b09fff..d59afb7675 100644 --- a/design-system/apps/design-lab/src/pages/ReferencePatterns.tsx +++ b/design-system/apps/design-lab/src/pages/ReferencePatterns.tsx @@ -41,8 +41,8 @@ export function ProviderConfigurationPattern() { const [revision, setRevision] = useState(0); const [saved, setSaved] = useState(false); const footer = (close: () => void) => - - + + ; return
diff --git a/design-system/packages/ui/README.md b/design-system/packages/ui/README.md index 3e0093c28c..a9870eb718 100644 --- a/design-system/packages/ui/README.md +++ b/design-system/packages/ui/README.md @@ -20,6 +20,13 @@ The package owns component anatomy, behavior, accessibility, and stable variants ## Buttons +Choose variants by action role: use `primary` for the main save, submit, create, +or confirm action, and `fill` for cancel, dismiss, or discard alongside it. +Keep `outline` for ordinary toolbar utilities and secondary choices. A neutral +`fill` button is a low-emphasis surface, not an alias for `primary`. Preserve +`tone="danger"` for destructive actions. Disabled and loading states belong to +the same variant; do not switch a primary action to outline when it is disabled. + Button outline and text variants have transparent resting surfaces. Fill and primary state colors come from the theme's `component.button.*` contract, with variant-specific disabled content. The secondary variant keeps its opaque diff --git a/design-system/packages/ui/src/components/ConfirmDialog/ConfirmDialog.tsx b/design-system/packages/ui/src/components/ConfirmDialog/ConfirmDialog.tsx index 737fcc519c..82fcc430b9 100644 --- a/design-system/packages/ui/src/components/ConfirmDialog/ConfirmDialog.tsx +++ b/design-system/packages/ui/src/components/ConfirmDialog/ConfirmDialog.tsx @@ -206,7 +206,7 @@ export const ConfirmDialog = forwardRef( onClick={() => void runAction("confirm", onConfirm)} ref={confirmButtonRef} tone={confirmDanger || type === "error" ? "danger" : "neutral"} - variant={confirmDanger || type === "error" ? "primary" : "fill"} + variant="primary" > {resolvedConfirmText} diff --git a/design-system/packages/ui/src/flow-chat/ask-user/AskUser.tsx b/design-system/packages/ui/src/flow-chat/ask-user/AskUser.tsx index 75e8886741..289dfe5a16 100644 --- a/design-system/packages/ui/src/flow-chat/ask-user/AskUser.tsx +++ b/design-system/packages/ui/src/flow-chat/ask-user/AskUser.tsx @@ -453,7 +453,7 @@ export const AskUser = forwardRef(function AskUser onClick={onSubmit} size="sm" title={submitTitle} - variant="fill" + variant="primary" > {state === "submitting" && submittingLabel !== undefined ? submittingLabel diff --git a/design-system/packages/ui/tests/ask-user.test.mjs b/design-system/packages/ui/tests/ask-user.test.mjs index da3d217079..2b23c4bde0 100644 --- a/design-system/packages/ui/tests/ask-user.test.mjs +++ b/design-system/packages/ui/tests/ask-user.test.mjs @@ -70,6 +70,7 @@ test("AskUser renders a controlled custom answer with an accessible text field", assert.match(markup, /aria-label="Custom version"/); assert.match(markup, /value="v0.2.17"/); assert.match(markup, /data-openbitfun-part="submit"/); + assert.match(markup, /data-openbitfun-component="button"[^>]+data-openbitfun-variant="primary"/); }); test("AskUser styles use public semantic and component geometry tokens", async () => { diff --git a/src/web-ui/src/app/components/AboutDialog/AboutDialog.tsx b/src/web-ui/src/app/components/AboutDialog/AboutDialog.tsx index 399839b112..870e440dac 100644 --- a/src/web-ui/src/app/components/AboutDialog/AboutDialog.tsx +++ b/src/web-ui/src/app/components/AboutDialog/AboutDialog.tsx @@ -430,7 +430,7 @@ export const AboutDialog: React.FC = ({
- diff --git a/src/web-ui/src/app/components/AppErrorBoundary.tsx b/src/web-ui/src/app/components/AppErrorBoundary.tsx index 9163c337f1..06c0ce7640 100644 --- a/src/web-ui/src/app/components/AppErrorBoundary.tsx +++ b/src/web-ui/src/app/components/AppErrorBoundary.tsx @@ -76,7 +76,7 @@ export class AppErrorBoundary extends Component {

{firstLine}

- diff --git a/src/web-ui/src/app/components/MCPInteractionDialog/MCPInteractionDialog.tsx b/src/web-ui/src/app/components/MCPInteractionDialog/MCPInteractionDialog.tsx index 812f27f471..3cc89814ee 100644 --- a/src/web-ui/src/app/components/MCPInteractionDialog/MCPInteractionDialog.tsx +++ b/src/web-ui/src/app/components/MCPInteractionDialog/MCPInteractionDialog.tsx @@ -190,7 +190,7 @@ export const MCPInteractionDialog: React.FC = () => {
-
-
@@ -1370,7 +1370,7 @@ export const AccountPanel: React.FC = ({
{relayError && (
@@ -1192,7 +1192,7 @@ export const RemoteConnectDialog: React.FC = ({
{renderErrorBlock()}
)} -
@@ -1422,7 +1422,7 @@ export const RemoteConnectDialog: React.FC = ({ {weixinQrSessionKey && !weixinQrImageUrl && weixinAwaitingPhoneConfirm && (
-
@@ -1430,7 +1430,7 @@ export const RemoteConnectDialog: React.FC = ({ {weixinQrSessionKey && !weixinQrImageUrl && !weixinAwaitingPhoneConfirm && !weixinNeedsVerifyCode && (
-
@@ -1455,7 +1455,7 @@ export const RemoteConnectDialog: React.FC = ({ {t('remoteConnect.botWeixinVerifyCodeHint')}

{room ? - : invitation ? - : + : }
diff --git a/src/web-ui/src/app/components/panels/BranchSelectModal.tsx b/src/web-ui/src/app/components/panels/BranchSelectModal.tsx index 8a9f0f0c2b..627ef0be31 100644 --- a/src/web-ui/src/app/components/panels/BranchSelectModal.tsx +++ b/src/web-ui/src/app/components/panels/BranchSelectModal.tsx @@ -320,13 +320,13 @@ export const BranchSelectModal: React.FC = ({ ) : null} - diff --git a/src/web-ui/src/app/components/panels/review-platform/ReviewPlatformPanel.tsx b/src/web-ui/src/app/components/panels/review-platform/ReviewPlatformPanel.tsx index 6036df059a..90ac4a61f4 100644 --- a/src/web-ui/src/app/components/panels/review-platform/ReviewPlatformPanel.tsx +++ b/src/web-ui/src/app/components/panels/review-platform/ReviewPlatformPanel.tsx @@ -1791,7 +1791,7 @@ export const ReviewPlatformPanel: React.FC = ({ {selectedRemote.platform === 'github' ? 'CLI authorization' : 'Required scopes'}: {authChallengeScopes(authChallenge)}
- @@ -2228,7 +2228,7 @@ export const ReviewPlatformPanel: React.FC = ({ - diff --git a/src/web-ui/src/app/components/scheduled-jobs/ScheduledJobsView.tsx b/src/web-ui/src/app/components/scheduled-jobs/ScheduledJobsView.tsx index 018d31eafe..5597dd2cfb 100644 --- a/src/web-ui/src/app/components/scheduled-jobs/ScheduledJobsView.tsx +++ b/src/web-ui/src/app/components/scheduled-jobs/ScheduledJobsView.tsx @@ -603,7 +603,7 @@ const ScheduledJobsView: React.FC = ({
@@ -536,7 +536,7 @@ const CreateAgentPage: React.FC = () => { - - ) : null} - diff --git a/src/web-ui/src/app/scenes/miniapps/views/MiniAppMarketView.tsx b/src/web-ui/src/app/scenes/miniapps/views/MiniAppMarketView.tsx index f3d767bdce..abc34b7e6e 100644 --- a/src/web-ui/src/app/scenes/miniapps/views/MiniAppMarketView.tsx +++ b/src/web-ui/src/app/scenes/miniapps/views/MiniAppMarketView.tsx @@ -458,7 +458,7 @@ const MiniAppMarketView: React.FC = ({ tabs }) => { {installed ? ( ) : ( )} @@ -833,7 +833,7 @@ const PagesScene: React.FC = ({ isActive = true }) => {