Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 <!DOCTYPE>, <html>, <head>, or <body>. 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 <!DOCTYPE>, <html>, <head>, or <body>. 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()
)
},
Expand Down
25 changes: 23 additions & 2 deletions src/web-ui/src/tools/generative-widget/appearancePayload.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @vitest-environment jsdom
import { createHash } from 'node:crypto';
import {
cssVariables as systemCssVariables,
Expand All @@ -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';
Expand Down Expand Up @@ -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<string, string>([
['--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',
Expand Down
18 changes: 6 additions & 12 deletions src/web-ui/src/tools/generative-widget/appearancePayload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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];
}
Expand Down
Loading