diff --git a/src/crates/assembly/core/src/agentic/tools/implementations/generative_ui_tool.rs b/src/crates/assembly/core/src/agentic/tools/implementations/generative_ui_tool.rs index 20f2b1c371..432a8dd24d 100644 --- a/src/crates/assembly/core/src/agentic/tools/implementations/generative_ui_tool.rs +++ b/src/crates/assembly/core/src/agentic/tools/implementations/generative_ui_tool.rs @@ -56,7 +56,7 @@ impl GenerativeUITool { } fn openbitfun_design_system_reminder() -> &'static str { - "OpenBitFun design-system reminder: when the widget should feel native to the host OpenBitFun app, compose the provided `openbitfun-*` scaffold classes first and use host-projected canonical tokens instead of hard-coded design values. Prefer CSS variables such as `var(--openbitfun-color-surface-canvas)`, `var(--openbitfun-color-surface-panel)`, `var(--openbitfun-color-surface-scene)`, `var(--openbitfun-color-surface-raised)`, `var(--openbitfun-color-content-primary)`, `var(--openbitfun-color-content-secondary)`, `var(--openbitfun-color-content-muted)`, `var(--openbitfun-color-accent-default)`, `var(--openbitfun-color-accent-hover)`, `var(--openbitfun-color-border-subtle)`, `var(--openbitfun-color-border-default)`, `var(--openbitfun-color-border-strong)`, `var(--openbitfun-color-action-neutral-surface)`, `var(--openbitfun-color-action-neutral-surface-hover)`, `var(--openbitfun-color-action-quiet-hover)`, `var(--openbitfun-shadow-*)`, `var(--openbitfun-motion-duration-*)`, `var(--openbitfun-motion-easing-*)`, `var(--openbitfun-font-family-sans)`, and `var(--openbitfun-font-family-mono)`. Radius, spacing, font-size, and font-weight variables are iframe-local scaffold values rather than host appearance extension points. Support both `openbitfun-dark` and `openbitfun-light`; do not assume dark-only, purple-only, or landing-page styling. Favor compact desktop workbench layouts, panel/card surfaces, strong information hierarchy, and reusable OpenBitFun component patterns. Avoid hard-coded colors, arbitrary spacing, giant hero sections, fake mobile chrome, and full marketing-page shells; prefer understated, premium UI with layered surfaces, restrained contrast, subtle borders, and do not use thick left-accent emphasis blocks." + "OpenBitFun design-system reminder: when the widget should feel native to the host OpenBitFun app, compose the provided `openbitfun-*` scaffold classes first and use host-projected canonical tokens instead of hard-coded design values. Prefer CSS variables such as `var(--openbitfun-color-surface-canvas)`, `var(--openbitfun-color-surface-panel)`, `var(--openbitfun-color-surface-scene)`, `var(--openbitfun-color-surface-raised)`, `var(--openbitfun-color-content-primary)`, `var(--openbitfun-color-content-secondary)`, `var(--openbitfun-color-content-muted)`, `var(--openbitfun-color-accent-default)`, `var(--openbitfun-color-accent-hover)`, `var(--openbitfun-color-border-subtle)`, `var(--openbitfun-color-border-default)`, `var(--openbitfun-color-border-strong)`, `var(--openbitfun-color-action-neutral-surface)`, `var(--openbitfun-color-action-neutral-surface-hover)`, `var(--openbitfun-color-action-quiet-hover)`, `var(--openbitfun-shadow-*)`, `var(--openbitfun-motion-duration-*)`, `var(--openbitfun-motion-easing-*)`, `var(--openbitfun-font-family-sans)`, and `var(--openbitfun-font-family-mono)`. Spacing (`var(--openbitfun-space-*)`), radius (`var(--openbitfun-radius-*)`), control sizing, typography (`var(--openbitfun-font-*)` and `var(--openbitfun-type-*)`), and motion variables are host-projected system tokens that follow the active appearance, density, and font settings; rely on them instead of hard-coded equivalents. Support both `openbitfun-dark` and `openbitfun-light`; do not assume dark-only, purple-only, or landing-page styling. Favor compact desktop workbench layouts, panel/card surfaces, strong information hierarchy, and reusable OpenBitFun component patterns. Avoid hard-coded colors, arbitrary spacing, giant hero sections, fake mobile chrome, and full marketing-page shells; prefer understated, premium UI with layered surfaces, restrained contrast, subtle borders, and do not use thick left-accent emphasis blocks." } fn openbitfun_widget_scaffold_reminder() -> &'static str { @@ -254,7 +254,7 @@ Input rules: "widget_code": { "type": "string", "description": format!( - "Raw HTML fragment or raw SVG. No Markdown code fences. For HTML: no , , , or . The 260-line / 28KB guideline is a soft reliability threshold. For larger widgets, use data-driven loops, shared CSS classes, and simpler markup rather than truncating required behavior. {} If the widget should match OpenBitFun, rely on `openbitfun-*` scaffold classes plus host-projected color, surface, status, border, shadow, motion, and font-family variables instead of hard-coded colors or custom chrome. Treat radius, spacing, font-size, and font-weight variables as iframe-local scaffold values, not host appearance extension points. If the user asked for file navigation, do not finish this field until each clickable node has verified file metadata or is intentionally non-clickable.", + "Raw HTML fragment or raw SVG. No Markdown code fences. For HTML: no , , , or . The 260-line / 28KB guideline is a soft reliability threshold. For larger widgets, use data-driven loops, shared CSS classes, and simpler markup rather than truncating required behavior. {} If the widget should match OpenBitFun, rely on `openbitfun-*` scaffold classes plus host-projected color, surface, status, border, shadow, motion, and font-family variables instead of hard-coded colors or custom chrome. Spacing, radius, control sizing, typography, and motion variables are also host-projected system tokens; prefer them over hard-coded values. If the user asked for file navigation, do not finish this field until each clickable node has verified file metadata or is intentionally non-clickable.", Self::combined_reminder() ) }, 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 32dce98fe3..69de31b95b 100644 --- a/src/web-ui/src/tools/generative-widget/appearancePayload.test.ts +++ b/src/web-ui/src/tools/generative-widget/appearancePayload.test.ts @@ -1,3 +1,4 @@ +// @vitest-environment jsdom import { createHash } from 'node:crypto'; import { cssVariables as systemCssVariables, @@ -10,7 +11,7 @@ import { widgetAppearanceAdapter } from '@/infrastructure/appearance/adapters/Wi import { WIDGET_APPEARANCE_FALLBACK_VARS, WIDGET_APPEARANCE_VAR_NAMES, - WIDGET_TYPOGRAPHY_VARIABLE_NAMES, + WIDGET_SYSTEM_VARIABLE_NAMES, createWidgetAppearanceFallbackCss, readWidgetAppearancePayload, } from './appearancePayload'; @@ -171,11 +172,31 @@ describe('generated widget appearance payload contract', () => { expect(Object.keys(WIDGET_APPEARANCE_FALLBACK_VARS)).toEqual(WIDGET_APPEARANCE_VAR_NAMES); expect(Object.keys(payload?.vars ?? {}).sort()).toEqual([ ...WIDGET_APPEARANCE_VAR_NAMES, - ...WIDGET_TYPOGRAPHY_VARIABLE_NAMES, + ...WIDGET_SYSTEM_VARIABLE_NAMES, ].sort()); expect(WIDGET_APPEARANCE_FALLBACK_VARS['--openbitfun-color-surface-canvas']).toBe('transparent'); }); + it('projects host system tokens over the static fallbacks', () => { + const computedStyle = new Map([ + ['--openbitfun-radius-base', '6px'], + ['--openbitfun-space-4', '12px'], + ['--openbitfun-type-body-sm-font-size', '15px'], + ]); + vi.stubGlobal('getComputedStyle', () => ({ + getPropertyValue: (name: string) => computedStyle.get(name) ?? '', + })); + + const payload = readWidgetAppearancePayload(); + + expect(payload?.vars['--openbitfun-radius-base']).toBe('6px'); + expect(payload?.vars['--openbitfun-space-4']).toBe('12px'); + expect(payload?.vars['--openbitfun-type-body-sm-font-size']).toBe('15px'); + expect(payload?.vars['--openbitfun-radius-lg']).toBe( + String(systemTokens['radius.lg']), + ); + }); + it('exports canonical state and status semantics without legacy aliases', () => { expect(WIDGET_APPEARANCE_VAR_NAMES).toEqual(expect.arrayContaining([ '--openbitfun-color-action-primary-background', diff --git a/src/web-ui/src/tools/generative-widget/appearancePayload.ts b/src/web-ui/src/tools/generative-widget/appearancePayload.ts index 6e2167b29b..bea8521f0a 100644 --- a/src/web-ui/src/tools/generative-widget/appearancePayload.ts +++ b/src/web-ui/src/tools/generative-widget/appearancePayload.ts @@ -36,17 +36,11 @@ const WIDGET_SYSTEM_FALLBACK_VARS = Object.freeze(Object.fromEntries( WIDGET_SYSTEM_TOKEN_NAMES.map(name => [systemCssVariables[name], String(systemTokens[name])]), )); -const WIDGET_TYPOGRAPHY_TOKEN_NAMES = Object.freeze( - WIDGET_SYSTEM_TOKEN_NAMES.filter(name => ( - name.startsWith('font.') - || name.startsWith('lineHeight.') - || name.startsWith('letterSpacing.') - || name.startsWith('type.') - )), -); - -export const WIDGET_TYPOGRAPHY_VARIABLE_NAMES = Object.freeze( - WIDGET_TYPOGRAPHY_TOKEN_NAMES.map(name => systemCssVariables[name]), +// The host design-system root carries the complete resolved system token set +// (density, font preferences, control sizing included), so every system token +// is projected instead of staying a frozen iframe scaffold value. +export const WIDGET_SYSTEM_VARIABLE_NAMES = Object.freeze( + WIDGET_SYSTEM_TOKEN_NAMES.map(name => systemCssVariables[name]), ); export function createWidgetAppearanceFallbackCss(): string { @@ -71,7 +65,7 @@ export function readWidgetAppearancePayload(): WidgetAppearancePayload | null { const computedStyle = typeof document !== 'undefined' && typeof getComputedStyle === 'function' ? getComputedStyle(document.documentElement) : null; - for (const name of WIDGET_TYPOGRAPHY_VARIABLE_NAMES) { + for (const name of WIDGET_SYSTEM_VARIABLE_NAMES) { vars[name] = computedStyle?.getPropertyValue(name).trim() || WIDGET_SYSTEM_FALLBACK_VARS[name]; }