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..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/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..a9870eb718 100644 --- a/design-system/packages/ui/README.md +++ b/design-system/packages/ui/README.md @@ -18,8 +18,26 @@ 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 + +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 +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/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/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/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 = () => {
- - diff --git a/src/web-ui/src/app/components/RemoteConnectDialog/AccountPanel.tsx b/src/web-ui/src/app/components/RemoteConnectDialog/AccountPanel.tsx index 28ca1cf9f5..103ca4e223 100644 --- a/src/web-ui/src/app/components/RemoteConnectDialog/AccountPanel.tsx +++ b/src/web-ui/src/app/components/RemoteConnectDialog/AccountPanel.tsx @@ -1144,7 +1144,7 @@ export const AccountPanel: 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 }) => {