From 183dc0bf1f61ee85c5960b59037ab77d2c3f7020 Mon Sep 17 00:00:00 2001 From: kev1n77 Date: Wed, 9 Sep 2026 00:12:16 +0800 Subject: [PATCH] fix(ui): align form labels and grouped surfaces --- .../design-lab/src/pages/PatternsPage.tsx | 4 +++- .../src/pages/ReferencePatterns.tsx | 21 ++++++++++++++++++ .../design-tokens/src/system.tokens.json | 8 +++++++ .../packages/theme-openbitfun/README.md | 3 ++- .../theme-openbitfun/src/dark.tokens.json | 1 + .../src/high-contrast-dark.tokens.json | 1 + .../src/high-contrast-light.tokens.json | 1 + .../theme-openbitfun/src/light.tokens.json | 1 + design-system/packages/ui/README.md | 8 +++++++ .../ui/src/components/Field/Field.meta.ts | 6 +++-- .../ui/src/components/Field/Field.module.css | 22 +++++++++++++------ .../components/FieldGroup/FieldGroup.meta.ts | 4 ++-- .../FieldGroup/FieldGroup.module.css | 6 ++--- .../packages/ui/tests/field-group.test.mjs | 2 +- .../packages/ui/tests/field.test.mjs | 4 ++-- .../ui/generated/design-system.css | 6 +++++ .../desktop/src/generated/bootstrap_theme.css | 1 + .../NewProjectDialog/NewProjectDialog.scss | 10 ++++----- .../builtins/appearancePresetOutput.test.ts | 2 ++ .../builtins/buildBuiltinAppearance.ts | 2 ++ .../builtins/composeAppearancePackage.test.ts | 9 +++++++- .../builtins/fieldThemeCompatibility.ts | 4 ++++ .../config/components/ConfigForm.scss | 4 ++-- .../appearancePayload.test.ts | 6 +++++ 24 files changed, 109 insertions(+), 27 deletions(-) diff --git a/design-system/apps/design-lab/src/pages/PatternsPage.tsx b/design-system/apps/design-lab/src/pages/PatternsPage.tsx index fc8105e470..3c9c1a14e8 100644 --- a/design-system/apps/design-lab/src/pages/PatternsPage.tsx +++ b/design-system/apps/design-lab/src/pages/PatternsPage.tsx @@ -35,7 +35,7 @@ import { type TokenOverrides, } from "@openbitfun/ui"; import { useI18n, type MessageKey } from "../i18n"; -import { NestedMenuPattern, ProviderConfigurationPattern, SceneToolbarPattern } from "./ReferencePatterns"; +import { FormTypographyPattern, NestedMenuPattern, ProviderConfigurationPattern, SceneToolbarPattern } from "./ReferencePatterns"; interface PatternsPageProps { colorScheme: ColorScheme; @@ -116,6 +116,8 @@ export function PatternsPage({ colorScheme, contrast, density, tokenOverrides }: + +
+ + + + + + + + + + + + + + + ; +} + export function ProviderConfigurationPattern() { const { t } = useI18n(); const [open, setOpen] = useState(false); diff --git a/design-system/packages/design-tokens/src/system.tokens.json b/design-system/packages/design-tokens/src/system.tokens.json index bdf6db6ca5..20c9233835 100644 --- a/design-system/packages/design-tokens/src/system.tokens.json +++ b/design-system/packages/design-tokens/src/system.tokens.json @@ -169,6 +169,14 @@ "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 } + }, + "formDescription": { + "$description": "Form-section description leading: 16px at the default 13px type size, scaling with user typography.", + "lineHeight": { "$type": "number", "$value": 1.2307692307692308 } + }, "support": { "fontFamily": { "$value": "{font.family.control}" }, "fontSize": { "$value": "{font.size.meta}" }, diff --git a/design-system/packages/theme-openbitfun/README.md b/design-system/packages/theme-openbitfun/README.md index 29f6a9d2a6..4afc95dbd8 100644 --- a/design-system/packages/theme-openbitfun/README.md +++ b/design-system/packages/theme-openbitfun/README.md @@ -62,7 +62,8 @@ 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. +- `color.surface.tertiary` is an opaque low-emphasis fill for persistent containers such as cards. +- `color.field.groupBackground` owns grouped form surfaces: light mode uses a 3% black tint so the underlying surface remains visible; dark and high-contrast modes retain their tertiary fill. Opaque tertiary containers and transient `surface.subtle` feedback cannot express this form-specific contract. Imported appearances inherit an explicitly supplied legacy tertiary color unless they supply the new field token. - `color.surface.subtle` is a translucent local tint for transient feedback and small inset details. It must not define a persistent application plane. - `color.selection.surface` owns persistent neutral selection. Hover and pressed colors remain action feedback and are not substitutes for selection. - `color.codeChange.added` (`#1aa73e`) and `color.codeChange.removed` (`#ec221f`) also anchor success and danger emphasis. Warning emphasis uses `#ff8c00`; information uses the existing creative-action blue (`#2e7eff`). These clear hues share light tints instead of separate per-component palettes. diff --git a/design-system/packages/theme-openbitfun/src/dark.tokens.json b/design-system/packages/theme-openbitfun/src/dark.tokens.json index 153120ddd6..ec8d286684 100644 --- a/design-system/packages/theme-openbitfun/src/dark.tokens.json +++ b/design-system/packages/theme-openbitfun/src/dark.tokens.json @@ -175,6 +175,7 @@ } }, "field": { + "groupBackground": { "$description": "Grouped form tint; independent of opaque tertiary containers and transient subtle feedback.", "$type": "color", "$value": "{color.surface.tertiary}" }, "borderActive": { "$type": "color", "$value": "{color.field.borderFocus}" }, "placeholder": { "$type": "color", "$value": "{color.content.muted}" }, "background": { "$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 5561e02ac7..30aa15e486 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 @@ -51,6 +51,7 @@ } }, "field": { + "groupBackground": { "$description": "Grouped form tint; independent of opaque tertiary containers and transient subtle feedback.", "$type": "color", "$value": "{color.surface.tertiary}" }, "borderActive": { "$type": "color", "$value": "{color.field.borderFocus}" }, "placeholder": { "$type": "color", "$value": "{color.content.muted}" }, "border": { "$type": "color", "$value": "{ref.color.gray.250}" }, 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 cc823429e7..0a66e1552f 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 @@ -51,6 +51,7 @@ } }, "field": { + "groupBackground": { "$description": "Grouped form tint; independent of opaque tertiary containers and transient subtle feedback.", "$type": "color", "$value": "{color.surface.tertiary}" }, "borderActive": { "$type": "color", "$value": "{color.field.borderFocus}" }, "placeholder": { "$type": "color", "$value": "{color.content.muted}" }, "border": { "$type": "color", "$value": "{ref.color.gray.650}" }, diff --git a/design-system/packages/theme-openbitfun/src/light.tokens.json b/design-system/packages/theme-openbitfun/src/light.tokens.json index 62f525e3fc..1578ff5c16 100644 --- a/design-system/packages/theme-openbitfun/src/light.tokens.json +++ b/design-system/packages/theme-openbitfun/src/light.tokens.json @@ -175,6 +175,7 @@ } }, "field": { + "groupBackground": { "$description": "Grouped form tint; independent of opaque tertiary containers and transient subtle feedback.", "$type": "color", "$value": "rgba(0, 0, 0, 0.03)" }, "borderActive": { "$description": "Input and SearchField editing border, distinct from the stronger generic focus indicator used by other controls.", "$type": "color", "$value": "{color.field.borderHover}" }, "placeholder": { "$description": "Empty field hints and decorative adornments; distinct from general secondary prose.", "$type": "color", "$value": "rgba(0, 0, 0, 0.40)" }, "background": { "$type": "color", "$value": "{ref.color.neutral.0}" }, diff --git a/design-system/packages/ui/README.md b/design-system/packages/ui/README.md index 5885a05f3b..3ef306285d 100644 --- a/design-system/packages/ui/README.md +++ b/design-system/packages/ui/README.md @@ -397,3 +397,11 @@ Tool-specific data shaping, localization, host actions, stores, and heavy renderers remain in the consuming product and enter through semantic props, callbacks, and slots. Bespoke product workflows remain product-owned rather than being forced into a standard package view. + +Field labels follow their orientation: horizontal labels use the 13px semibold +label role, while vertical labels use the 11px regular meta role. Field helpers +use secondary content with 16px leading at the default 11px font size; +FormSection descriptions use primary content with 16px leading at 13px. Both +leading roles scale with user typography. FieldGroup uses the form group tint, +retaining its existing row padding, dividers, and radius. The Patterns form +specimen shows both orientations and long values over a tinted container. diff --git a/design-system/packages/ui/src/components/Field/Field.meta.ts b/design-system/packages/ui/src/components/Field/Field.meta.ts index 1d53681caf..735b0d542d 100644 --- a/design-system/packages/ui/src/components/Field/Field.meta.ts +++ b/design-system/packages/ui/src/components/Field/Field.meta.ts @@ -23,7 +23,7 @@ export const fieldMeta = { tokens: [ "color.content.requiredIndicator", "color.content.primary", - "color.content.muted", + "color.content.secondary", "color.status.danger.content", "layout.field.rootGap", "layout.field.horizontalGap", @@ -35,7 +35,9 @@ export const fieldMeta = { "layout.field.labelGap", "layout.field.labelActionGap", "layout.field.controlGap", - "type.label.md.fontSize", + "type.label.selected.fontSize", + "type.meta.fontSize", + "type.fieldHelper.lineHeight", "type.support.fontSize", ], } as const satisfies ComponentMeta; diff --git a/design-system/packages/ui/src/components/Field/Field.module.css b/design-system/packages/ui/src/components/Field/Field.module.css index 01a7fe0c3a..c6ed724fb6 100644 --- a/design-system/packages/ui/src/components/Field/Field.module.css +++ b/design-system/packages/ui/src/components/Field/Field.module.css @@ -66,11 +66,19 @@ align-items: center; gap: var(--openbitfun-layout-field-label-gap); color: var(--openbitfun-color-content-primary); - font-family: var(--openbitfun-type-label-md-font-family); - font-size: var(--openbitfun-type-label-md-font-size); - font-weight: var(--openbitfun-type-label-md-font-weight); - line-height: var(--openbitfun-type-label-md-line-height); - letter-spacing: var(--openbitfun-type-label-md-letter-spacing); + font-family: var(--openbitfun-type-meta-font-family); + font-size: var(--openbitfun-type-meta-font-size); + font-weight: var(--openbitfun-type-meta-font-weight); + line-height: var(--openbitfun-type-meta-line-height); + letter-spacing: var(--openbitfun-type-meta-letter-spacing); + } + + .root[data-orientation="horizontal"] .label { + font-family: var(--openbitfun-type-label-selected-font-family); + font-size: var(--openbitfun-type-label-selected-font-size); + font-weight: var(--openbitfun-type-label-selected-font-weight); + line-height: var(--openbitfun-type-label-selected-line-height); + letter-spacing: var(--openbitfun-type-label-selected-letter-spacing); } .required { @@ -85,11 +93,11 @@ } .description { - color: var(--openbitfun-color-content-muted); + color: var(--openbitfun-color-content-secondary); font-family: var(--openbitfun-type-support-font-family); font-size: var(--openbitfun-type-support-font-size); font-weight: var(--openbitfun-type-support-font-weight); - line-height: var(--openbitfun-type-support-line-height); + line-height: var(--openbitfun-type-field-helper-line-height); letter-spacing: var(--openbitfun-type-support-letter-spacing); } diff --git a/design-system/packages/ui/src/components/FieldGroup/FieldGroup.meta.ts b/design-system/packages/ui/src/components/FieldGroup/FieldGroup.meta.ts index 8f6bdc320e..efbb40b178 100644 --- a/design-system/packages/ui/src/components/FieldGroup/FieldGroup.meta.ts +++ b/design-system/packages/ui/src/components/FieldGroup/FieldGroup.meta.ts @@ -19,9 +19,8 @@ export const fieldGroupMeta = { ], states: ["subtle", "plain", "divided"], tokens: [ - "color.surface.tertiary", + "color.field.groupBackground", "color.content.primary", - "color.content.muted", "color.border.subtle", "color.field.background", "color.field.backgroundHover", @@ -30,6 +29,7 @@ export const fieldGroupMeta = { "layout.formSection.titleDescriptionGap", "type.heading.section.fontSize", "type.body.sm.fontSize", + "type.formDescription.lineHeight", "layout.fieldGroup.radius", "layout.fieldGroup.rowPaddingBlock", "layout.fieldGroup.rowPaddingInline", diff --git a/design-system/packages/ui/src/components/FieldGroup/FieldGroup.module.css b/design-system/packages/ui/src/components/FieldGroup/FieldGroup.module.css index 533ab09f75..d550fb16bf 100644 --- a/design-system/packages/ui/src/components/FieldGroup/FieldGroup.module.css +++ b/design-system/packages/ui/src/components/FieldGroup/FieldGroup.module.css @@ -63,11 +63,11 @@ } .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-form-description-line-height); letter-spacing: var(--openbitfun-type-body-sm-letter-spacing); } @@ -86,7 +86,7 @@ } .group[data-appearance="subtle"] { - background: var(--openbitfun-color-surface-tertiary); + background: var(--openbitfun-color-field-group-background); } .group[data-appearance="plain"] { diff --git a/design-system/packages/ui/tests/field-group.test.mjs b/design-system/packages/ui/tests/field-group.test.mjs index 25fdbaa5e4..672330e9ce 100644 --- a/design-system/packages/ui/tests/field-group.test.mjs +++ b/design-system/packages/ui/tests/field-group.test.mjs @@ -62,7 +62,7 @@ test("form grouping styles consume only shared public composition tokens", async assert.match(styles, /--openbitfun-layout-form-section-gap/); assert.match(styles, /--openbitfun-layout-field-group-radius/); assert.match(styles, /--openbitfun-layout-field-group-row-padding-block/); - assert.match(styles, /--openbitfun-color-surface-tertiary/); + assert.match(styles, /--openbitfun-color-field-group-background/); assert.match(styles, /--openbitfun-color-border-subtle/); }); diff --git a/design-system/packages/ui/tests/field.test.mjs b/design-system/packages/ui/tests/field.test.mjs index e5c9583605..ea394226f7 100644 --- a/design-system/packages/ui/tests/field.test.mjs +++ b/design-system/packages/ui/tests/field.test.mjs @@ -98,10 +98,10 @@ test("Field styles consume shared content and typography tokens", async () => { const styles = await readFile(new URL("../src/components/Field/Field.module.css", import.meta.url), "utf8"); assert.match(styles, /--openbitfun-color-content-primary/); - assert.match(styles, /--openbitfun-color-content-muted/); + assert.match(styles, /--openbitfun-color-content-secondary/); assert.match(styles, /--openbitfun-color-content-required-indicator/); assert.doesNotMatch(styles, /--openbitfun-color-status-danger-content[^\n]*required/); - assert.match(styles, /--openbitfun-type-label-md-font-size/); + assert.match(styles, /--openbitfun-type-label-selected-font-size/); assert.match(styles, /--openbitfun-type-support-font-size/); assert.match(styles, /--openbitfun-layout-field-root-gap/); assert.match(styles, /--openbitfun-layout-field-label-action-gap/); diff --git a/src/apps/data-migrator/ui/generated/design-system.css b/src/apps/data-migrator/ui/generated/design-system.css index 54da7f2e68..2f22ed0dc8 100644 --- a/src/apps/data-migrator/ui/generated/design-system.css +++ b/src/apps/data-migrator/ui/generated/design-system.css @@ -463,6 +463,7 @@ --openbitfun-type-display-xxl-font-weight: var(--openbitfun-font-weight-semibold); --openbitfun-type-display-xxl-letter-spacing: var(--openbitfun-letter-spacing-tight); --openbitfun-type-display-xxl-line-height: var(--openbitfun-line-height-display); + --openbitfun-type-field-helper-line-height: 1.4545454545454546; --openbitfun-type-flow-body-font-family: var(--openbitfun-font-family-sans); --openbitfun-type-flow-body-font-size: var(--openbitfun-font-size-base); --openbitfun-type-flow-body-font-weight: var(--openbitfun-font-weight-regular); @@ -513,6 +514,7 @@ --openbitfun-type-flow-title-font-weight: var(--openbitfun-font-weight-semibold); --openbitfun-type-flow-title-letter-spacing: var(--openbitfun-letter-spacing-snug); --openbitfun-type-flow-title-line-height: var(--openbitfun-line-height-tight); + --openbitfun-type-form-description-line-height: 1.2307692307692308; --openbitfun-type-heading-card-font-family: var(--openbitfun-font-family-control); --openbitfun-type-heading-card-font-size: var(--openbitfun-font-size-sm); --openbitfun-type-heading-card-font-weight: var(--openbitfun-font-weight-semibold); @@ -728,6 +730,7 @@ --openbitfun-color-field-border-active: rgba(0, 0, 0, 0.20); --openbitfun-color-field-border-focus: #858585; --openbitfun-color-field-border-hover: rgba(0, 0, 0, 0.20); + --openbitfun-color-field-group-background: rgba(0, 0, 0, 0.03); --openbitfun-color-field-placeholder: rgba(0, 0, 0, 0.40); --openbitfun-color-focus-ring: #6a6a6a; --openbitfun-color-identity-assistant-border: color-mix(in srgb, #db2777 15%, transparent); @@ -875,6 +878,7 @@ --openbitfun-color-field-border-active: #858585; --openbitfun-color-field-border-focus: #858585; --openbitfun-color-field-border-hover: rgba(255, 255, 255, 0.24); + --openbitfun-color-field-group-background: #0e0e10; --openbitfun-color-field-placeholder: #858585; --openbitfun-color-focus-ring: #60a5fa; --openbitfun-color-identity-assistant-border: color-mix(in srgb, #ec4899 15%, transparent); @@ -993,6 +997,7 @@ --openbitfun-color-field-border-active: #005fcc; --openbitfun-color-field-border-focus: #005fcc; --openbitfun-color-field-border-hover: #000000; + --openbitfun-color-field-group-background: #f7f7f7; --openbitfun-color-field-placeholder: #343434; --openbitfun-color-focus-ring: #005fcc; --openbitfun-color-identity-global-search-open-terminal: #000000; @@ -1054,6 +1059,7 @@ --openbitfun-color-field-border-active: #ffcc00; --openbitfun-color-field-border-focus: #ffcc00; --openbitfun-color-field-border-hover: #ffffff; + --openbitfun-color-field-group-background: #000000; --openbitfun-color-field-placeholder: #d0d0d0; --openbitfun-color-focus-ring: #ffcc00; --openbitfun-color-identity-global-search-open-terminal: #ffffff; diff --git a/src/apps/desktop/src/generated/bootstrap_theme.css b/src/apps/desktop/src/generated/bootstrap_theme.css index 80794639f6..f7431c6a52 100644 --- a/src/apps/desktop/src/generated/bootstrap_theme.css +++ b/src/apps/desktop/src/generated/bootstrap_theme.css @@ -63,6 +63,7 @@ --openbitfun-color-field-border-active: #858585; --openbitfun-color-field-border-focus: #858585; --openbitfun-color-field-border-hover: rgba(255, 255, 255, 0.24); + --openbitfun-color-field-group-background: #0e0e10; --openbitfun-color-field-placeholder: #858585; --openbitfun-color-focus-ring: #60a5fa; --openbitfun-color-identity-assistant-border: color-mix(in srgb, #ec4899 15%, transparent); diff --git a/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.scss b/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.scss index a581ba8179..f5a1426ed1 100644 --- a/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.scss +++ b/src/web-ui/src/app/components/NewProjectDialog/NewProjectDialog.scss @@ -6,7 +6,7 @@ &__content { overflow: hidden; - background: var(--openbitfun-color-surface-tertiary); + background: var(--openbitfun-color-field-group-background); border-radius: var(--openbitfun-radius-lg); } @@ -29,10 +29,10 @@ &__preview-label { color: var(--openbitfun-color-content-primary); - font-family: var(--openbitfun-type-label-md-font-family); - font-size: var(--openbitfun-type-label-md-font-size); - font-weight: var(--openbitfun-type-label-md-font-weight); - line-height: var(--openbitfun-type-label-md-line-height); + font-family: var(--openbitfun-type-meta-font-family); + font-size: var(--openbitfun-type-meta-font-size); + font-weight: var(--openbitfun-type-meta-font-weight); + line-height: var(--openbitfun-type-meta-line-height); } &__preview-path { 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 a23ce4a7ca..ac87080a1a 100644 --- a/src/web-ui/src/infrastructure/appearance/builtins/appearancePresetOutput.test.ts +++ b/src/web-ui/src/infrastructure/appearance/builtins/appearancePresetOutput.test.ts @@ -56,6 +56,7 @@ describe('builtin appearance preset output', () => { expect(settings?.tokens['--openbitfun-color-field-border-hover']).toBe('rgba(0, 0, 0, 0.20)'); expect(settings?.tokens['--openbitfun-color-field-border-active']).toBe('rgba(0, 0, 0, 0.20)'); expect(settings?.tokens['--openbitfun-color-field-border-focus']).toBe(themes.light['color.field.borderFocus']); + expect(settings?.tokens['--openbitfun-color-field-group-background']).toBe('rgba(0, 0, 0, 0.03)'); expect(settings?.tokens['--openbitfun-color-field-placeholder']).toBe('rgba(0, 0, 0, 0.40)'); for (const palette of builtinAppearancePalettes) { if (palette.id === 'openbitfun-light') continue; @@ -63,6 +64,7 @@ describe('builtin appearance preset output', () => { 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]); + expect(tokens['--openbitfun-color-field-group-background']).toBe(palette.colors.background.tertiary); expect(tokens['--openbitfun-color-field-placeholder']).toBe(palette.colors.text.muted); } }); diff --git a/src/web-ui/src/infrastructure/appearance/builtins/buildBuiltinAppearance.ts b/src/web-ui/src/infrastructure/appearance/builtins/buildBuiltinAppearance.ts index 7ee70f7c76..ecb1881237 100644 --- a/src/web-ui/src/infrastructure/appearance/builtins/buildBuiltinAppearance.ts +++ b/src/web-ui/src/infrastructure/appearance/builtins/buildBuiltinAppearance.ts @@ -157,6 +157,7 @@ function createThemeTokenValues(palette: AppearancePalette): Record { '--openbitfun-color-field-border': '#123456', '--openbitfun-color-field-border-focus': '#654321', '--openbitfun-color-content-muted': '#778899', + '--openbitfun-color-surface-tertiary': '#abcdef', }, - scopes: { chrome: { '--openbitfun-color-content-muted': '#556677', '--openbitfun-color-field-border-focus': '#445566' } }, + scopes: { chrome: { '--openbitfun-color-content-muted': '#556677', '--openbitfun-color-field-border-focus': '#445566', '--openbitfun-color-surface-tertiary': '#aabbcc' } }, }, }, }, @@ -30,7 +31,9 @@ describe('composeAppearancePackage', () => { '--openbitfun-color-field-border-focus': '#654321', '--openbitfun-color-field-border-active': '#654321', '--openbitfun-color-field-placeholder': '#778899', + '--openbitfun-color-field-group-background': '#abcdef', }); + expect(settings.scopes?.chrome?.['--openbitfun-color-field-group-background']).toBe('#aabbcc'); expect(settings.scopes?.chrome?.['--openbitfun-color-field-placeholder']).toBe('#556677'); expect(settings.scopes?.chrome?.['--openbitfun-color-field-border-active']).toBe('#445566'); expect(composeAppearancePackage(JSON.parse(JSON.stringify(resolved))).renderers?.['theme-tokens']).toEqual(resolved.renderers?.['theme-tokens']); @@ -40,7 +43,11 @@ describe('composeAppearancePackage', () => { original.renderers!['theme-tokens']!.settings.tokens['--openbitfun-color-field-border-active'] = '#223344'; original.renderers!['theme-tokens']!.settings.scopes!.chrome!['--openbitfun-color-field-placeholder'] = '#334455'; original.renderers!['theme-tokens']!.settings.scopes!.chrome!['--openbitfun-color-field-border-active'] = '#556688'; + original.renderers!['theme-tokens']!.settings.tokens['--openbitfun-color-field-group-background'] = '#123abc'; + original.renderers!['theme-tokens']!.settings.scopes!.chrome!['--openbitfun-color-field-group-background'] = '#456def'; const explicit = composeAppearancePackage(original).renderers!['theme-tokens']!.settings; + expect(explicit.tokens['--openbitfun-color-field-group-background']).toBe('#123abc'); + expect(explicit.scopes?.chrome?.['--openbitfun-color-field-group-background']).toBe('#456def'); expect(explicit.tokens['--openbitfun-color-field-placeholder']).toBe('#112233'); expect(explicit.tokens['--openbitfun-color-field-border-active']).toBe('#223344'); expect(explicit.scopes?.chrome?.['--openbitfun-color-field-placeholder']).toBe('#334455'); diff --git a/src/web-ui/src/infrastructure/appearance/builtins/fieldThemeCompatibility.ts b/src/web-ui/src/infrastructure/appearance/builtins/fieldThemeCompatibility.ts index fce11ae98b..da41d55a76 100644 --- a/src/web-ui/src/infrastructure/appearance/builtins/fieldThemeCompatibility.ts +++ b/src/web-ui/src/infrastructure/appearance/builtins/fieldThemeCompatibility.ts @@ -14,5 +14,9 @@ export function withLegacyFieldTokens( if (result['--openbitfun-color-field-border-active'] === undefined && legacyFocus !== undefined) { result['--openbitfun-color-field-border-active'] = legacyFocus; } + const legacyGroup = tokens?.['--openbitfun-color-surface-tertiary']; + if (result['--openbitfun-color-field-group-background'] === undefined && legacyGroup !== undefined) { + result['--openbitfun-color-field-group-background'] = legacyGroup; + } return result; } diff --git a/src/web-ui/src/infrastructure/config/components/ConfigForm.scss b/src/web-ui/src/infrastructure/config/components/ConfigForm.scss index a6cef30ee3..3fc00259aa 100644 --- a/src/web-ui/src/infrastructure/config/components/ConfigForm.scss +++ b/src/web-ui/src/infrastructure/config/components/ConfigForm.scss @@ -63,7 +63,7 @@ $config-input-height: 32px; font-weight: var(--openbitfun-type-support-font-weight); color: var(--openbitfun-color-content-secondary); margin-top: var(--openbitfun-space-1); - line-height: var(--openbitfun-type-modifier-leading-support-line-height); + line-height: var(--openbitfun-type-field-helper-line-height); letter-spacing: var(--openbitfun-type-support-letter-spacing); overflow-wrap: break-word; } @@ -414,7 +414,7 @@ $config-input-height: 32px; font-weight: var(--openbitfun-type-body-sm-font-weight); color: var(--openbitfun-color-content-primary); margin: 0 0 var(--openbitfun-space-5) 0; - line-height: var(--openbitfun-type-body-sm-line-height); + line-height: var(--openbitfun-type-form-description-line-height); letter-spacing: var(--openbitfun-type-body-sm-letter-spacing); overflow-wrap: break-word; } 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 7129b89d90..269183bf32 100644 --- a/src/web-ui/src/tools/generative-widget/appearancePayload.test.ts +++ b/src/web-ui/src/tools/generative-widget/appearancePayload.test.ts @@ -40,6 +40,9 @@ const FIELD_STATE_THEME_VARIABLE_NAMES = [ '--openbitfun-color-field-border-active', '--openbitfun-color-field-placeholder', ] as const; +// Grouped forms own a translucent fill independently of opaque tertiary surfaces +// and transient subtle feedback; keep this addition outside the shared fingerprint. +const FIELD_GROUP_THEME_VARIABLE_NAME = '--openbitfun-color-field-group-background'; const RETIRED_WIDGET_VARIABLE_NAMES = [ '--background-primary', '--bg-primary', @@ -87,9 +90,11 @@ describe('generated widget appearance payload contract', () => { const sharedNames = WIDGET_APPEARANCE_VAR_NAMES.filter(name => ( !name.startsWith('--openbitfun-component-button-') && !FIELD_STATE_THEME_VARIABLE_NAMES.some(fieldName => fieldName === name) + && name !== FIELD_GROUP_THEME_VARIABLE_NAME )); 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({ count: sharedNames.length, hash: hashNames(sharedNames), @@ -149,6 +154,7 @@ describe('generated widget appearance payload contract', () => { '--openbitfun-component-button-primary-background': '#303030', '--openbitfun-component-button-fill-background': 'rgba(0, 0, 0, 0.08)', '--openbitfun-color-field-border-active': 'rgba(0, 0, 0, 0.20)', + '--openbitfun-color-field-group-background': 'rgba(0, 0, 0, 0.03)', '--openbitfun-color-field-placeholder': 'rgba(0, 0, 0, 0.40)', '--openbitfun-color-status-danger-surface': 'rgba(200, 0, 0, 0.12)', '--openbitfun-color-status-danger-border': '#303030',