From cb38709d1c10727a6944c39da81eae2848020803 Mon Sep 17 00:00:00 2001 From: kev1n77 Date: Wed, 9 Sep 2026 01:15:20 +0800 Subject: [PATCH] fix(ui): align page headers and entry cards with design Distinguish settings and welcome heading typography, use a translucent 3% entry-card surface, and align card padding and text without border-driven height inflation. Remove redundant ConfigPageHeader typography and preview long descriptions with sibling actions in Lab. Preserve legacy Appearance background overrides in root and chrome, including old-payload round trips and explicit new-token precedence. Validation: design-system:check; 19 focused Appearance tests; check:web (all theme audits and type checking); final Lab production build. Native Windows desktop settings and global-search entry cards inspected. Other themes, narrow layouts, and four remote scenarios were not interactively verified. --- .../apps/design-lab/src/i18n/messages.ts | 6 ++-- .../src/pages/ComponentDetailPage.tsx | 4 +-- .../design-lab/src/pages/PatternsPage.tsx | 4 +-- .../design-tokens/src/system.tokens.json | 18 ++++++++++-- .../packages/theme-openbitfun/README.md | 2 ++ .../theme-openbitfun/src/dark.tokens.json | 3 ++ .../src/high-contrast-dark.tokens.json | 3 ++ .../src/high-contrast-light.tokens.json | 3 ++ .../theme-openbitfun/src/light.tokens.json | 3 ++ design-system/packages/ui/README.md | 2 ++ .../components/ActionCard/ActionCard.meta.ts | 6 ++-- .../ActionCard/ActionCard.module.css | 28 +++++++++--------- .../components/PageHeader/PageHeader.meta.ts | 3 +- .../PageHeader/PageHeader.module.css | 25 ++++++++++++---- .../packages/ui/tests/page-header.test.mjs | 2 +- .../packages/ui/tests/text-clipping.test.mjs | 4 +++ .../ui/generated/design-system.css | 18 ++++++++++-- .../desktop/src/generated/bootstrap_theme.css | 1 + .../SettingsContentTypography.test.ts | 12 +++++--- .../src/flow_chat/components/WelcomePanel.css | 29 ------------------- .../src/flow_chat/components/WelcomePanel.tsx | 17 +++++------ .../builtins/actionCardThemeCompatibility.ts | 12 ++++++++ .../builtins/appearancePresetOutput.test.ts | 3 +- .../builtins/buildBuiltinAppearance.ts | 3 ++ .../builtins/composeAppearancePackage.test.ts | 22 ++++++++++++++ .../builtins/composeAppearancePackage.ts | 5 ++-- .../components/common/ConfigPageHeader.scss | 12 -------- .../appearancePayload.test.ts | 6 ++++ 28 files changed, 164 insertions(+), 92 deletions(-) create mode 100644 src/web-ui/src/infrastructure/appearance/builtins/actionCardThemeCompatibility.ts diff --git a/design-system/apps/design-lab/src/i18n/messages.ts b/design-system/apps/design-lab/src/i18n/messages.ts index 22926047c5..fa48d5a4b9 100644 --- a/design-system/apps/design-lab/src/i18n/messages.ts +++ b/design-system/apps/design-lab/src/i18n/messages.ts @@ -279,7 +279,7 @@ export const enUSMessages = { "patterns.menu.keyboard": "Use arrows, Home/End, Enter and Escape; Tab closes the menu.", "components.preview.primary": "Primary", "components.preview.actionCardTitle": "New session", - "components.preview.actionCardDescription": "Start a conversation using the selected assistant", + "components.preview.actionCardDescription": "Start a conversation using the selected assistant and keep your project context, files, and tools together", "components.preview.assistant": "AI Assistant", "components.preview.activityAction": "Run command", "components.preview.activityDescription": "curl -s -o /dev/null -w HTTP status from localhost", @@ -1025,7 +1025,7 @@ export const zhCNMessages = { "patterns.menu.keyboard": "支持方向键、Home/End、Enter 和 Escape,Tab 可关闭菜单。", "components.preview.primary": "主要操作", "components.preview.actionCardTitle": "新建会话", - "components.preview.actionCardDescription": "使用所选助手开始对话", + "components.preview.actionCardDescription": "使用所选助手开始对话,让项目上下文、文件与工具在同一工作区中保持连贯", "components.preview.assistant": "AI 助手", "components.preview.activityAction": "运行命令", "components.preview.activityDescription": "从本地服务读取 HTTP 状态", @@ -1721,7 +1721,7 @@ export const zhTWMessages = { "resources.boundaryDescription": "Design Lab 是創作與檢查介面,公共套件繼續與它的路由、狀態和語言目錄保持獨立。", "components.preview.primary": "主要操作", "components.preview.actionCardTitle": "新增工作階段", - "components.preview.actionCardDescription": "使用所選助理開始對話", + "components.preview.actionCardDescription": "使用所選助理開始對話,讓專案上下文、檔案與工具在同一工作區中保持連貫", "components.preview.assistant": "AI 助手", "components.preview.activityAction": "執行命令", "components.preview.activityDescription": "從本機服務讀取 HTTP 狀態", diff --git a/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx b/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx index 691f49f6a1..a4e3a1d56b 100644 --- a/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx +++ b/design-system/apps/design-lab/src/pages/ComponentDetailPage.tsx @@ -349,13 +349,13 @@ export function ComponentDetailPage({ const [fieldShowControlTrailing, setFieldShowControlTrailing] = useState(false); const [pageHeaderAlign, setPageHeaderAlign] = useState("start"); const [cardContentAlign, setCardContentAlign] = useState("start"); - const [pageHeaderSize, setPageHeaderSize] = useState("lg"); + const [pageHeaderSize, setPageHeaderSize] = useState("md"); const [scrollAreaOrientation, setScrollAreaOrientation] = useState("vertical"); const [activityItemAppearance, setActivityItemAppearance] = useState("surface"); const [activityShowDetail, setActivityShowDetail] = useState(false); const [pageHeaderRequired, setPageHeaderRequired] = useState(false); const [actionItemShowMetadata, setActionItemShowMetadata] = useState(false); - const [actionCardSize, setActionCardSize] = useState("sm"); + const [actionCardSize, setActionCardSize] = useState("md"); const [tabGroupSize, setTabGroupSize] = useState("sm"); const [toolbarSize, setToolbarSize] = useState("sm"); const [previewState, setPreviewState] = useState( diff --git a/design-system/apps/design-lab/src/pages/PatternsPage.tsx b/design-system/apps/design-lab/src/pages/PatternsPage.tsx index 10e5e2d033..38264a4061 100644 --- a/design-system/apps/design-lab/src/pages/PatternsPage.tsx +++ b/design-system/apps/design-lab/src/pages/PatternsPage.tsx @@ -82,7 +82,7 @@ export function PatternsPage({ colorScheme, contrast, density, tokenOverrides }: - + @@ -147,7 +147,7 @@ export function PatternsPage({ colorScheme, contrast, density, tokenOverrides }:
- +

{t("patterns.navigation.status")}

diff --git a/design-system/packages/design-tokens/src/system.tokens.json b/design-system/packages/design-tokens/src/system.tokens.json index cfed08c856..ad30d7bbf3 100644 --- a/design-system/packages/design-tokens/src/system.tokens.json +++ b/design-system/packages/design-tokens/src/system.tokens.json @@ -173,6 +173,13 @@ "$description": "Single-line action leading: 16px at the default 13px type size, scaling with user typography.", "lineHeight": { "$type": "number", "$value": 1.2307692307692308 } }, + "pageIntro": { + "fontFamily": { "$value": "{font.family.control}" }, + "fontSize": { "$value": "{font.size.xl-plus}" }, + "fontWeight": { "$value": "{font.weight.medium}" }, + "lineHeight": { "$value": "{lineHeight.tight}" }, + "letterSpacing": { "$value": "{letterSpacing.normal}" } + }, "fieldHelper": { "$description": "Field helper leading: 16px at the default 11px type size, scaling with user typography.", "lineHeight": { "$type": "number", "$value": 1.4545454545454546 } @@ -226,6 +233,13 @@ } }, "heading": { + "welcome": { + "fontFamily": { "$value": "{font.family.control}" }, + "fontSize": { "$value": "{font.size.5xl}" }, + "fontWeight": { "$value": "{font.weight.semibold}" }, + "lineHeight": { "$value": "{lineHeight.tight}" }, + "letterSpacing": { "$value": "{letterSpacing.normal}" } + }, "navigation": { "fontFamily": { "$value": "{font.family.control}" }, "fontSize": { "$value": "{font.size.xl-plus}" }, @@ -591,14 +605,14 @@ "$type": "dimension", "smMinBlockSize": { "$value": "54px" }, "mdMinBlockSize": { "$value": "62px" }, - "paddingBlock": { "$value": "{space.2}" }, + "paddingBlock": { "$value": "{space.3}" }, "paddingInline": { "$value": "{space.3}" }, "gap": { "$value": "{space.2}" }, "leadingSize": { "$value": "30px" }, "iconSize": { "$value": "16px" }, "contentGap": { "$value": "{space.1}" }, "actionsGap": { "$value": "0px" }, - "actionsPaddingInlineEnd": { "$value": "{space.2}" }, + "actionsPaddingInlineEnd": { "$value": "{space.3}" }, "radius": { "$value": "{radius.base}" } }, "launcherButton": { diff --git a/design-system/packages/theme-openbitfun/README.md b/design-system/packages/theme-openbitfun/README.md index ad49098403..7f7f458db8 100644 --- a/design-system/packages/theme-openbitfun/README.md +++ b/design-system/packages/theme-openbitfun/README.md @@ -77,3 +77,5 @@ appearance's explicit old muted color is retained when the caption token is abse The built-in light Appearance preserves the public neutral action content (80%) in both root and chrome; generic palette projection previously reduced product menu labels to secondary text (60%). Explicit imported action colors still win. + +Action cards own `color.actionCard.background`: the light entry surface is black at 3% opacity. `surface.subtle` is a transient navy tint and `field.groupBackground` belongs to form groups, so neither represents this persistent action surface. Other modes retain their neutral action surface. Product Appearance preserves explicit legacy neutral-surface overrides in root and chrome. diff --git a/design-system/packages/theme-openbitfun/src/dark.tokens.json b/design-system/packages/theme-openbitfun/src/dark.tokens.json index 035d9143ce..52195b1bbc 100644 --- a/design-system/packages/theme-openbitfun/src/dark.tokens.json +++ b/design-system/packages/theme-openbitfun/src/dark.tokens.json @@ -19,6 +19,9 @@ } }, "color": { + "actionCard": { + "background": { "$type": "color", "$value": "{color.action.neutral.surface}" } + }, "surface": { "canvas": { "$type": "color", "$value": "{ref.color.neutral.950}" }, "panel": { "$type": "color", "$value": "{ref.color.neutral.900}" }, 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 68b3d0f84e..40a32423cd 100644 --- a/design-system/packages/theme-openbitfun/src/high-contrast-dark.tokens.json +++ b/design-system/packages/theme-openbitfun/src/high-contrast-dark.tokens.json @@ -19,6 +19,9 @@ } }, "color": { + "actionCard": { + "background": { "$type": "color", "$value": "{color.action.neutral.surface}" } + }, "surface": { "canvas": { "$type": "color", "$value": "{ref.color.gray.1000}" }, "panel": { "$type": "color", "$value": "{ref.color.gray.900}" } 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 2f85b85a3c..a542e03701 100644 --- a/design-system/packages/theme-openbitfun/src/high-contrast-light.tokens.json +++ b/design-system/packages/theme-openbitfun/src/high-contrast-light.tokens.json @@ -19,6 +19,9 @@ } }, "color": { + "actionCard": { + "background": { "$type": "color", "$value": "{color.action.neutral.surface}" } + }, "surface": { "canvas": { "$type": "color", "$value": "{ref.color.gray.0}" }, "panel": { "$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 3ec1cdf62a..7d7ae605e2 100644 --- a/design-system/packages/theme-openbitfun/src/light.tokens.json +++ b/design-system/packages/theme-openbitfun/src/light.tokens.json @@ -19,6 +19,9 @@ } }, "color": { + "actionCard": { + "background": { "$type": "color", "$value": "rgba(0, 0, 0, 0.03)" } + }, "surface": { "canvas": { "$type": "color", "$value": "{ref.color.neutral.50}" }, "panel": { "$type": "color", "$value": "{ref.color.neutral.0}" }, diff --git a/design-system/packages/ui/README.md b/design-system/packages/ui/README.md index ef599ff9a8..259bf1ce46 100644 --- a/design-system/packages/ui/README.md +++ b/design-system/packages/ui/README.md @@ -419,3 +419,5 @@ Compact tabs use `size="sm"` (30px, 14px icons, 4px icon gap); standard tabs ret Dialog titles use 24px bold type with their own 29px line box and normal tracking. `DialogHeader` and `DialogFooter` omit separators by default; pass `separator` for a deliberate divider. A direct `DialogBody` sibling of `DialogFooter appearance="floating"` owns the trailing scroll inset automatically. The floating footer provides the 68px centered action area and a masked blur/gradient using the current theme surface; reduced transparency and forced colors use an opaque fallback. Keep scrollable form content inside `DialogBody` instead of adding a second viewport with independent footer spacing. Extra-large (`xl`) dialogs have an 800px maximum width and continue shrinking within the viewport gutter. Provider editing uses the floating footer; small workspace creation retains its attached footer and existing button/input sizes. The Lab workspace pattern uses local sample paths and callbacks only. + +PageHeader `md` uses the settings title with a primary 15px description; `display` uses the welcome heading and medium 17px introduction with a 12px gap. ActionCard uses 12px padding, section-heading typography (15px semibold), and a primary 13px single-line action description. Its inset outline does not inflate the 62px medium minimum height; longer content keeps the independent sibling actions and OverflowText behavior. diff --git a/design-system/packages/ui/src/components/ActionCard/ActionCard.meta.ts b/design-system/packages/ui/src/components/ActionCard/ActionCard.meta.ts index 3a40eac9fb..2c7292e81a 100644 --- a/design-system/packages/ui/src/components/ActionCard/ActionCard.meta.ts +++ b/design-system/packages/ui/src/components/ActionCard/ActionCard.meta.ts @@ -16,14 +16,13 @@ export const actionCardMeta = { ], states: ["default", "hover", "active", "focus-visible", "selected", "disabled"], tokens: [ - "color.action.neutral.surface", + "color.actionCard.background", "color.action.neutral.surfaceHover", "color.action.neutral.surfacePressed", "color.action.neutral.content", "color.action.neutral.contentDisabled", "color.content.primary", "color.content.secondary", - "color.content.muted", "color.border.default", "color.focus.ring", "control.actionCard.smMinBlockSize", @@ -37,7 +36,8 @@ export const actionCardMeta = { "control.actionCard.actionsGap", "control.actionCard.actionsPaddingInlineEnd", "control.actionCard.radius", - "type.heading.card.fontSize", + "type.heading.section.fontSize", "type.body.sm.fontSize", + "type.actionRow.lineHeight", ], } as const satisfies ComponentMeta; diff --git a/design-system/packages/ui/src/components/ActionCard/ActionCard.module.css b/design-system/packages/ui/src/components/ActionCard/ActionCard.module.css index 3116ae2589..016fd0ea9c 100644 --- a/design-system/packages/ui/src/components/ActionCard/ActionCard.module.css +++ b/design-system/packages/ui/src/components/ActionCard/ActionCard.module.css @@ -14,14 +14,16 @@ align-items: center; inline-size: 100%; min-inline-size: 0; - border: var(--openbitfun-border-width-default) solid transparent; + border: 0; + outline: var(--openbitfun-border-width-default) solid transparent; + outline-offset: calc(-1 * var(--openbitfun-border-width-default)); border-radius: var(--openbitfun-control-action-card-radius); color: var(--openbitfun-color-action-neutral-content); - background: var(--openbitfun-color-action-neutral-surface); + background: var(--openbitfun-color-action-card-background); transition: color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), background-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard), - border-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); + outline-color var(--openbitfun-motion-duration-fast) var(--openbitfun-motion-easing-standard); } .root[data-size="sm"] { @@ -33,7 +35,7 @@ } .root:not([data-disabled="true"]):is(:hover, :has(.trigger[data-openbitfun-preview-state="hover"])) { - border-color: var(--openbitfun-color-border-default); + outline-color: var(--openbitfun-color-border-default); background: var(--openbitfun-color-action-neutral-surface-hover); } @@ -42,7 +44,7 @@ } .root[data-selected="true"] { - border-color: var(--openbitfun-color-border-default); + outline-color: var(--openbitfun-color-border-default); background: var(--openbitfun-color-selection-surface); } @@ -115,19 +117,19 @@ .title { color: var(--openbitfun-color-content-primary); - font-family: var(--openbitfun-type-heading-card-font-family); - font-size: var(--openbitfun-type-heading-card-font-size); - font-weight: var(--openbitfun-type-heading-card-font-weight); - line-height: var(--openbitfun-type-body-sm-line-height); - letter-spacing: var(--openbitfun-type-heading-card-letter-spacing); + font-family: var(--openbitfun-type-heading-section-font-family); + font-size: var(--openbitfun-type-heading-section-font-size); + font-weight: var(--openbitfun-type-heading-section-font-weight); + line-height: var(--openbitfun-type-heading-section-line-height); + letter-spacing: var(--openbitfun-type-heading-section-letter-spacing); } .description { - color: var(--openbitfun-color-content-muted); + color: var(--openbitfun-color-content-primary); font-family: var(--openbitfun-type-body-sm-font-family); font-size: var(--openbitfun-type-body-sm-font-size); font-weight: var(--openbitfun-type-body-sm-font-weight); - line-height: var(--openbitfun-type-body-sm-line-height); + line-height: var(--openbitfun-type-action-row-line-height); letter-spacing: var(--openbitfun-type-body-sm-letter-spacing); } @@ -148,7 +150,7 @@ @media (forced-colors: active) { .root { - border-color: ButtonText; + outline-color: ButtonText; } .root:not([data-disabled="true"]):is(:hover, :active), diff --git a/design-system/packages/ui/src/components/PageHeader/PageHeader.meta.ts b/design-system/packages/ui/src/components/PageHeader/PageHeader.meta.ts index 5f8cd55f52..05d0f2f2ea 100644 --- a/design-system/packages/ui/src/components/PageHeader/PageHeader.meta.ts +++ b/design-system/packages/ui/src/components/PageHeader/PageHeader.meta.ts @@ -23,7 +23,8 @@ export const pageHeaderMeta = { "type.heading.page.fontSize", "type.heading.section.fontSize", "type.heading.display.fontSize", - "type.display.sm.fontSize", + "type.heading.welcome.fontSize", + "type.pageIntro.fontSize", "type.body.lg.fontSize", "type.body.sm.fontSize", ], diff --git a/design-system/packages/ui/src/components/PageHeader/PageHeader.module.css b/design-system/packages/ui/src/components/PageHeader/PageHeader.module.css index 9844b9efd8..d7a16a48a1 100644 --- a/design-system/packages/ui/src/components/PageHeader/PageHeader.module.css +++ b/design-system/packages/ui/src/components/PageHeader/PageHeader.module.css @@ -60,11 +60,11 @@ } .root[data-size="display"] .heading { - font-family: var(--openbitfun-type-display-sm-font-family); - font-size: var(--openbitfun-type-display-sm-font-size); - font-weight: var(--openbitfun-type-display-sm-font-weight); - line-height: var(--openbitfun-type-display-sm-line-height); - letter-spacing: var(--openbitfun-type-display-sm-letter-spacing); + font-family: var(--openbitfun-type-heading-welcome-font-family); + font-size: var(--openbitfun-type-heading-welcome-font-size); + font-weight: var(--openbitfun-type-heading-welcome-font-weight); + line-height: var(--openbitfun-type-heading-welcome-line-height); + letter-spacing: var(--openbitfun-type-heading-welcome-letter-spacing); } .required { @@ -73,7 +73,7 @@ } .description { - color: var(--openbitfun-color-content-muted); + color: var(--openbitfun-color-content-primary); font-family: var(--openbitfun-type-body-lg-font-family); font-size: var(--openbitfun-type-body-lg-font-size); font-weight: var(--openbitfun-type-body-lg-font-weight); @@ -82,6 +82,7 @@ } .root[data-size="sm"] .description { + color: var(--openbitfun-color-content-muted); font-family: var(--openbitfun-type-body-sm-font-family); font-size: var(--openbitfun-type-body-sm-font-size); font-weight: var(--openbitfun-type-body-sm-font-weight); @@ -89,6 +90,18 @@ letter-spacing: var(--openbitfun-type-body-sm-letter-spacing); } + .root[data-size="display"] .content { + gap: var(--openbitfun-space-3); + } + + .root[data-size="display"] .description { + font-family: var(--openbitfun-type-page-intro-font-family); + font-size: var(--openbitfun-type-page-intro-font-size); + font-weight: var(--openbitfun-type-page-intro-font-weight); + line-height: var(--openbitfun-type-page-intro-line-height); + letter-spacing: var(--openbitfun-type-page-intro-letter-spacing); + } + .action { display: inline-flex; flex: 0 0 auto; diff --git a/design-system/packages/ui/tests/page-header.test.mjs b/design-system/packages/ui/tests/page-header.test.mjs index 1b11f40cea..e668c2a6f4 100644 --- a/design-system/packages/ui/tests/page-header.test.mjs +++ b/design-system/packages/ui/tests/page-header.test.mjs @@ -69,7 +69,7 @@ test("PageHeader styles use shared typography and content tokens", async () => { assert.match(styles, /--openbitfun-type-heading-page-font-size/); assert.match(styles, /--openbitfun-type-heading-section-font-size/); assert.match(styles, /--openbitfun-type-heading-display-font-size/); - assert.match(styles, /--openbitfun-type-display-sm-font-size/); + assert.match(styles, /--openbitfun-type-heading-welcome-font-size/); assert.match(styles, /--openbitfun-type-body-lg-font-size/); assert.match(styles, /--openbitfun-color-content-primary/); assert.match(styles, /--openbitfun-color-content-muted/); diff --git a/design-system/packages/ui/tests/text-clipping.test.mjs b/design-system/packages/ui/tests/text-clipping.test.mjs index 1816c41044..4d75a5380c 100644 --- a/design-system/packages/ui/tests/text-clipping.test.mjs +++ b/design-system/packages/ui/tests/text-clipping.test.mjs @@ -33,6 +33,10 @@ for (const [component, selector] of [ declarations["line-height"], component === "ActionItem" ? /^var\(--openbitfun-type-action-row-line-height\)$/ + : component === "ActionCard" + ? selector === ".title" + ? /^var\(--openbitfun-type-heading-section-line-height\)$/ + : /^var\(--openbitfun-type-action-row-line-height\)$/ : component === "Dialog" ? /^var\(--openbitfun-type-heading-dialog-line-height\)$/ : /^var\(--openbitfun-(?:line-height-base|type-body-sm-line-height|type-label-(?:xs|md)-line-height)\)$/, diff --git a/src/apps/data-migrator/ui/generated/design-system.css b/src/apps/data-migrator/ui/generated/design-system.css index f54a291fd0..56ba65ca00 100644 --- a/src/apps/data-migrator/ui/generated/design-system.css +++ b/src/apps/data-migrator/ui/generated/design-system.css @@ -6,13 +6,13 @@ --openbitfun-border-width-default: 1px; --openbitfun-border-width-strong: 2px; --openbitfun-control-action-card-actions-gap: 0px; - --openbitfun-control-action-card-actions-padding-inline-end: var(--openbitfun-space-2); + --openbitfun-control-action-card-actions-padding-inline-end: var(--openbitfun-space-3); --openbitfun-control-action-card-content-gap: var(--openbitfun-space-1); --openbitfun-control-action-card-gap: var(--openbitfun-space-2); --openbitfun-control-action-card-icon-size: 16px; --openbitfun-control-action-card-leading-size: 30px; --openbitfun-control-action-card-md-min-block-size: 62px; - --openbitfun-control-action-card-padding-block: var(--openbitfun-space-2); + --openbitfun-control-action-card-padding-block: var(--openbitfun-space-3); --openbitfun-control-action-card-padding-inline: var(--openbitfun-space-3); --openbitfun-control-action-card-radius: var(--openbitfun-radius-base); --openbitfun-control-action-card-sm-min-block-size: 54px; @@ -554,6 +554,11 @@ --openbitfun-type-heading-section-font-weight: var(--openbitfun-font-weight-semibold); --openbitfun-type-heading-section-letter-spacing: var(--openbitfun-letter-spacing-normal); --openbitfun-type-heading-section-line-height: var(--openbitfun-line-height-tight); + --openbitfun-type-heading-welcome-font-family: var(--openbitfun-font-family-control); + --openbitfun-type-heading-welcome-font-size: var(--openbitfun-font-size-5xl); + --openbitfun-type-heading-welcome-font-weight: var(--openbitfun-font-weight-semibold); + --openbitfun-type-heading-welcome-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-heading-welcome-line-height: var(--openbitfun-line-height-tight); --openbitfun-type-label-lg-font-family: var(--openbitfun-font-family-control); --openbitfun-type-label-lg-font-size: var(--openbitfun-font-size-base); --openbitfun-type-label-lg-font-weight: var(--openbitfun-font-weight-medium); @@ -619,6 +624,11 @@ --openbitfun-type-overline-xs-font-weight: var(--openbitfun-font-weight-semibold); --openbitfun-type-overline-xs-letter-spacing: var(--openbitfun-letter-spacing-widest); --openbitfun-type-overline-xs-line-height: var(--openbitfun-line-height-none); + --openbitfun-type-page-intro-font-family: var(--openbitfun-font-family-control); + --openbitfun-type-page-intro-font-size: var(--openbitfun-font-size-xl-plus); + --openbitfun-type-page-intro-font-weight: var(--openbitfun-font-weight-medium); + --openbitfun-type-page-intro-letter-spacing: var(--openbitfun-letter-spacing-normal); + --openbitfun-type-page-intro-line-height: var(--openbitfun-line-height-tight); --openbitfun-type-support-font-family: var(--openbitfun-font-family-control); --openbitfun-type-support-font-size: var(--openbitfun-font-size-meta); --openbitfun-type-support-font-weight: var(--openbitfun-font-weight-regular); @@ -703,6 +713,7 @@ --openbitfun-color-action-secondary-content: rgba(0, 0, 0, 0.80); --openbitfun-color-action-secondary-hover: rgba(16, 26, 39, 0.09); --openbitfun-color-action-secondary-pressed: rgba(16, 26, 39, 0.13); + --openbitfun-color-action-card-background: rgba(0, 0, 0, 0.03); --openbitfun-color-border-default: rgba(16, 26, 39, 0.15); --openbitfun-color-border-strong: rgba(16, 26, 39, 0.34); --openbitfun-color-border-subtle: rgba(16, 26, 39, 0.08); @@ -852,6 +863,7 @@ --openbitfun-color-action-secondary-content: #e8e8e8; --openbitfun-color-action-secondary-hover: rgba(255, 255, 255, 0.1); --openbitfun-color-action-secondary-pressed: rgba(255, 255, 255, 0.12); + --openbitfun-color-action-card-background: rgba(255, 255, 255, 0.1); --openbitfun-color-border-default: rgba(255, 255, 255, 0.18); --openbitfun-color-border-strong: rgba(255, 255, 255, 0.3); --openbitfun-color-border-subtle: rgba(255, 255, 255, 0.12); @@ -981,6 +993,7 @@ --openbitfun-color-action-primary-hover: #262626; --openbitfun-color-action-quiet-content: #161616; --openbitfun-color-action-secondary-content: #000000; + --openbitfun-color-action-card-background: #e6e6e6; --openbitfun-color-border-default: #3b3b3b; --openbitfun-color-border-strong: #000000; --openbitfun-color-border-subtle: #707070; @@ -1044,6 +1057,7 @@ --openbitfun-color-action-primary-content: #000000; --openbitfun-color-action-primary-hover: #e6e6e6; --openbitfun-color-action-primary-pressed: #ffffff; + --openbitfun-color-action-card-background: #343434; --openbitfun-color-border-default: #b5b5b5; --openbitfun-color-border-strong: #ffffff; --openbitfun-color-border-subtle: #8a8a8a; diff --git a/src/apps/desktop/src/generated/bootstrap_theme.css b/src/apps/desktop/src/generated/bootstrap_theme.css index 0d82aa44e7..829478a4f1 100644 --- a/src/apps/desktop/src/generated/bootstrap_theme.css +++ b/src/apps/desktop/src/generated/bootstrap_theme.css @@ -32,6 +32,7 @@ --openbitfun-color-action-secondary-content: #e8e8e8; --openbitfun-color-action-secondary-hover: rgba(255, 255, 255, 0.1); --openbitfun-color-action-secondary-pressed: rgba(255, 255, 255, 0.12); + --openbitfun-color-action-card-background: rgba(255, 255, 255, 0.1); --openbitfun-color-border-default: rgba(255, 255, 255, 0.18); --openbitfun-color-border-strong: rgba(255, 255, 255, 0.3); --openbitfun-color-border-subtle: rgba(255, 255, 255, 0.12); diff --git a/src/web-ui/src/app/scenes/settings/SettingsContentTypography.test.ts b/src/web-ui/src/app/scenes/settings/SettingsContentTypography.test.ts index cc67826f3d..7fd79a089d 100644 --- a/src/web-ui/src/app/scenes/settings/SettingsContentTypography.test.ts +++ b/src/web-ui/src/app/scenes/settings/SettingsContentTypography.test.ts @@ -8,6 +8,7 @@ function readSource(relativePath: string): string { const headerComponent = readSource('../../../infrastructure/config/components/common/ConfigPageHeader.tsx'); const headerStyles = readSource('../../../infrastructure/config/components/common/ConfigPageHeader.scss'); +const pageHeaderStyles = readSource('../../../../../../design-system/packages/ui/src/components/PageHeader/PageHeader.module.css'); const layoutStyles = readSource('../../../infrastructure/config/components/common/ConfigPageLayout.scss'); const formStyles = readSource('../../../infrastructure/config/components/ConfigForm.scss'); const appearanceStyles = readSource('../../../infrastructure/config/components/AppearanceSettingsPage.scss'); @@ -15,12 +16,15 @@ const shortcutStyles = readSource('./components/KeyboardShortcutsTab.scss'); describe('Settings content typography', () => { it('maps the shared settings hierarchy to canonical semantic type roles', () => { + expect(headerComponent).toContain("import { PageHeader } from '@openbitfun/ui'"); + expect(headerComponent).toContain(' = ({
{/* Greeting */}
-
-
-

- {greeting.title},{t(aiPartnerKey)}{isClawSession && assistantName ? `,${assistantName}` : ''} -

-

{tagline}

-
-
+ + {greeting.title},{t(aiPartnerKey)}{isClawSession && assistantName ? `,${assistantName}` : ''} + } + description={{tagline}} + />
diff --git a/src/web-ui/src/infrastructure/appearance/builtins/actionCardThemeCompatibility.ts b/src/web-ui/src/infrastructure/appearance/builtins/actionCardThemeCompatibility.ts new file mode 100644 index 0000000000..0193a9b384 --- /dev/null +++ b/src/web-ui/src/infrastructure/appearance/builtins/actionCardThemeCompatibility.ts @@ -0,0 +1,12 @@ +import type { AppearanceThemeTokenName } from '../types'; + +export function withLegacyActionCardToken( + tokens: Partial> | undefined, +): Partial> { + const result = { ...tokens }; + if (result['--openbitfun-color-action-card-background'] === undefined + && tokens?.['--openbitfun-color-action-neutral-surface'] !== undefined) { + result['--openbitfun-color-action-card-background'] = tokens['--openbitfun-color-action-neutral-surface']; + } + return result; +} 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 bb30bf8cf4..9e0a070ad4 100644 --- a/src/web-ui/src/infrastructure/appearance/builtins/appearancePresetOutput.test.ts +++ b/src/web-ui/src/infrastructure/appearance/builtins/appearancePresetOutput.test.ts @@ -49,7 +49,7 @@ describe('builtin appearance preset output', () => { const settings = getBuiltinAppearance('openbitfun-light')?.renderers?.['theme-tokens']?.settings; for (const tokens of [settings?.tokens, settings?.scopes?.chrome].filter(Boolean)) { for (const [name, value] of Object.entries(themes.light)) { - if (name.startsWith('color.field.')) expect(tokens?.[themeCssVariables[name as ThemeTokenName]]).toBe(value); + if (name.startsWith('color.field.') || name === 'color.actionCard.background') expect(tokens?.[themeCssVariables[name as ThemeTokenName]]).toBe(value); } } expect(settings?.tokens['--openbitfun-color-field-border']).toBe('rgba(0, 0, 0, 0.08)'); @@ -61,6 +61,7 @@ describe('builtin appearance preset output', () => { for (const palette of builtinAppearancePalettes) { if (palette.id === 'openbitfun-light') continue; const tokens = getBuiltinAppearanceThemeTokens(palette.id); + expect(tokens['--openbitfun-color-action-card-background']).toBe(palette.colors.element.base); expect(tokens['--openbitfun-color-field-border']).toBe(palette.colors.border.base); expect(tokens['--openbitfun-color-field-border-focus']).toBe(palette.colors.accent[500]); expect(tokens['--openbitfun-color-field-border-active']).toBe(palette.colors.accent[500]); diff --git a/src/web-ui/src/infrastructure/appearance/builtins/buildBuiltinAppearance.ts b/src/web-ui/src/infrastructure/appearance/builtins/buildBuiltinAppearance.ts index 203fe79c85..837a3a8ce0 100644 --- a/src/web-ui/src/infrastructure/appearance/builtins/buildBuiltinAppearance.ts +++ b/src/web-ui/src/infrastructure/appearance/builtins/buildBuiltinAppearance.ts @@ -80,6 +80,7 @@ function themeValuesToCssTokens( // Neutral action labels are primary text in the public theme. The generic // palette's secondary text projection used to make product menus too faint. tokens['--openbitfun-color-action-neutral-content'] = String(themes.light['color.action.neutral.content']); + tokens['--openbitfun-color-action-card-background'] = String(themes.light['color.actionCard.background']); tokens['--openbitfun-color-content-caption'] = String(themes.light['color.content.caption']); // Default light fields use the published neutral states in both root and // chrome. Branded palettes and imported overrides retain their own colors. @@ -147,6 +148,7 @@ function createThemeTokenValues(palette: AppearancePalette): Record { + it('preserves legacy action-card backgrounds in root and chrome across old-payload round trips', () => { + const original: AppearancePackage = { + schema: 'openbitfun.appearance', schemaVersion: 2, + id: 'example.legacy-cards', name: 'Legacy cards', version: '1.0.0', mode: 'light', + renderers: { 'theme-tokens': { version: 1, settings: { + tokens: { '--openbitfun-color-action-neutral-surface': '#123456' }, + scopes: { chrome: { '--openbitfun-color-action-neutral-surface': '#654321' } }, + } } }, + }; + const payload = JSON.stringify(original); + const resolved = composeAppearancePackage(JSON.parse(payload)); + const settings = resolved.renderers!['theme-tokens']!.settings; + expect(settings.tokens['--openbitfun-color-action-card-background']).toBe('#123456'); + expect(settings.scopes?.chrome?.['--openbitfun-color-action-card-background']).toBe('#654321'); + expect(composeAppearancePackage(JSON.parse(JSON.stringify(resolved))).renderers?.['theme-tokens']).toEqual(resolved.renderers?.['theme-tokens']); + expect(JSON.stringify(original)).toBe(payload); + original.renderers!['theme-tokens']!.settings.tokens['--openbitfun-color-action-card-background'] = '#112233'; + original.renderers!['theme-tokens']!.settings.scopes!.chrome!['--openbitfun-color-action-card-background'] = '#334455'; + const explicit = composeAppearancePackage(original).renderers!['theme-tokens']!.settings; + expect(explicit.tokens['--openbitfun-color-action-card-background']).toBe('#112233'); + expect(explicit.scopes?.chrome?.['--openbitfun-color-action-card-background']).toBe('#334455'); + }); it('preserves legacy field colors and explicit hint overrides across old-payload round trips', () => { const original: AppearancePackage = { schema: 'openbitfun.appearance', schemaVersion: 2, diff --git a/src/web-ui/src/infrastructure/appearance/builtins/composeAppearancePackage.ts b/src/web-ui/src/infrastructure/appearance/builtins/composeAppearancePackage.ts index 0e41d7d8aa..c961c578e8 100644 --- a/src/web-ui/src/infrastructure/appearance/builtins/composeAppearancePackage.ts +++ b/src/web-ui/src/infrastructure/appearance/builtins/composeAppearancePackage.ts @@ -15,6 +15,7 @@ import { import { withLegacyButtonTokens } from './buttonThemeCompatibility'; import { withLegacyFieldTokens } from './fieldThemeCompatibility'; import { withLegacyCaptionToken } from './captionThemeCompatibility'; +import { withLegacyActionCardToken } from './actionCardThemeCompatibility'; const COMPOSITION_LAYERS = Symbol('openbitfun.appearance.composition-layers'); @@ -123,12 +124,12 @@ function mergeRenderers( ...override['theme-tokens'].settings, tokens: { ...(base?.['theme-tokens']?.settings.tokens ?? {}), - ...withLegacyCaptionToken(withLegacyFieldTokens(withLegacyButtonTokens(override['theme-tokens'].settings.tokens))), + ...withLegacyActionCardToken(withLegacyCaptionToken(withLegacyFieldTokens(withLegacyButtonTokens(override['theme-tokens'].settings.tokens)))), }, scopes: { chrome: { ...(base?.['theme-tokens']?.settings.scopes?.chrome ?? {}), - ...withLegacyCaptionToken(withLegacyFieldTokens(withLegacyButtonTokens(override['theme-tokens'].settings.scopes?.chrome))), + ...withLegacyActionCardToken(withLegacyCaptionToken(withLegacyFieldTokens(withLegacyButtonTokens(override['theme-tokens'].settings.scopes?.chrome)))), }, }, }, diff --git a/src/web-ui/src/infrastructure/config/components/common/ConfigPageHeader.scss b/src/web-ui/src/infrastructure/config/components/common/ConfigPageHeader.scss index f53112c414..a7bad6f186 100644 --- a/src/web-ui/src/infrastructure/config/components/common/ConfigPageHeader.scss +++ b/src/web-ui/src/infrastructure/config/components/common/ConfigPageHeader.scss @@ -33,23 +33,12 @@ } .openbitfun-config-page-header__title { - color: var(--openbitfun-color-content-primary); - font-size: var(--openbitfun-type-heading-page-font-size); - font-weight: var(--openbitfun-type-heading-page-font-weight); - line-height: var(--openbitfun-type-heading-page-line-height); - letter-spacing: var(--openbitfun-type-heading-page-letter-spacing); overflow-wrap: break-word; } .openbitfun-config-page-header__subtitle { display: block; margin: 0; - font-size: var(--openbitfun-type-body-lg-font-size); - font-weight: var(--openbitfun-type-body-lg-font-weight); - color: var(--openbitfun-color-content-primary); - line-height: var(--openbitfun-type-body-lg-line-height); - letter-spacing: var(--openbitfun-type-body-lg-letter-spacing); - font-family:var(--openbitfun-type-body-sm-font-family); max-width: 100%; overflow-wrap: break-word; } @@ -127,7 +116,6 @@ } .openbitfun-config-page-header__subtitle { - margin-top: var(--openbitfun-space-1); max-width: 100%; } diff --git a/src/web-ui/src/tools/generative-widget/appearancePayload.test.ts b/src/web-ui/src/tools/generative-widget/appearancePayload.test.ts index 41b446be9b..e0f24ff45d 100644 --- a/src/web-ui/src/tools/generative-widget/appearancePayload.test.ts +++ b/src/web-ui/src/tools/generative-widget/appearancePayload.test.ts @@ -46,6 +46,9 @@ const FIELD_GROUP_THEME_VARIABLE_NAME = '--openbitfun-color-field-group-backgrou // Menu and navigation captions own their final contrast independently of body // descriptions and field hints; assert this addition without changing the shared baseline. const CAPTION_THEME_VARIABLE_NAME = '--openbitfun-color-content-caption'; +// Persistent action cards own their fill independently of transient feedback +// and form groups; keep the shared contract fingerprint unchanged. +const ACTION_CARD_THEME_VARIABLE_NAME = '--openbitfun-color-action-card-background'; const RETIRED_WIDGET_VARIABLE_NAMES = [ '--background-primary', '--bg-primary', @@ -95,11 +98,13 @@ describe('generated widget appearance payload contract', () => { && !FIELD_STATE_THEME_VARIABLE_NAMES.some(fieldName => fieldName === name) && name !== FIELD_GROUP_THEME_VARIABLE_NAME && name !== CAPTION_THEME_VARIABLE_NAME + && name !== ACTION_CARD_THEME_VARIABLE_NAME )); expect(buttonNames).toEqual(BUTTON_THEME_VARIABLE_NAMES); expect(fieldStateNames).toEqual(FIELD_STATE_THEME_VARIABLE_NAMES); expect(WIDGET_APPEARANCE_VAR_NAMES).toContain(FIELD_GROUP_THEME_VARIABLE_NAME); expect(WIDGET_APPEARANCE_VAR_NAMES).toContain(CAPTION_THEME_VARIABLE_NAME); + expect(WIDGET_APPEARANCE_VAR_NAMES).toContain(ACTION_CARD_THEME_VARIABLE_NAME); expect({ count: sharedNames.length, hash: hashNames(sharedNames), @@ -156,6 +161,7 @@ describe('generated widget appearance payload contract', () => { '--openbitfun-color-action-primary-content': '#101010', '--openbitfun-color-action-primary-hover': 'linear-gradient(test-hover)', '--openbitfun-color-action-primary-pressed': '#202020', + '--openbitfun-color-action-card-background': 'rgba(0, 0, 0, 0.07)', '--openbitfun-color-content-caption': 'rgba(0, 0, 0, 0.45)', '--openbitfun-component-button-primary-background': '#303030', '--openbitfun-component-button-fill-background': 'rgba(0, 0, 0, 0.08)',