diff --git a/craft/src/utils/html-export.ts b/craft/src/utils/html-export.ts index bbb7418..25d46a5 100644 --- a/craft/src/utils/html-export.ts +++ b/craft/src/utils/html-export.ts @@ -1,10 +1,10 @@ import { componentResolver } from '../components/resolver'; import type { CSSProperties } from 'react'; import { cssPropsToString } from './style-helpers'; -import { escapeHtml, escapeAttr, cssValue, safeImageUrl } from './escape'; +import { escapeHtml, escapeAttr, safeImageUrl } from './escape'; import { sanitizeContainerTag } from '../components/layout/Container'; import { SiteDesign, DEFAULT_SITE_DESIGN } from '../state/SiteDesignContext'; -import { TOKEN_VARS } from './tokens'; +import { buildTokenVarDecls } from './tokens'; import { BREAKPOINTS } from './breakpoints'; import { responsiveCssForNode, responsiveScopeId } from './responsive-style'; @@ -322,13 +322,8 @@ const A11Y_PERF_CSS = `a:focus-visible,button:focus-visible,input:focus-visible, * element itself. */ export function buildTokenCss(design: SiteDesign): string { - // A blank/whitespace token falls back to its default: an empty custom property - // would invalidate every var() that reads it (mirrors the backend sb_token_css). - const vars = TOKEN_VARS.map(([varName, key]) => { - const raw = String(design[key] ?? '').trim(); - return `${varName}:${cssValue(raw !== '' ? raw : (DEFAULT_SITE_DESIGN[key] as string))}`; - }).join(';'); - return `:root{${vars}}${TOKEN_BASE_CSS}${A11Y_PERF_CSS}`; + // Blank tokens fall back to defaults (shared with the canvas, see buildTokenVarDecls). + return `:root{${buildTokenVarDecls(design)}}${TOKEN_BASE_CSS}${A11Y_PERF_CSS}`; } // ---------- SEO/meta block (contract ยง4) ---------- diff --git a/craft/src/utils/tokens.test.ts b/craft/src/utils/tokens.test.ts index 6634f66..4d5a510 100644 --- a/craft/src/utils/tokens.test.ts +++ b/craft/src/utils/tokens.test.ts @@ -135,3 +135,16 @@ describe('buildCanvasTokenCss', () => { expect(css.split('}').length - 1).toBe(4); }); }); + +/** Minor finding: canvas token CSS had no blank-token fallback (export does). */ +describe('buildCanvasTokenCss blank-token fallback (parity with export)', () => { + test('blank/whitespace/missing tokens fall back to the default, same vars as the export :root block', () => { + const design = { ...DEFAULT_SITE_DESIGN, primaryColor: '', textColor: ' ', borderRadius: undefined } as unknown as SiteDesign; + const css = buildCanvasTokenCss(design); + expect(css).toContain(`--wsb-primary:${DEFAULT_SITE_DESIGN.primaryColor}`); + expect(css).toContain(`--wsb-text:${DEFAULT_SITE_DESIGN.textColor}`); + expect(css).toContain(`--wsb-radius:${DEFAULT_SITE_DESIGN.borderRadius}`); + const exportVars = /^:root\{([^}]*)\}/.exec(buildTokenCss(design))![1]; + expect(css).toContain(`.canvas-device-frame{${exportVars}}`); + }); +}); diff --git a/craft/src/utils/tokens.ts b/craft/src/utils/tokens.ts index 253608c..009f178 100644 --- a/craft/src/utils/tokens.ts +++ b/craft/src/utils/tokens.ts @@ -1,4 +1,4 @@ -import type { SiteDesign } from '../state/SiteDesignContext'; +import { DEFAULT_SITE_DESIGN, type SiteDesign } from '../state/SiteDesignContext'; import { cssValue } from './escape'; /** @@ -112,6 +112,18 @@ const CANVAS_TOKEN_BASE_CSS = * or `<` in a design value could otherwise break out of the rule / `