diff --git a/.agents/skills/impeccable/reference/document.md b/.agents/skills/impeccable/reference/document.md index 9ba4ee17a..572b7d879 100644 --- a/.agents/skills/impeccable/reference/document.md +++ b/.agents/skills/impeccable/reference/document.md @@ -1,8 +1,54 @@ Generate a `DESIGN.md` file at the project root that captures the current visual design system, so AI agents generating new screens stay on-brand. -DESIGN.md follows the [official Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/): exactly six sections in a fixed order. Sections may be omitted when not relevant, but **do not reorder them and do not rename them**. Section headers must match the spec character-for-character so the file stays parseable by other DESIGN.md-aware tools (Stitch itself, awesome-design-md, skill-rest, etc.). +DESIGN.md follows the [official Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/): YAML frontmatter carrying machine-readable design tokens, followed by a markdown body with exactly six sections in a fixed order. **Tokens are normative; prose provides context for how to apply them.** Sections may be omitted when not relevant, but **do not reorder them and do not rename them**. Section headers must match the spec character-for-character so the file stays parseable by other DESIGN.md-aware tools (Stitch itself, awesome-design-md, skill-rest, etc.). -## The six sections (exact order) +## The frontmatter: token schema + +The YAML frontmatter is the machine-readable layer. It's what Stitch's linter validates and what the live panel renders tiles from. Keep it tight; every entry should correspond to a token the project actually uses. + +```yaml +--- +name: +description: +colors: + primary: "#b8422e" + neutral-bg: "#faf7f2" + # ...one entry per extracted color; key = descriptive slug +typography: + display: + fontFamily: "Cormorant Garamond, Georgia, serif" + fontSize: "clamp(2.5rem, 7vw, 4.5rem)" + fontWeight: 300 + lineHeight: 1 + letterSpacing: "normal" + body: + # ... +rounded: + sm: "4px" + md: "8px" +spacing: + sm: "8px" + md: "16px" +components: + button-primary: + backgroundColor: "{colors.primary}" + textColor: "{colors.neutral-bg}" + rounded: "{rounded.sm}" + padding: "16px 48px" + button-primary-hover: + backgroundColor: "{colors.primary-deep}" +--- +``` + +Rules that matter: + +- **Token refs** use `{path.to.token}` (e.g. `{colors.primary}`, `{rounded.md}`). Components may reference primitives; primitives may not reference each other. +- **Stitch validates colors as hex sRGB only** (`#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`); OKLCH/HSL/P3 trigger a linter warning, not a hard error. YAML accepts the string either way and our own parser is format-agnostic. Choose based on project posture: (a) if the project has an "OKLCH-only" doctrine or uses Display-P3 values that don't round-trip through sRGB, put OKLCH directly in the frontmatter and accept the Stitch linter warning; (b) if the project wants strict Stitch compliance or plans to use their Tailwind/DTCG export pipeline, put hex in the frontmatter and keep OKLCH in prose as the canonical reference. Never split the source of truth without explicit reason. +- **Component sub-tokens** are limited to 8 props: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, `width`. Shadows, motion, focus rings, backdrop-filter — none of those fit. Carry them in the sidecar (Step 4b). +- **Scale keys are open-ended.** Use whatever names the project already uses (`warm-ash-cream`, `surface-container-low`). Don't rename to Material defaults. +- **Variants are naming convention, not schema.** `button-primary` / `button-primary-hover` / `button-primary-active` as sibling keys. + +## The markdown body: six sections (exact order) 1. `## Overview` 2. `## Colors` @@ -53,6 +99,17 @@ Build a structured draft from the discovered tokens. For each token class: - **Components**: For each common component (button, card, input, chip, list item, tooltip, nav), extract shape (radius), color assignment, hover/focus treatment, internal padding. - **Spacing + layout**: Fold into Overview or relevant Components. The spec does NOT have a Layout section. +### Step 2b: Stage the frontmatter + +From the auto-extracted tokens, draft the YAML frontmatter now (you'll write it at the top of DESIGN.md in Step 4). This is the machine-readable layer — what the live panel and Stitch's linter consume. + +- **Colors**: one entry per extracted color. Key = descriptive slug (`warm-ash-cream`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex — see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. +- **Typography**: one entry per role (`display`, `headline`, `title`, `body`, `label`). Typography is an object; include only the props that are real for the project (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`). +- **Rounded / Spacing**: whatever scale steps the project actually uses, keyed by whatever scale name the project uses (`sm` / `md` / `lg`, or `surface-sm`, or numeric steps). +- **Components**: one entry per variant (`button-primary`, `button-primary-hover`, `button-ghost`). Reference primitives via `{colors.X}`, `{rounded.Y}`. If a variant needs a property Stitch's 8-prop set doesn't cover (shadow, focus ring, backdrop-filter), carry the full snippet in the sidecar instead. + +Skip anything the project doesn't have. Empty scale keys or fabricated tokens pollute the spec. + ### Step 3: Ask the user for qualitative language The following require creative input that cannot be auto-extracted. Group them into one `AskUserQuestion` interaction: @@ -67,9 +124,16 @@ Quote a line from PRODUCT.md when possible so the user sees their own strategic ### Step 4: Write DESIGN.md -Use this exact structure. Headers must match character-for-character. Optional evocative subtitles (e.g. `## 2. Colors: The Coastal Palette`) are allowed. +The file opens with the YAML frontmatter staged in Step 2b (schema documented at the top of this reference), then the markdown body using the structure below. Headers must match character-for-character. Optional evocative subtitles (e.g. `## 2. Colors: The Coastal Palette`) are allowed. ```markdown +--- +name: [Project Title] +description: [one-line tagline] +colors: + # ... staged frontmatter from Step 2b +--- + # Design System: [Project Title] ## 1. Overview @@ -161,7 +225,7 @@ For each component, lead with a short character line, then specify shape, color ## 6. Do's and Don'ts -Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — include exact colors, pixel values, and named anti-patterns the user mentioned in PRODUCT.md. +Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — include exact colors, pixel values, and named anti-patterns the user mentioned in PRODUCT.md. **Every anti-reference in PRODUCT.md should show up here as a "Don't" with the same language**, so the visual spec carries the strategic line through. Quote PRODUCT.md directly where possible: if PRODUCT.md says *"avoid dark mode with purple gradients, neon accents, glassmorphism"*, the Don'ts here should repeat that by name. ### Do: - **Do** [specific prescription with exact values / named rule]. @@ -173,9 +237,9 @@ Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — i - **Don't** [...] ``` -### Step 4b: Write DESIGN.json sidecar +### Step 4b: Write DESIGN.json sidecar (extensions only) -After the Markdown is written, produce a machine-readable sidecar at `DESIGN.json` next to `DESIGN.md`. This powers the `$impeccable live` design-system panel, which renders a tile-based visualization of the system — color swatches with tonal ramps, Aa type specimens, live component previews. **The sidecar is how the panel shows *this project's* actual button/input/nav/card, not a generic approximation.** +The frontmatter owns token primitives (colors, typography, rounded, spacing, components). The sidecar at `DESIGN.json` carries **what Stitch's schema can't hold**: tonal ramps per color, shadow/elevation tokens, motion tokens, breakpoints, full component HTML/CSS snippets (the panel renders these into a shadow DOM), and narrative (north star, rules, do's/don'ts). It extends the frontmatter, it doesn't duplicate it. Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks to refresh the sidecar (e.g., from the live panel's stale-hint), preserve DESIGN.md and write only DESIGN.json. @@ -183,42 +247,32 @@ Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks ```json { - "schemaVersion": 1, + "schemaVersion": 2, "generatedAt": "ISO-8601 string", "title": "Design System: [Project Title]", - "tokens": { - "colors": [ - { - "role": "primary | secondary | tertiary | neutral | accent", - "name": "Descriptive Name", - "value": "#HEX or oklch(...) or rgba(...)", - "description": "Short role explanation (one sentence).", - "tonalRamp": ["...", "...", "..."] - } + "extensions": { + "colorMeta": { + "primary": { "role": "primary", "displayName": "Editorial Magenta", "canonical": "oklch(60% 0.25 350)", "tonalRamp": ["...", "...", "..."] }, + "warm-ash-cream": { "role": "neutral", "displayName": "Warm Ash Cream", "canonical": "oklch(96% 0.005 350)", "tonalRamp": ["...", "...", "..."] } + }, + "typographyMeta": { + "display": { "displayName": "Display", "purpose": "Hero headlines only." } + }, + "shadows": [ + { "name": "ambient-low", "value": "0 4px 24px rgba(0,0,0,0.12)", "purpose": "Diffuse hover glow under accent elements." } ], - "typography": [ - { - "role": "display | headline | title | body | label | mono", - "name": "Display", - "family": "Cormorant Garamond", - "fallback": "Georgia, serif", - "weight": 300, - "style": "normal | italic", - "sampleSize": "clamp(2.5rem, 7vw, 4.5rem) or 1rem", - "lineHeight": "1 | 1.2 | 1.6", - "letterSpacing": "normal | 0.05em", - "textTransform": "none | uppercase", - "purpose": "Short description of where this role is used." - } + "motion": [ + { "name": "ease-standard", "value": "cubic-bezier(0.4, 0, 0.2, 1)", "purpose": "Default easing for state transitions." } ], - "radii": [{ "name": "sm|md|lg|xl|full", "value": "4px" }], - "shadows": [{ "name": "Descriptive Name", "value": "0 4px 24px rgba(0,0,0,0.12)", "purpose": "..." }], - "spacing": [{ "name": "xs|sm|md|lg|xl|2xl|3xl", "value": "8px" }] + "breakpoints": [ + { "name": "sm", "value": "640px" } + ] }, "components": [ { "name": "Primary Button", "kind": "button | input | nav | chip | card | custom", + "refersTo": "button-primary", "description": "One-line what and when.", "html": "", "css": ".ds-btn-primary { background: #191c1d; color: #fff; padding: 16px 48px; letter-spacing: 0.05em; text-transform: uppercase; font-weight: 500; border: none; border-radius: 0; transition: background 0.2s, transform 0.2s; } .ds-btn-primary:hover { background: oklch(60% 0.25 350); transform: translateY(-2px); }" @@ -235,6 +289,8 @@ Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks } ``` +**What changed from schemaVersion 1.** The old sidecar carried token primitive arrays (`tokens.colors[]`, `tokens.typography[]`, etc.). Those values now live in the frontmatter. The sidecar only carries metadata that can't live in the frontmatter — tonal ramps, canonical OKLCH when the hex is an approximation, display names, role hints — keyed by the frontmatter token name (`colorMeta.`, `typographyMeta.`). Components still carry full HTML/CSS because Stitch's 8-prop set can't hold them. + #### Component translation rules The `html` and `css` fields must be **self-contained, drop-in snippets** that render correctly when injected into a shadow DOM. The panel applies them directly — no post-processing, no framework runtime. @@ -336,7 +392,7 @@ Per-section guidance in seed mode: - **Components**: omit entirely — no components exist yet. - **Do's and Don'ts**: carry PRODUCT.md's anti-references directly plus the anti-reference named in Q5. -Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens and components to render; there is nothing to show yet. The sidecar gets generated on the next Scan-mode run. +Seed mode writes a minimal frontmatter with `name` and `description` only — no colors, typography, rounded, spacing, or components yet. Real tokens land on the next Scan-mode run. Skip the `DESIGN.json` sidecar in seed mode for the same reason: nothing to render. ### Step 4: Confirm and refresh session cache @@ -346,6 +402,8 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an ## Style guidelines +- **Frontmatter first, prose second.** Tokens go in the YAML frontmatter; prose contextualizes them. Don't redefine a token value in two places — the frontmatter is normative. +- **Cite PRODUCT.md anti-references by name** in the Do's and Don'ts section. If PRODUCT.md lists "SaaS landing-page clichés" or "generic AI tool marketing" as anti-references, the DESIGN.md Don'ts should repeat those phrases verbatim so the visual spec enforces the strategic line. - **Match the spec, don't invent new sections.** The six section names are fixed. If you have Layout/Motion/Responsive content to document, fold it into Overview (philosophy-level rules) or Components (per-component behavior). - **Descriptive > technical**: "Gently curved edges (8px radius)" > "rounded-lg". Include the technical value in parens, lead with the description. - **Functional > decorative**: for each token, explain WHERE and WHY it's used, not just WHAT it is. @@ -365,3 +423,5 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an - Don't duplicate content from PRODUCT.md. DESIGN.md is strictly visual. - Don't add a "Layout Principles" or "Motion" or "Responsive Behavior" top-level section. The spec has six, not nine. Fold that content where it belongs. - Don't rename sections even slightly. "Colors" not "Color Palette & Roles". "Typography" not "Typography Rules". Tooling parsing depends on exact headers. +- Don't duplicate token values between frontmatter and prose. If a color is in `colors.primary` as hex, the prose can name it and describe its role but should not reassert a different hex. The frontmatter is normative. +- Don't invent frontmatter token groups outside Stitch's schema (no `motion:`, `breakpoints:`, `shadows:` at the top level). Stitch's Zod schema only accepts `colors`, `typography`, `rounded`, `spacing`, `components`. Anything else belongs in the sidecar's `extensions`. diff --git a/.agents/skills/impeccable/scripts/design-parser.mjs b/.agents/skills/impeccable/scripts/design-parser.mjs index 6220f67e7..b68756640 100644 --- a/.agents/skills/impeccable/scripts/design-parser.mjs +++ b/.agents/skills/impeccable/scripts/design-parser.mjs @@ -1,5 +1,10 @@ // Parse a DESIGN.md (Stitch-spec format) into a structured JSON model that // the live-mode design-system panel can render. Deterministic, dependency-free. +// +// Two-layer: YAML frontmatter (machine-readable tokens) + markdown body +// (prose with six canonical H2 sections). When frontmatter is present, it's +// exposed on `model.frontmatter` alongside the prose-scraped sections; +// consumers can prefer frontmatter values and fall back to prose. const CANONICAL_SECTIONS = [ 'Overview', @@ -10,6 +15,97 @@ const CANONICAL_SECTIONS = [ "Do's and Don'ts", ]; +// ---------- Frontmatter (Stitch YAML subset) ---------- + +function parseFrontmatter(md) { + const lines = md.split(/\r?\n/); + if (lines[0]?.trim() !== '---') return { frontmatter: null, body: md }; + + let end = -1; + for (let i = 1; i < lines.length; i++) { + if (lines[i].trim() === '---') { end = i; break; } + } + if (end === -1) return { frontmatter: null, body: md }; + + const yaml = lines.slice(1, end).join('\n'); + const body = lines.slice(end + 1).join('\n'); + try { + return { frontmatter: parseYamlSubset(yaml), body }; + } catch { + return { frontmatter: null, body: md }; + } +} + +// Minimal YAML reader for the Stitch frontmatter subset: scalar maps with +// one level of nested objects (typography roles, components). Indent-based, +// 2-space convention. No arrays, no anchors, no multi-line scalars — Stitch's +// schema doesn't need them and accepting them would require a real YAML +// dependency we don't want to vendor. +function parseYamlSubset(yaml) { + const lines = yaml.split(/\r?\n/); + const root = {}; + const stack = [{ indent: -1, obj: root }]; + + for (const raw of lines) { + // Skip blanks and line-only comments. Don't strip inline comments: + // unquoted hex values start with `#` and can't be safely distinguished + // from a comment after whitespace. + if (!raw.trim() || /^\s*#/.test(raw)) continue; + + const indent = raw.match(/^\s*/)[0].length; + const content = raw.slice(indent); + + const colonIdx = findTopLevelColon(content); + if (colonIdx === -1) continue; + + while (stack.length > 1 && stack[stack.length - 1].indent >= indent) { + stack.pop(); + } + + const key = content.slice(0, colonIdx).trim(); + const rest = content.slice(colonIdx + 1).trim(); + const parent = stack[stack.length - 1].obj; + + if (rest === '') { + const obj = {}; + parent[key] = obj; + stack.push({ indent, obj }); + } else { + parent[key] = parseScalar(rest); + } + } + + return root; +} + +function findTopLevelColon(s) { + let inQuote = null; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (inQuote) { + if (ch === inQuote && s[i - 1] !== '\\') inQuote = null; + } else if (ch === '"' || ch === "'") { + inQuote = ch; + } else if (ch === ':') { + return i; + } + } + return -1; +} + +function parseScalar(raw) { + const s = raw.trim(); + if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { + return s.slice(1, -1); + } + if (s === 'true') return true; + if (s === 'false') return false; + if (s === 'null' || s === '~') return null; + if (/^-?\d+$/.test(s)) return Number(s); + if (/^-?\d*\.\d+$/.test(s)) return Number(s); + return s; +} + const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g; const OKLCH_RE = /oklch\([^)]+\)/gi; const RGBA_RE = /rgba?\([^)]+\)/gi; @@ -706,10 +802,12 @@ function assessCoverage(model) { // ---------- Main ---------- export function parseDesignMd(md) { - const { title, sections } = splitSections(md); + const { frontmatter, body } = parseFrontmatter(md); + const { title, sections } = splitSections(body); return { - schemaVersion: 1, + schemaVersion: 2, title, + frontmatter, overview: extractOverview(sections['Overview']), colors: extractColors(sections['Colors']), typography: extractTypography(sections['Typography']), diff --git a/.agents/skills/impeccable/scripts/live-browser.js b/.agents/skills/impeccable/scripts/live-browser.js index 32ea9a191..d36af3536 100644 --- a/.agents/skills/impeccable/scripts/live-browser.js +++ b/.agents/skills/impeccable/scripts/live-browser.js @@ -3958,7 +3958,7 @@ void main() { function renderParsedMdCta() { const box = document.createElement('div'); box.className = 'parsed-md-cta'; - box.innerHTML = `Basic viewRunning /impeccable document generates DESIGN.json alongside your DESIGN.md, which lets this panel render your project's actual button, input, and nav primitives — not generic approximations.`; + box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running /impeccable document also generates a DESIGN.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; return box; } diff --git a/.claude/skills/impeccable/reference/document.md b/.claude/skills/impeccable/reference/document.md index 74f35efc5..98359de36 100644 --- a/.claude/skills/impeccable/reference/document.md +++ b/.claude/skills/impeccable/reference/document.md @@ -1,8 +1,54 @@ Generate a `DESIGN.md` file at the project root that captures the current visual design system, so AI agents generating new screens stay on-brand. -DESIGN.md follows the [official Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/): exactly six sections in a fixed order. Sections may be omitted when not relevant, but **do not reorder them and do not rename them**. Section headers must match the spec character-for-character so the file stays parseable by other DESIGN.md-aware tools (Stitch itself, awesome-design-md, skill-rest, etc.). +DESIGN.md follows the [official Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/): YAML frontmatter carrying machine-readable design tokens, followed by a markdown body with exactly six sections in a fixed order. **Tokens are normative; prose provides context for how to apply them.** Sections may be omitted when not relevant, but **do not reorder them and do not rename them**. Section headers must match the spec character-for-character so the file stays parseable by other DESIGN.md-aware tools (Stitch itself, awesome-design-md, skill-rest, etc.). -## The six sections (exact order) +## The frontmatter: token schema + +The YAML frontmatter is the machine-readable layer. It's what Stitch's linter validates and what the live panel renders tiles from. Keep it tight; every entry should correspond to a token the project actually uses. + +```yaml +--- +name: +description: +colors: + primary: "#b8422e" + neutral-bg: "#faf7f2" + # ...one entry per extracted color; key = descriptive slug +typography: + display: + fontFamily: "Cormorant Garamond, Georgia, serif" + fontSize: "clamp(2.5rem, 7vw, 4.5rem)" + fontWeight: 300 + lineHeight: 1 + letterSpacing: "normal" + body: + # ... +rounded: + sm: "4px" + md: "8px" +spacing: + sm: "8px" + md: "16px" +components: + button-primary: + backgroundColor: "{colors.primary}" + textColor: "{colors.neutral-bg}" + rounded: "{rounded.sm}" + padding: "16px 48px" + button-primary-hover: + backgroundColor: "{colors.primary-deep}" +--- +``` + +Rules that matter: + +- **Token refs** use `{path.to.token}` (e.g. `{colors.primary}`, `{rounded.md}`). Components may reference primitives; primitives may not reference each other. +- **Stitch validates colors as hex sRGB only** (`#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`); OKLCH/HSL/P3 trigger a linter warning, not a hard error. YAML accepts the string either way and our own parser is format-agnostic. Choose based on project posture: (a) if the project has an "OKLCH-only" doctrine or uses Display-P3 values that don't round-trip through sRGB, put OKLCH directly in the frontmatter and accept the Stitch linter warning; (b) if the project wants strict Stitch compliance or plans to use their Tailwind/DTCG export pipeline, put hex in the frontmatter and keep OKLCH in prose as the canonical reference. Never split the source of truth without explicit reason. +- **Component sub-tokens** are limited to 8 props: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, `width`. Shadows, motion, focus rings, backdrop-filter — none of those fit. Carry them in the sidecar (Step 4b). +- **Scale keys are open-ended.** Use whatever names the project already uses (`warm-ash-cream`, `surface-container-low`). Don't rename to Material defaults. +- **Variants are naming convention, not schema.** `button-primary` / `button-primary-hover` / `button-primary-active` as sibling keys. + +## The markdown body: six sections (exact order) 1. `## Overview` 2. `## Colors` @@ -53,6 +99,17 @@ Build a structured draft from the discovered tokens. For each token class: - **Components**: For each common component (button, card, input, chip, list item, tooltip, nav), extract shape (radius), color assignment, hover/focus treatment, internal padding. - **Spacing + layout**: Fold into Overview or relevant Components. The spec does NOT have a Layout section. +### Step 2b: Stage the frontmatter + +From the auto-extracted tokens, draft the YAML frontmatter now (you'll write it at the top of DESIGN.md in Step 4). This is the machine-readable layer — what the live panel and Stitch's linter consume. + +- **Colors**: one entry per extracted color. Key = descriptive slug (`warm-ash-cream`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex — see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. +- **Typography**: one entry per role (`display`, `headline`, `title`, `body`, `label`). Typography is an object; include only the props that are real for the project (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`). +- **Rounded / Spacing**: whatever scale steps the project actually uses, keyed by whatever scale name the project uses (`sm` / `md` / `lg`, or `surface-sm`, or numeric steps). +- **Components**: one entry per variant (`button-primary`, `button-primary-hover`, `button-ghost`). Reference primitives via `{colors.X}`, `{rounded.Y}`. If a variant needs a property Stitch's 8-prop set doesn't cover (shadow, focus ring, backdrop-filter), carry the full snippet in the sidecar instead. + +Skip anything the project doesn't have. Empty scale keys or fabricated tokens pollute the spec. + ### Step 3: Ask the user for qualitative language The following require creative input that cannot be auto-extracted. Group them into one `AskUserQuestion` interaction: @@ -67,9 +124,16 @@ Quote a line from PRODUCT.md when possible so the user sees their own strategic ### Step 4: Write DESIGN.md -Use this exact structure. Headers must match character-for-character. Optional evocative subtitles (e.g. `## 2. Colors: The Coastal Palette`) are allowed. +The file opens with the YAML frontmatter staged in Step 2b (schema documented at the top of this reference), then the markdown body using the structure below. Headers must match character-for-character. Optional evocative subtitles (e.g. `## 2. Colors: The Coastal Palette`) are allowed. ```markdown +--- +name: [Project Title] +description: [one-line tagline] +colors: + # ... staged frontmatter from Step 2b +--- + # Design System: [Project Title] ## 1. Overview @@ -161,7 +225,7 @@ For each component, lead with a short character line, then specify shape, color ## 6. Do's and Don'ts -Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — include exact colors, pixel values, and named anti-patterns the user mentioned in PRODUCT.md. +Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — include exact colors, pixel values, and named anti-patterns the user mentioned in PRODUCT.md. **Every anti-reference in PRODUCT.md should show up here as a "Don't" with the same language**, so the visual spec carries the strategic line through. Quote PRODUCT.md directly where possible: if PRODUCT.md says *"avoid dark mode with purple gradients, neon accents, glassmorphism"*, the Don'ts here should repeat that by name. ### Do: - **Do** [specific prescription with exact values / named rule]. @@ -173,9 +237,9 @@ Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — i - **Don't** [...] ``` -### Step 4b: Write DESIGN.json sidecar +### Step 4b: Write DESIGN.json sidecar (extensions only) -After the Markdown is written, produce a machine-readable sidecar at `DESIGN.json` next to `DESIGN.md`. This powers the `/impeccable live` design-system panel, which renders a tile-based visualization of the system — color swatches with tonal ramps, Aa type specimens, live component previews. **The sidecar is how the panel shows *this project's* actual button/input/nav/card, not a generic approximation.** +The frontmatter owns token primitives (colors, typography, rounded, spacing, components). The sidecar at `DESIGN.json` carries **what Stitch's schema can't hold**: tonal ramps per color, shadow/elevation tokens, motion tokens, breakpoints, full component HTML/CSS snippets (the panel renders these into a shadow DOM), and narrative (north star, rules, do's/don'ts). It extends the frontmatter, it doesn't duplicate it. Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks to refresh the sidecar (e.g., from the live panel's stale-hint), preserve DESIGN.md and write only DESIGN.json. @@ -183,42 +247,32 @@ Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks ```json { - "schemaVersion": 1, + "schemaVersion": 2, "generatedAt": "ISO-8601 string", "title": "Design System: [Project Title]", - "tokens": { - "colors": [ - { - "role": "primary | secondary | tertiary | neutral | accent", - "name": "Descriptive Name", - "value": "#HEX or oklch(...) or rgba(...)", - "description": "Short role explanation (one sentence).", - "tonalRamp": ["...", "...", "..."] - } + "extensions": { + "colorMeta": { + "primary": { "role": "primary", "displayName": "Editorial Magenta", "canonical": "oklch(60% 0.25 350)", "tonalRamp": ["...", "...", "..."] }, + "warm-ash-cream": { "role": "neutral", "displayName": "Warm Ash Cream", "canonical": "oklch(96% 0.005 350)", "tonalRamp": ["...", "...", "..."] } + }, + "typographyMeta": { + "display": { "displayName": "Display", "purpose": "Hero headlines only." } + }, + "shadows": [ + { "name": "ambient-low", "value": "0 4px 24px rgba(0,0,0,0.12)", "purpose": "Diffuse hover glow under accent elements." } ], - "typography": [ - { - "role": "display | headline | title | body | label | mono", - "name": "Display", - "family": "Cormorant Garamond", - "fallback": "Georgia, serif", - "weight": 300, - "style": "normal | italic", - "sampleSize": "clamp(2.5rem, 7vw, 4.5rem) or 1rem", - "lineHeight": "1 | 1.2 | 1.6", - "letterSpacing": "normal | 0.05em", - "textTransform": "none | uppercase", - "purpose": "Short description of where this role is used." - } + "motion": [ + { "name": "ease-standard", "value": "cubic-bezier(0.4, 0, 0.2, 1)", "purpose": "Default easing for state transitions." } ], - "radii": [{ "name": "sm|md|lg|xl|full", "value": "4px" }], - "shadows": [{ "name": "Descriptive Name", "value": "0 4px 24px rgba(0,0,0,0.12)", "purpose": "..." }], - "spacing": [{ "name": "xs|sm|md|lg|xl|2xl|3xl", "value": "8px" }] + "breakpoints": [ + { "name": "sm", "value": "640px" } + ] }, "components": [ { "name": "Primary Button", "kind": "button | input | nav | chip | card | custom", + "refersTo": "button-primary", "description": "One-line what and when.", "html": "", "css": ".ds-btn-primary { background: #191c1d; color: #fff; padding: 16px 48px; letter-spacing: 0.05em; text-transform: uppercase; font-weight: 500; border: none; border-radius: 0; transition: background 0.2s, transform 0.2s; } .ds-btn-primary:hover { background: oklch(60% 0.25 350); transform: translateY(-2px); }" @@ -235,6 +289,8 @@ Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks } ``` +**What changed from schemaVersion 1.** The old sidecar carried token primitive arrays (`tokens.colors[]`, `tokens.typography[]`, etc.). Those values now live in the frontmatter. The sidecar only carries metadata that can't live in the frontmatter — tonal ramps, canonical OKLCH when the hex is an approximation, display names, role hints — keyed by the frontmatter token name (`colorMeta.`, `typographyMeta.`). Components still carry full HTML/CSS because Stitch's 8-prop set can't hold them. + #### Component translation rules The `html` and `css` fields must be **self-contained, drop-in snippets** that render correctly when injected into a shadow DOM. The panel applies them directly — no post-processing, no framework runtime. @@ -336,7 +392,7 @@ Per-section guidance in seed mode: - **Components**: omit entirely — no components exist yet. - **Do's and Don'ts**: carry PRODUCT.md's anti-references directly plus the anti-reference named in Q5. -Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens and components to render; there is nothing to show yet. The sidecar gets generated on the next Scan-mode run. +Seed mode writes a minimal frontmatter with `name` and `description` only — no colors, typography, rounded, spacing, or components yet. Real tokens land on the next Scan-mode run. Skip the `DESIGN.json` sidecar in seed mode for the same reason: nothing to render. ### Step 4: Confirm and refresh session cache @@ -346,6 +402,8 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an ## Style guidelines +- **Frontmatter first, prose second.** Tokens go in the YAML frontmatter; prose contextualizes them. Don't redefine a token value in two places — the frontmatter is normative. +- **Cite PRODUCT.md anti-references by name** in the Do's and Don'ts section. If PRODUCT.md lists "SaaS landing-page clichés" or "generic AI tool marketing" as anti-references, the DESIGN.md Don'ts should repeat those phrases verbatim so the visual spec enforces the strategic line. - **Match the spec, don't invent new sections.** The six section names are fixed. If you have Layout/Motion/Responsive content to document, fold it into Overview (philosophy-level rules) or Components (per-component behavior). - **Descriptive > technical**: "Gently curved edges (8px radius)" > "rounded-lg". Include the technical value in parens, lead with the description. - **Functional > decorative**: for each token, explain WHERE and WHY it's used, not just WHAT it is. @@ -365,3 +423,5 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an - Don't duplicate content from PRODUCT.md. DESIGN.md is strictly visual. - Don't add a "Layout Principles" or "Motion" or "Responsive Behavior" top-level section. The spec has six, not nine. Fold that content where it belongs. - Don't rename sections even slightly. "Colors" not "Color Palette & Roles". "Typography" not "Typography Rules". Tooling parsing depends on exact headers. +- Don't duplicate token values between frontmatter and prose. If a color is in `colors.primary` as hex, the prose can name it and describe its role but should not reassert a different hex. The frontmatter is normative. +- Don't invent frontmatter token groups outside Stitch's schema (no `motion:`, `breakpoints:`, `shadows:` at the top level). Stitch's Zod schema only accepts `colors`, `typography`, `rounded`, `spacing`, `components`. Anything else belongs in the sidecar's `extensions`. diff --git a/.claude/skills/impeccable/scripts/design-parser.mjs b/.claude/skills/impeccable/scripts/design-parser.mjs index 6220f67e7..b68756640 100644 --- a/.claude/skills/impeccable/scripts/design-parser.mjs +++ b/.claude/skills/impeccable/scripts/design-parser.mjs @@ -1,5 +1,10 @@ // Parse a DESIGN.md (Stitch-spec format) into a structured JSON model that // the live-mode design-system panel can render. Deterministic, dependency-free. +// +// Two-layer: YAML frontmatter (machine-readable tokens) + markdown body +// (prose with six canonical H2 sections). When frontmatter is present, it's +// exposed on `model.frontmatter` alongside the prose-scraped sections; +// consumers can prefer frontmatter values and fall back to prose. const CANONICAL_SECTIONS = [ 'Overview', @@ -10,6 +15,97 @@ const CANONICAL_SECTIONS = [ "Do's and Don'ts", ]; +// ---------- Frontmatter (Stitch YAML subset) ---------- + +function parseFrontmatter(md) { + const lines = md.split(/\r?\n/); + if (lines[0]?.trim() !== '---') return { frontmatter: null, body: md }; + + let end = -1; + for (let i = 1; i < lines.length; i++) { + if (lines[i].trim() === '---') { end = i; break; } + } + if (end === -1) return { frontmatter: null, body: md }; + + const yaml = lines.slice(1, end).join('\n'); + const body = lines.slice(end + 1).join('\n'); + try { + return { frontmatter: parseYamlSubset(yaml), body }; + } catch { + return { frontmatter: null, body: md }; + } +} + +// Minimal YAML reader for the Stitch frontmatter subset: scalar maps with +// one level of nested objects (typography roles, components). Indent-based, +// 2-space convention. No arrays, no anchors, no multi-line scalars — Stitch's +// schema doesn't need them and accepting them would require a real YAML +// dependency we don't want to vendor. +function parseYamlSubset(yaml) { + const lines = yaml.split(/\r?\n/); + const root = {}; + const stack = [{ indent: -1, obj: root }]; + + for (const raw of lines) { + // Skip blanks and line-only comments. Don't strip inline comments: + // unquoted hex values start with `#` and can't be safely distinguished + // from a comment after whitespace. + if (!raw.trim() || /^\s*#/.test(raw)) continue; + + const indent = raw.match(/^\s*/)[0].length; + const content = raw.slice(indent); + + const colonIdx = findTopLevelColon(content); + if (colonIdx === -1) continue; + + while (stack.length > 1 && stack[stack.length - 1].indent >= indent) { + stack.pop(); + } + + const key = content.slice(0, colonIdx).trim(); + const rest = content.slice(colonIdx + 1).trim(); + const parent = stack[stack.length - 1].obj; + + if (rest === '') { + const obj = {}; + parent[key] = obj; + stack.push({ indent, obj }); + } else { + parent[key] = parseScalar(rest); + } + } + + return root; +} + +function findTopLevelColon(s) { + let inQuote = null; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (inQuote) { + if (ch === inQuote && s[i - 1] !== '\\') inQuote = null; + } else if (ch === '"' || ch === "'") { + inQuote = ch; + } else if (ch === ':') { + return i; + } + } + return -1; +} + +function parseScalar(raw) { + const s = raw.trim(); + if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { + return s.slice(1, -1); + } + if (s === 'true') return true; + if (s === 'false') return false; + if (s === 'null' || s === '~') return null; + if (/^-?\d+$/.test(s)) return Number(s); + if (/^-?\d*\.\d+$/.test(s)) return Number(s); + return s; +} + const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g; const OKLCH_RE = /oklch\([^)]+\)/gi; const RGBA_RE = /rgba?\([^)]+\)/gi; @@ -706,10 +802,12 @@ function assessCoverage(model) { // ---------- Main ---------- export function parseDesignMd(md) { - const { title, sections } = splitSections(md); + const { frontmatter, body } = parseFrontmatter(md); + const { title, sections } = splitSections(body); return { - schemaVersion: 1, + schemaVersion: 2, title, + frontmatter, overview: extractOverview(sections['Overview']), colors: extractColors(sections['Colors']), typography: extractTypography(sections['Typography']), diff --git a/.claude/skills/impeccable/scripts/live-browser.js b/.claude/skills/impeccable/scripts/live-browser.js index 32ea9a191..d36af3536 100644 --- a/.claude/skills/impeccable/scripts/live-browser.js +++ b/.claude/skills/impeccable/scripts/live-browser.js @@ -3958,7 +3958,7 @@ void main() { function renderParsedMdCta() { const box = document.createElement('div'); box.className = 'parsed-md-cta'; - box.innerHTML = `Basic viewRunning /impeccable document generates DESIGN.json alongside your DESIGN.md, which lets this panel render your project's actual button, input, and nav primitives — not generic approximations.`; + box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running /impeccable document also generates a DESIGN.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; return box; } diff --git a/.cursor/skills/impeccable/reference/document.md b/.cursor/skills/impeccable/reference/document.md index 244a1927f..2973b0594 100644 --- a/.cursor/skills/impeccable/reference/document.md +++ b/.cursor/skills/impeccable/reference/document.md @@ -1,8 +1,54 @@ Generate a `DESIGN.md` file at the project root that captures the current visual design system, so AI agents generating new screens stay on-brand. -DESIGN.md follows the [official Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/): exactly six sections in a fixed order. Sections may be omitted when not relevant, but **do not reorder them and do not rename them**. Section headers must match the spec character-for-character so the file stays parseable by other DESIGN.md-aware tools (Stitch itself, awesome-design-md, skill-rest, etc.). +DESIGN.md follows the [official Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/): YAML frontmatter carrying machine-readable design tokens, followed by a markdown body with exactly six sections in a fixed order. **Tokens are normative; prose provides context for how to apply them.** Sections may be omitted when not relevant, but **do not reorder them and do not rename them**. Section headers must match the spec character-for-character so the file stays parseable by other DESIGN.md-aware tools (Stitch itself, awesome-design-md, skill-rest, etc.). -## The six sections (exact order) +## The frontmatter: token schema + +The YAML frontmatter is the machine-readable layer. It's what Stitch's linter validates and what the live panel renders tiles from. Keep it tight; every entry should correspond to a token the project actually uses. + +```yaml +--- +name: +description: +colors: + primary: "#b8422e" + neutral-bg: "#faf7f2" + # ...one entry per extracted color; key = descriptive slug +typography: + display: + fontFamily: "Cormorant Garamond, Georgia, serif" + fontSize: "clamp(2.5rem, 7vw, 4.5rem)" + fontWeight: 300 + lineHeight: 1 + letterSpacing: "normal" + body: + # ... +rounded: + sm: "4px" + md: "8px" +spacing: + sm: "8px" + md: "16px" +components: + button-primary: + backgroundColor: "{colors.primary}" + textColor: "{colors.neutral-bg}" + rounded: "{rounded.sm}" + padding: "16px 48px" + button-primary-hover: + backgroundColor: "{colors.primary-deep}" +--- +``` + +Rules that matter: + +- **Token refs** use `{path.to.token}` (e.g. `{colors.primary}`, `{rounded.md}`). Components may reference primitives; primitives may not reference each other. +- **Stitch validates colors as hex sRGB only** (`#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`); OKLCH/HSL/P3 trigger a linter warning, not a hard error. YAML accepts the string either way and our own parser is format-agnostic. Choose based on project posture: (a) if the project has an "OKLCH-only" doctrine or uses Display-P3 values that don't round-trip through sRGB, put OKLCH directly in the frontmatter and accept the Stitch linter warning; (b) if the project wants strict Stitch compliance or plans to use their Tailwind/DTCG export pipeline, put hex in the frontmatter and keep OKLCH in prose as the canonical reference. Never split the source of truth without explicit reason. +- **Component sub-tokens** are limited to 8 props: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, `width`. Shadows, motion, focus rings, backdrop-filter — none of those fit. Carry them in the sidecar (Step 4b). +- **Scale keys are open-ended.** Use whatever names the project already uses (`warm-ash-cream`, `surface-container-low`). Don't rename to Material defaults. +- **Variants are naming convention, not schema.** `button-primary` / `button-primary-hover` / `button-primary-active` as sibling keys. + +## The markdown body: six sections (exact order) 1. `## Overview` 2. `## Colors` @@ -53,6 +99,17 @@ Build a structured draft from the discovered tokens. For each token class: - **Components**: For each common component (button, card, input, chip, list item, tooltip, nav), extract shape (radius), color assignment, hover/focus treatment, internal padding. - **Spacing + layout**: Fold into Overview or relevant Components. The spec does NOT have a Layout section. +### Step 2b: Stage the frontmatter + +From the auto-extracted tokens, draft the YAML frontmatter now (you'll write it at the top of DESIGN.md in Step 4). This is the machine-readable layer — what the live panel and Stitch's linter consume. + +- **Colors**: one entry per extracted color. Key = descriptive slug (`warm-ash-cream`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex — see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. +- **Typography**: one entry per role (`display`, `headline`, `title`, `body`, `label`). Typography is an object; include only the props that are real for the project (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`). +- **Rounded / Spacing**: whatever scale steps the project actually uses, keyed by whatever scale name the project uses (`sm` / `md` / `lg`, or `surface-sm`, or numeric steps). +- **Components**: one entry per variant (`button-primary`, `button-primary-hover`, `button-ghost`). Reference primitives via `{colors.X}`, `{rounded.Y}`. If a variant needs a property Stitch's 8-prop set doesn't cover (shadow, focus ring, backdrop-filter), carry the full snippet in the sidecar instead. + +Skip anything the project doesn't have. Empty scale keys or fabricated tokens pollute the spec. + ### Step 3: Ask the user for qualitative language The following require creative input that cannot be auto-extracted. Group them into one `AskUserQuestion` interaction: @@ -67,9 +124,16 @@ Quote a line from PRODUCT.md when possible so the user sees their own strategic ### Step 4: Write DESIGN.md -Use this exact structure. Headers must match character-for-character. Optional evocative subtitles (e.g. `## 2. Colors: The Coastal Palette`) are allowed. +The file opens with the YAML frontmatter staged in Step 2b (schema documented at the top of this reference), then the markdown body using the structure below. Headers must match character-for-character. Optional evocative subtitles (e.g. `## 2. Colors: The Coastal Palette`) are allowed. ```markdown +--- +name: [Project Title] +description: [one-line tagline] +colors: + # ... staged frontmatter from Step 2b +--- + # Design System: [Project Title] ## 1. Overview @@ -161,7 +225,7 @@ For each component, lead with a short character line, then specify shape, color ## 6. Do's and Don'ts -Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — include exact colors, pixel values, and named anti-patterns the user mentioned in PRODUCT.md. +Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — include exact colors, pixel values, and named anti-patterns the user mentioned in PRODUCT.md. **Every anti-reference in PRODUCT.md should show up here as a "Don't" with the same language**, so the visual spec carries the strategic line through. Quote PRODUCT.md directly where possible: if PRODUCT.md says *"avoid dark mode with purple gradients, neon accents, glassmorphism"*, the Don'ts here should repeat that by name. ### Do: - **Do** [specific prescription with exact values / named rule]. @@ -173,9 +237,9 @@ Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — i - **Don't** [...] ``` -### Step 4b: Write DESIGN.json sidecar +### Step 4b: Write DESIGN.json sidecar (extensions only) -After the Markdown is written, produce a machine-readable sidecar at `DESIGN.json` next to `DESIGN.md`. This powers the `/impeccable live` design-system panel, which renders a tile-based visualization of the system — color swatches with tonal ramps, Aa type specimens, live component previews. **The sidecar is how the panel shows *this project's* actual button/input/nav/card, not a generic approximation.** +The frontmatter owns token primitives (colors, typography, rounded, spacing, components). The sidecar at `DESIGN.json` carries **what Stitch's schema can't hold**: tonal ramps per color, shadow/elevation tokens, motion tokens, breakpoints, full component HTML/CSS snippets (the panel renders these into a shadow DOM), and narrative (north star, rules, do's/don'ts). It extends the frontmatter, it doesn't duplicate it. Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks to refresh the sidecar (e.g., from the live panel's stale-hint), preserve DESIGN.md and write only DESIGN.json. @@ -183,42 +247,32 @@ Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks ```json { - "schemaVersion": 1, + "schemaVersion": 2, "generatedAt": "ISO-8601 string", "title": "Design System: [Project Title]", - "tokens": { - "colors": [ - { - "role": "primary | secondary | tertiary | neutral | accent", - "name": "Descriptive Name", - "value": "#HEX or oklch(...) or rgba(...)", - "description": "Short role explanation (one sentence).", - "tonalRamp": ["...", "...", "..."] - } + "extensions": { + "colorMeta": { + "primary": { "role": "primary", "displayName": "Editorial Magenta", "canonical": "oklch(60% 0.25 350)", "tonalRamp": ["...", "...", "..."] }, + "warm-ash-cream": { "role": "neutral", "displayName": "Warm Ash Cream", "canonical": "oklch(96% 0.005 350)", "tonalRamp": ["...", "...", "..."] } + }, + "typographyMeta": { + "display": { "displayName": "Display", "purpose": "Hero headlines only." } + }, + "shadows": [ + { "name": "ambient-low", "value": "0 4px 24px rgba(0,0,0,0.12)", "purpose": "Diffuse hover glow under accent elements." } ], - "typography": [ - { - "role": "display | headline | title | body | label | mono", - "name": "Display", - "family": "Cormorant Garamond", - "fallback": "Georgia, serif", - "weight": 300, - "style": "normal | italic", - "sampleSize": "clamp(2.5rem, 7vw, 4.5rem) or 1rem", - "lineHeight": "1 | 1.2 | 1.6", - "letterSpacing": "normal | 0.05em", - "textTransform": "none | uppercase", - "purpose": "Short description of where this role is used." - } + "motion": [ + { "name": "ease-standard", "value": "cubic-bezier(0.4, 0, 0.2, 1)", "purpose": "Default easing for state transitions." } ], - "radii": [{ "name": "sm|md|lg|xl|full", "value": "4px" }], - "shadows": [{ "name": "Descriptive Name", "value": "0 4px 24px rgba(0,0,0,0.12)", "purpose": "..." }], - "spacing": [{ "name": "xs|sm|md|lg|xl|2xl|3xl", "value": "8px" }] + "breakpoints": [ + { "name": "sm", "value": "640px" } + ] }, "components": [ { "name": "Primary Button", "kind": "button | input | nav | chip | card | custom", + "refersTo": "button-primary", "description": "One-line what and when.", "html": "", "css": ".ds-btn-primary { background: #191c1d; color: #fff; padding: 16px 48px; letter-spacing: 0.05em; text-transform: uppercase; font-weight: 500; border: none; border-radius: 0; transition: background 0.2s, transform 0.2s; } .ds-btn-primary:hover { background: oklch(60% 0.25 350); transform: translateY(-2px); }" @@ -235,6 +289,8 @@ Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks } ``` +**What changed from schemaVersion 1.** The old sidecar carried token primitive arrays (`tokens.colors[]`, `tokens.typography[]`, etc.). Those values now live in the frontmatter. The sidecar only carries metadata that can't live in the frontmatter — tonal ramps, canonical OKLCH when the hex is an approximation, display names, role hints — keyed by the frontmatter token name (`colorMeta.`, `typographyMeta.`). Components still carry full HTML/CSS because Stitch's 8-prop set can't hold them. + #### Component translation rules The `html` and `css` fields must be **self-contained, drop-in snippets** that render correctly when injected into a shadow DOM. The panel applies them directly — no post-processing, no framework runtime. @@ -336,7 +392,7 @@ Per-section guidance in seed mode: - **Components**: omit entirely — no components exist yet. - **Do's and Don'ts**: carry PRODUCT.md's anti-references directly plus the anti-reference named in Q5. -Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens and components to render; there is nothing to show yet. The sidecar gets generated on the next Scan-mode run. +Seed mode writes a minimal frontmatter with `name` and `description` only — no colors, typography, rounded, spacing, or components yet. Real tokens land on the next Scan-mode run. Skip the `DESIGN.json` sidecar in seed mode for the same reason: nothing to render. ### Step 4: Confirm and refresh session cache @@ -346,6 +402,8 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an ## Style guidelines +- **Frontmatter first, prose second.** Tokens go in the YAML frontmatter; prose contextualizes them. Don't redefine a token value in two places — the frontmatter is normative. +- **Cite PRODUCT.md anti-references by name** in the Do's and Don'ts section. If PRODUCT.md lists "SaaS landing-page clichés" or "generic AI tool marketing" as anti-references, the DESIGN.md Don'ts should repeat those phrases verbatim so the visual spec enforces the strategic line. - **Match the spec, don't invent new sections.** The six section names are fixed. If you have Layout/Motion/Responsive content to document, fold it into Overview (philosophy-level rules) or Components (per-component behavior). - **Descriptive > technical**: "Gently curved edges (8px radius)" > "rounded-lg". Include the technical value in parens, lead with the description. - **Functional > decorative**: for each token, explain WHERE and WHY it's used, not just WHAT it is. @@ -365,3 +423,5 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an - Don't duplicate content from PRODUCT.md. DESIGN.md is strictly visual. - Don't add a "Layout Principles" or "Motion" or "Responsive Behavior" top-level section. The spec has six, not nine. Fold that content where it belongs. - Don't rename sections even slightly. "Colors" not "Color Palette & Roles". "Typography" not "Typography Rules". Tooling parsing depends on exact headers. +- Don't duplicate token values between frontmatter and prose. If a color is in `colors.primary` as hex, the prose can name it and describe its role but should not reassert a different hex. The frontmatter is normative. +- Don't invent frontmatter token groups outside Stitch's schema (no `motion:`, `breakpoints:`, `shadows:` at the top level). Stitch's Zod schema only accepts `colors`, `typography`, `rounded`, `spacing`, `components`. Anything else belongs in the sidecar's `extensions`. diff --git a/.cursor/skills/impeccable/scripts/design-parser.mjs b/.cursor/skills/impeccable/scripts/design-parser.mjs index 6220f67e7..b68756640 100644 --- a/.cursor/skills/impeccable/scripts/design-parser.mjs +++ b/.cursor/skills/impeccable/scripts/design-parser.mjs @@ -1,5 +1,10 @@ // Parse a DESIGN.md (Stitch-spec format) into a structured JSON model that // the live-mode design-system panel can render. Deterministic, dependency-free. +// +// Two-layer: YAML frontmatter (machine-readable tokens) + markdown body +// (prose with six canonical H2 sections). When frontmatter is present, it's +// exposed on `model.frontmatter` alongside the prose-scraped sections; +// consumers can prefer frontmatter values and fall back to prose. const CANONICAL_SECTIONS = [ 'Overview', @@ -10,6 +15,97 @@ const CANONICAL_SECTIONS = [ "Do's and Don'ts", ]; +// ---------- Frontmatter (Stitch YAML subset) ---------- + +function parseFrontmatter(md) { + const lines = md.split(/\r?\n/); + if (lines[0]?.trim() !== '---') return { frontmatter: null, body: md }; + + let end = -1; + for (let i = 1; i < lines.length; i++) { + if (lines[i].trim() === '---') { end = i; break; } + } + if (end === -1) return { frontmatter: null, body: md }; + + const yaml = lines.slice(1, end).join('\n'); + const body = lines.slice(end + 1).join('\n'); + try { + return { frontmatter: parseYamlSubset(yaml), body }; + } catch { + return { frontmatter: null, body: md }; + } +} + +// Minimal YAML reader for the Stitch frontmatter subset: scalar maps with +// one level of nested objects (typography roles, components). Indent-based, +// 2-space convention. No arrays, no anchors, no multi-line scalars — Stitch's +// schema doesn't need them and accepting them would require a real YAML +// dependency we don't want to vendor. +function parseYamlSubset(yaml) { + const lines = yaml.split(/\r?\n/); + const root = {}; + const stack = [{ indent: -1, obj: root }]; + + for (const raw of lines) { + // Skip blanks and line-only comments. Don't strip inline comments: + // unquoted hex values start with `#` and can't be safely distinguished + // from a comment after whitespace. + if (!raw.trim() || /^\s*#/.test(raw)) continue; + + const indent = raw.match(/^\s*/)[0].length; + const content = raw.slice(indent); + + const colonIdx = findTopLevelColon(content); + if (colonIdx === -1) continue; + + while (stack.length > 1 && stack[stack.length - 1].indent >= indent) { + stack.pop(); + } + + const key = content.slice(0, colonIdx).trim(); + const rest = content.slice(colonIdx + 1).trim(); + const parent = stack[stack.length - 1].obj; + + if (rest === '') { + const obj = {}; + parent[key] = obj; + stack.push({ indent, obj }); + } else { + parent[key] = parseScalar(rest); + } + } + + return root; +} + +function findTopLevelColon(s) { + let inQuote = null; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (inQuote) { + if (ch === inQuote && s[i - 1] !== '\\') inQuote = null; + } else if (ch === '"' || ch === "'") { + inQuote = ch; + } else if (ch === ':') { + return i; + } + } + return -1; +} + +function parseScalar(raw) { + const s = raw.trim(); + if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { + return s.slice(1, -1); + } + if (s === 'true') return true; + if (s === 'false') return false; + if (s === 'null' || s === '~') return null; + if (/^-?\d+$/.test(s)) return Number(s); + if (/^-?\d*\.\d+$/.test(s)) return Number(s); + return s; +} + const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g; const OKLCH_RE = /oklch\([^)]+\)/gi; const RGBA_RE = /rgba?\([^)]+\)/gi; @@ -706,10 +802,12 @@ function assessCoverage(model) { // ---------- Main ---------- export function parseDesignMd(md) { - const { title, sections } = splitSections(md); + const { frontmatter, body } = parseFrontmatter(md); + const { title, sections } = splitSections(body); return { - schemaVersion: 1, + schemaVersion: 2, title, + frontmatter, overview: extractOverview(sections['Overview']), colors: extractColors(sections['Colors']), typography: extractTypography(sections['Typography']), diff --git a/.cursor/skills/impeccable/scripts/live-browser.js b/.cursor/skills/impeccable/scripts/live-browser.js index 32ea9a191..d36af3536 100644 --- a/.cursor/skills/impeccable/scripts/live-browser.js +++ b/.cursor/skills/impeccable/scripts/live-browser.js @@ -3958,7 +3958,7 @@ void main() { function renderParsedMdCta() { const box = document.createElement('div'); box.className = 'parsed-md-cta'; - box.innerHTML = `Basic viewRunning /impeccable document generates DESIGN.json alongside your DESIGN.md, which lets this panel render your project's actual button, input, and nav primitives — not generic approximations.`; + box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running /impeccable document also generates a DESIGN.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; return box; } diff --git a/.gemini/skills/impeccable/reference/document.md b/.gemini/skills/impeccable/reference/document.md index 6f74e2aaa..fd3202ba3 100644 --- a/.gemini/skills/impeccable/reference/document.md +++ b/.gemini/skills/impeccable/reference/document.md @@ -1,8 +1,54 @@ Generate a `DESIGN.md` file at the project root that captures the current visual design system, so AI agents generating new screens stay on-brand. -DESIGN.md follows the [official Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/): exactly six sections in a fixed order. Sections may be omitted when not relevant, but **do not reorder them and do not rename them**. Section headers must match the spec character-for-character so the file stays parseable by other DESIGN.md-aware tools (Stitch itself, awesome-design-md, skill-rest, etc.). +DESIGN.md follows the [official Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/): YAML frontmatter carrying machine-readable design tokens, followed by a markdown body with exactly six sections in a fixed order. **Tokens are normative; prose provides context for how to apply them.** Sections may be omitted when not relevant, but **do not reorder them and do not rename them**. Section headers must match the spec character-for-character so the file stays parseable by other DESIGN.md-aware tools (Stitch itself, awesome-design-md, skill-rest, etc.). -## The six sections (exact order) +## The frontmatter: token schema + +The YAML frontmatter is the machine-readable layer. It's what Stitch's linter validates and what the live panel renders tiles from. Keep it tight; every entry should correspond to a token the project actually uses. + +```yaml +--- +name: +description: +colors: + primary: "#b8422e" + neutral-bg: "#faf7f2" + # ...one entry per extracted color; key = descriptive slug +typography: + display: + fontFamily: "Cormorant Garamond, Georgia, serif" + fontSize: "clamp(2.5rem, 7vw, 4.5rem)" + fontWeight: 300 + lineHeight: 1 + letterSpacing: "normal" + body: + # ... +rounded: + sm: "4px" + md: "8px" +spacing: + sm: "8px" + md: "16px" +components: + button-primary: + backgroundColor: "{colors.primary}" + textColor: "{colors.neutral-bg}" + rounded: "{rounded.sm}" + padding: "16px 48px" + button-primary-hover: + backgroundColor: "{colors.primary-deep}" +--- +``` + +Rules that matter: + +- **Token refs** use `{path.to.token}` (e.g. `{colors.primary}`, `{rounded.md}`). Components may reference primitives; primitives may not reference each other. +- **Stitch validates colors as hex sRGB only** (`#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`); OKLCH/HSL/P3 trigger a linter warning, not a hard error. YAML accepts the string either way and our own parser is format-agnostic. Choose based on project posture: (a) if the project has an "OKLCH-only" doctrine or uses Display-P3 values that don't round-trip through sRGB, put OKLCH directly in the frontmatter and accept the Stitch linter warning; (b) if the project wants strict Stitch compliance or plans to use their Tailwind/DTCG export pipeline, put hex in the frontmatter and keep OKLCH in prose as the canonical reference. Never split the source of truth without explicit reason. +- **Component sub-tokens** are limited to 8 props: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, `width`. Shadows, motion, focus rings, backdrop-filter — none of those fit. Carry them in the sidecar (Step 4b). +- **Scale keys are open-ended.** Use whatever names the project already uses (`warm-ash-cream`, `surface-container-low`). Don't rename to Material defaults. +- **Variants are naming convention, not schema.** `button-primary` / `button-primary-hover` / `button-primary-active` as sibling keys. + +## The markdown body: six sections (exact order) 1. `## Overview` 2. `## Colors` @@ -53,6 +99,17 @@ Build a structured draft from the discovered tokens. For each token class: - **Components**: For each common component (button, card, input, chip, list item, tooltip, nav), extract shape (radius), color assignment, hover/focus treatment, internal padding. - **Spacing + layout**: Fold into Overview or relevant Components. The spec does NOT have a Layout section. +### Step 2b: Stage the frontmatter + +From the auto-extracted tokens, draft the YAML frontmatter now (you'll write it at the top of DESIGN.md in Step 4). This is the machine-readable layer — what the live panel and Stitch's linter consume. + +- **Colors**: one entry per extracted color. Key = descriptive slug (`warm-ash-cream`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex — see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. +- **Typography**: one entry per role (`display`, `headline`, `title`, `body`, `label`). Typography is an object; include only the props that are real for the project (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`). +- **Rounded / Spacing**: whatever scale steps the project actually uses, keyed by whatever scale name the project uses (`sm` / `md` / `lg`, or `surface-sm`, or numeric steps). +- **Components**: one entry per variant (`button-primary`, `button-primary-hover`, `button-ghost`). Reference primitives via `{colors.X}`, `{rounded.Y}`. If a variant needs a property Stitch's 8-prop set doesn't cover (shadow, focus ring, backdrop-filter), carry the full snippet in the sidecar instead. + +Skip anything the project doesn't have. Empty scale keys or fabricated tokens pollute the spec. + ### Step 3: Ask the user for qualitative language The following require creative input that cannot be auto-extracted. Group them into one `AskUserQuestion` interaction: @@ -67,9 +124,16 @@ Quote a line from PRODUCT.md when possible so the user sees their own strategic ### Step 4: Write DESIGN.md -Use this exact structure. Headers must match character-for-character. Optional evocative subtitles (e.g. `## 2. Colors: The Coastal Palette`) are allowed. +The file opens with the YAML frontmatter staged in Step 2b (schema documented at the top of this reference), then the markdown body using the structure below. Headers must match character-for-character. Optional evocative subtitles (e.g. `## 2. Colors: The Coastal Palette`) are allowed. ```markdown +--- +name: [Project Title] +description: [one-line tagline] +colors: + # ... staged frontmatter from Step 2b +--- + # Design System: [Project Title] ## 1. Overview @@ -161,7 +225,7 @@ For each component, lead with a short character line, then specify shape, color ## 6. Do's and Don'ts -Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — include exact colors, pixel values, and named anti-patterns the user mentioned in PRODUCT.md. +Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — include exact colors, pixel values, and named anti-patterns the user mentioned in PRODUCT.md. **Every anti-reference in PRODUCT.md should show up here as a "Don't" with the same language**, so the visual spec carries the strategic line through. Quote PRODUCT.md directly where possible: if PRODUCT.md says *"avoid dark mode with purple gradients, neon accents, glassmorphism"*, the Don'ts here should repeat that by name. ### Do: - **Do** [specific prescription with exact values / named rule]. @@ -173,9 +237,9 @@ Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — i - **Don't** [...] ``` -### Step 4b: Write DESIGN.json sidecar +### Step 4b: Write DESIGN.json sidecar (extensions only) -After the Markdown is written, produce a machine-readable sidecar at `DESIGN.json` next to `DESIGN.md`. This powers the `/impeccable live` design-system panel, which renders a tile-based visualization of the system — color swatches with tonal ramps, Aa type specimens, live component previews. **The sidecar is how the panel shows *this project's* actual button/input/nav/card, not a generic approximation.** +The frontmatter owns token primitives (colors, typography, rounded, spacing, components). The sidecar at `DESIGN.json` carries **what Stitch's schema can't hold**: tonal ramps per color, shadow/elevation tokens, motion tokens, breakpoints, full component HTML/CSS snippets (the panel renders these into a shadow DOM), and narrative (north star, rules, do's/don'ts). It extends the frontmatter, it doesn't duplicate it. Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks to refresh the sidecar (e.g., from the live panel's stale-hint), preserve DESIGN.md and write only DESIGN.json. @@ -183,42 +247,32 @@ Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks ```json { - "schemaVersion": 1, + "schemaVersion": 2, "generatedAt": "ISO-8601 string", "title": "Design System: [Project Title]", - "tokens": { - "colors": [ - { - "role": "primary | secondary | tertiary | neutral | accent", - "name": "Descriptive Name", - "value": "#HEX or oklch(...) or rgba(...)", - "description": "Short role explanation (one sentence).", - "tonalRamp": ["...", "...", "..."] - } + "extensions": { + "colorMeta": { + "primary": { "role": "primary", "displayName": "Editorial Magenta", "canonical": "oklch(60% 0.25 350)", "tonalRamp": ["...", "...", "..."] }, + "warm-ash-cream": { "role": "neutral", "displayName": "Warm Ash Cream", "canonical": "oklch(96% 0.005 350)", "tonalRamp": ["...", "...", "..."] } + }, + "typographyMeta": { + "display": { "displayName": "Display", "purpose": "Hero headlines only." } + }, + "shadows": [ + { "name": "ambient-low", "value": "0 4px 24px rgba(0,0,0,0.12)", "purpose": "Diffuse hover glow under accent elements." } ], - "typography": [ - { - "role": "display | headline | title | body | label | mono", - "name": "Display", - "family": "Cormorant Garamond", - "fallback": "Georgia, serif", - "weight": 300, - "style": "normal | italic", - "sampleSize": "clamp(2.5rem, 7vw, 4.5rem) or 1rem", - "lineHeight": "1 | 1.2 | 1.6", - "letterSpacing": "normal | 0.05em", - "textTransform": "none | uppercase", - "purpose": "Short description of where this role is used." - } + "motion": [ + { "name": "ease-standard", "value": "cubic-bezier(0.4, 0, 0.2, 1)", "purpose": "Default easing for state transitions." } ], - "radii": [{ "name": "sm|md|lg|xl|full", "value": "4px" }], - "shadows": [{ "name": "Descriptive Name", "value": "0 4px 24px rgba(0,0,0,0.12)", "purpose": "..." }], - "spacing": [{ "name": "xs|sm|md|lg|xl|2xl|3xl", "value": "8px" }] + "breakpoints": [ + { "name": "sm", "value": "640px" } + ] }, "components": [ { "name": "Primary Button", "kind": "button | input | nav | chip | card | custom", + "refersTo": "button-primary", "description": "One-line what and when.", "html": "", "css": ".ds-btn-primary { background: #191c1d; color: #fff; padding: 16px 48px; letter-spacing: 0.05em; text-transform: uppercase; font-weight: 500; border: none; border-radius: 0; transition: background 0.2s, transform 0.2s; } .ds-btn-primary:hover { background: oklch(60% 0.25 350); transform: translateY(-2px); }" @@ -235,6 +289,8 @@ Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks } ``` +**What changed from schemaVersion 1.** The old sidecar carried token primitive arrays (`tokens.colors[]`, `tokens.typography[]`, etc.). Those values now live in the frontmatter. The sidecar only carries metadata that can't live in the frontmatter — tonal ramps, canonical OKLCH when the hex is an approximation, display names, role hints — keyed by the frontmatter token name (`colorMeta.`, `typographyMeta.`). Components still carry full HTML/CSS because Stitch's 8-prop set can't hold them. + #### Component translation rules The `html` and `css` fields must be **self-contained, drop-in snippets** that render correctly when injected into a shadow DOM. The panel applies them directly — no post-processing, no framework runtime. @@ -336,7 +392,7 @@ Per-section guidance in seed mode: - **Components**: omit entirely — no components exist yet. - **Do's and Don'ts**: carry PRODUCT.md's anti-references directly plus the anti-reference named in Q5. -Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens and components to render; there is nothing to show yet. The sidecar gets generated on the next Scan-mode run. +Seed mode writes a minimal frontmatter with `name` and `description` only — no colors, typography, rounded, spacing, or components yet. Real tokens land on the next Scan-mode run. Skip the `DESIGN.json` sidecar in seed mode for the same reason: nothing to render. ### Step 4: Confirm and refresh session cache @@ -346,6 +402,8 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an ## Style guidelines +- **Frontmatter first, prose second.** Tokens go in the YAML frontmatter; prose contextualizes them. Don't redefine a token value in two places — the frontmatter is normative. +- **Cite PRODUCT.md anti-references by name** in the Do's and Don'ts section. If PRODUCT.md lists "SaaS landing-page clichés" or "generic AI tool marketing" as anti-references, the DESIGN.md Don'ts should repeat those phrases verbatim so the visual spec enforces the strategic line. - **Match the spec, don't invent new sections.** The six section names are fixed. If you have Layout/Motion/Responsive content to document, fold it into Overview (philosophy-level rules) or Components (per-component behavior). - **Descriptive > technical**: "Gently curved edges (8px radius)" > "rounded-lg". Include the technical value in parens, lead with the description. - **Functional > decorative**: for each token, explain WHERE and WHY it's used, not just WHAT it is. @@ -365,3 +423,5 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an - Don't duplicate content from PRODUCT.md. DESIGN.md is strictly visual. - Don't add a "Layout Principles" or "Motion" or "Responsive Behavior" top-level section. The spec has six, not nine. Fold that content where it belongs. - Don't rename sections even slightly. "Colors" not "Color Palette & Roles". "Typography" not "Typography Rules". Tooling parsing depends on exact headers. +- Don't duplicate token values between frontmatter and prose. If a color is in `colors.primary` as hex, the prose can name it and describe its role but should not reassert a different hex. The frontmatter is normative. +- Don't invent frontmatter token groups outside Stitch's schema (no `motion:`, `breakpoints:`, `shadows:` at the top level). Stitch's Zod schema only accepts `colors`, `typography`, `rounded`, `spacing`, `components`. Anything else belongs in the sidecar's `extensions`. diff --git a/.gemini/skills/impeccable/scripts/design-parser.mjs b/.gemini/skills/impeccable/scripts/design-parser.mjs index 6220f67e7..b68756640 100644 --- a/.gemini/skills/impeccable/scripts/design-parser.mjs +++ b/.gemini/skills/impeccable/scripts/design-parser.mjs @@ -1,5 +1,10 @@ // Parse a DESIGN.md (Stitch-spec format) into a structured JSON model that // the live-mode design-system panel can render. Deterministic, dependency-free. +// +// Two-layer: YAML frontmatter (machine-readable tokens) + markdown body +// (prose with six canonical H2 sections). When frontmatter is present, it's +// exposed on `model.frontmatter` alongside the prose-scraped sections; +// consumers can prefer frontmatter values and fall back to prose. const CANONICAL_SECTIONS = [ 'Overview', @@ -10,6 +15,97 @@ const CANONICAL_SECTIONS = [ "Do's and Don'ts", ]; +// ---------- Frontmatter (Stitch YAML subset) ---------- + +function parseFrontmatter(md) { + const lines = md.split(/\r?\n/); + if (lines[0]?.trim() !== '---') return { frontmatter: null, body: md }; + + let end = -1; + for (let i = 1; i < lines.length; i++) { + if (lines[i].trim() === '---') { end = i; break; } + } + if (end === -1) return { frontmatter: null, body: md }; + + const yaml = lines.slice(1, end).join('\n'); + const body = lines.slice(end + 1).join('\n'); + try { + return { frontmatter: parseYamlSubset(yaml), body }; + } catch { + return { frontmatter: null, body: md }; + } +} + +// Minimal YAML reader for the Stitch frontmatter subset: scalar maps with +// one level of nested objects (typography roles, components). Indent-based, +// 2-space convention. No arrays, no anchors, no multi-line scalars — Stitch's +// schema doesn't need them and accepting them would require a real YAML +// dependency we don't want to vendor. +function parseYamlSubset(yaml) { + const lines = yaml.split(/\r?\n/); + const root = {}; + const stack = [{ indent: -1, obj: root }]; + + for (const raw of lines) { + // Skip blanks and line-only comments. Don't strip inline comments: + // unquoted hex values start with `#` and can't be safely distinguished + // from a comment after whitespace. + if (!raw.trim() || /^\s*#/.test(raw)) continue; + + const indent = raw.match(/^\s*/)[0].length; + const content = raw.slice(indent); + + const colonIdx = findTopLevelColon(content); + if (colonIdx === -1) continue; + + while (stack.length > 1 && stack[stack.length - 1].indent >= indent) { + stack.pop(); + } + + const key = content.slice(0, colonIdx).trim(); + const rest = content.slice(colonIdx + 1).trim(); + const parent = stack[stack.length - 1].obj; + + if (rest === '') { + const obj = {}; + parent[key] = obj; + stack.push({ indent, obj }); + } else { + parent[key] = parseScalar(rest); + } + } + + return root; +} + +function findTopLevelColon(s) { + let inQuote = null; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (inQuote) { + if (ch === inQuote && s[i - 1] !== '\\') inQuote = null; + } else if (ch === '"' || ch === "'") { + inQuote = ch; + } else if (ch === ':') { + return i; + } + } + return -1; +} + +function parseScalar(raw) { + const s = raw.trim(); + if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { + return s.slice(1, -1); + } + if (s === 'true') return true; + if (s === 'false') return false; + if (s === 'null' || s === '~') return null; + if (/^-?\d+$/.test(s)) return Number(s); + if (/^-?\d*\.\d+$/.test(s)) return Number(s); + return s; +} + const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g; const OKLCH_RE = /oklch\([^)]+\)/gi; const RGBA_RE = /rgba?\([^)]+\)/gi; @@ -706,10 +802,12 @@ function assessCoverage(model) { // ---------- Main ---------- export function parseDesignMd(md) { - const { title, sections } = splitSections(md); + const { frontmatter, body } = parseFrontmatter(md); + const { title, sections } = splitSections(body); return { - schemaVersion: 1, + schemaVersion: 2, title, + frontmatter, overview: extractOverview(sections['Overview']), colors: extractColors(sections['Colors']), typography: extractTypography(sections['Typography']), diff --git a/.gemini/skills/impeccable/scripts/live-browser.js b/.gemini/skills/impeccable/scripts/live-browser.js index 32ea9a191..d36af3536 100644 --- a/.gemini/skills/impeccable/scripts/live-browser.js +++ b/.gemini/skills/impeccable/scripts/live-browser.js @@ -3958,7 +3958,7 @@ void main() { function renderParsedMdCta() { const box = document.createElement('div'); box.className = 'parsed-md-cta'; - box.innerHTML = `Basic viewRunning /impeccable document generates DESIGN.json alongside your DESIGN.md, which lets this panel render your project's actual button, input, and nav primitives — not generic approximations.`; + box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running /impeccable document also generates a DESIGN.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; return box; } diff --git a/.github/skills/impeccable/reference/document.md b/.github/skills/impeccable/reference/document.md index 361ccfba3..049100871 100644 --- a/.github/skills/impeccable/reference/document.md +++ b/.github/skills/impeccable/reference/document.md @@ -1,8 +1,54 @@ Generate a `DESIGN.md` file at the project root that captures the current visual design system, so AI agents generating new screens stay on-brand. -DESIGN.md follows the [official Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/): exactly six sections in a fixed order. Sections may be omitted when not relevant, but **do not reorder them and do not rename them**. Section headers must match the spec character-for-character so the file stays parseable by other DESIGN.md-aware tools (Stitch itself, awesome-design-md, skill-rest, etc.). +DESIGN.md follows the [official Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/): YAML frontmatter carrying machine-readable design tokens, followed by a markdown body with exactly six sections in a fixed order. **Tokens are normative; prose provides context for how to apply them.** Sections may be omitted when not relevant, but **do not reorder them and do not rename them**. Section headers must match the spec character-for-character so the file stays parseable by other DESIGN.md-aware tools (Stitch itself, awesome-design-md, skill-rest, etc.). -## The six sections (exact order) +## The frontmatter: token schema + +The YAML frontmatter is the machine-readable layer. It's what Stitch's linter validates and what the live panel renders tiles from. Keep it tight; every entry should correspond to a token the project actually uses. + +```yaml +--- +name: +description: +colors: + primary: "#b8422e" + neutral-bg: "#faf7f2" + # ...one entry per extracted color; key = descriptive slug +typography: + display: + fontFamily: "Cormorant Garamond, Georgia, serif" + fontSize: "clamp(2.5rem, 7vw, 4.5rem)" + fontWeight: 300 + lineHeight: 1 + letterSpacing: "normal" + body: + # ... +rounded: + sm: "4px" + md: "8px" +spacing: + sm: "8px" + md: "16px" +components: + button-primary: + backgroundColor: "{colors.primary}" + textColor: "{colors.neutral-bg}" + rounded: "{rounded.sm}" + padding: "16px 48px" + button-primary-hover: + backgroundColor: "{colors.primary-deep}" +--- +``` + +Rules that matter: + +- **Token refs** use `{path.to.token}` (e.g. `{colors.primary}`, `{rounded.md}`). Components may reference primitives; primitives may not reference each other. +- **Stitch validates colors as hex sRGB only** (`#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`); OKLCH/HSL/P3 trigger a linter warning, not a hard error. YAML accepts the string either way and our own parser is format-agnostic. Choose based on project posture: (a) if the project has an "OKLCH-only" doctrine or uses Display-P3 values that don't round-trip through sRGB, put OKLCH directly in the frontmatter and accept the Stitch linter warning; (b) if the project wants strict Stitch compliance or plans to use their Tailwind/DTCG export pipeline, put hex in the frontmatter and keep OKLCH in prose as the canonical reference. Never split the source of truth without explicit reason. +- **Component sub-tokens** are limited to 8 props: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, `width`. Shadows, motion, focus rings, backdrop-filter — none of those fit. Carry them in the sidecar (Step 4b). +- **Scale keys are open-ended.** Use whatever names the project already uses (`warm-ash-cream`, `surface-container-low`). Don't rename to Material defaults. +- **Variants are naming convention, not schema.** `button-primary` / `button-primary-hover` / `button-primary-active` as sibling keys. + +## The markdown body: six sections (exact order) 1. `## Overview` 2. `## Colors` @@ -53,6 +99,17 @@ Build a structured draft from the discovered tokens. For each token class: - **Components**: For each common component (button, card, input, chip, list item, tooltip, nav), extract shape (radius), color assignment, hover/focus treatment, internal padding. - **Spacing + layout**: Fold into Overview or relevant Components. The spec does NOT have a Layout section. +### Step 2b: Stage the frontmatter + +From the auto-extracted tokens, draft the YAML frontmatter now (you'll write it at the top of DESIGN.md in Step 4). This is the machine-readable layer — what the live panel and Stitch's linter consume. + +- **Colors**: one entry per extracted color. Key = descriptive slug (`warm-ash-cream`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex — see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. +- **Typography**: one entry per role (`display`, `headline`, `title`, `body`, `label`). Typography is an object; include only the props that are real for the project (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`). +- **Rounded / Spacing**: whatever scale steps the project actually uses, keyed by whatever scale name the project uses (`sm` / `md` / `lg`, or `surface-sm`, or numeric steps). +- **Components**: one entry per variant (`button-primary`, `button-primary-hover`, `button-ghost`). Reference primitives via `{colors.X}`, `{rounded.Y}`. If a variant needs a property Stitch's 8-prop set doesn't cover (shadow, focus ring, backdrop-filter), carry the full snippet in the sidecar instead. + +Skip anything the project doesn't have. Empty scale keys or fabricated tokens pollute the spec. + ### Step 3: Ask the user for qualitative language The following require creative input that cannot be auto-extracted. Group them into one `AskUserQuestion` interaction: @@ -67,9 +124,16 @@ Quote a line from PRODUCT.md when possible so the user sees their own strategic ### Step 4: Write DESIGN.md -Use this exact structure. Headers must match character-for-character. Optional evocative subtitles (e.g. `## 2. Colors: The Coastal Palette`) are allowed. +The file opens with the YAML frontmatter staged in Step 2b (schema documented at the top of this reference), then the markdown body using the structure below. Headers must match character-for-character. Optional evocative subtitles (e.g. `## 2. Colors: The Coastal Palette`) are allowed. ```markdown +--- +name: [Project Title] +description: [one-line tagline] +colors: + # ... staged frontmatter from Step 2b +--- + # Design System: [Project Title] ## 1. Overview @@ -161,7 +225,7 @@ For each component, lead with a short character line, then specify shape, color ## 6. Do's and Don'ts -Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — include exact colors, pixel values, and named anti-patterns the user mentioned in PRODUCT.md. +Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — include exact colors, pixel values, and named anti-patterns the user mentioned in PRODUCT.md. **Every anti-reference in PRODUCT.md should show up here as a "Don't" with the same language**, so the visual spec carries the strategic line through. Quote PRODUCT.md directly where possible: if PRODUCT.md says *"avoid dark mode with purple gradients, neon accents, glassmorphism"*, the Don'ts here should repeat that by name. ### Do: - **Do** [specific prescription with exact values / named rule]. @@ -173,9 +237,9 @@ Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — i - **Don't** [...] ``` -### Step 4b: Write DESIGN.json sidecar +### Step 4b: Write DESIGN.json sidecar (extensions only) -After the Markdown is written, produce a machine-readable sidecar at `DESIGN.json` next to `DESIGN.md`. This powers the `/impeccable live` design-system panel, which renders a tile-based visualization of the system — color swatches with tonal ramps, Aa type specimens, live component previews. **The sidecar is how the panel shows *this project's* actual button/input/nav/card, not a generic approximation.** +The frontmatter owns token primitives (colors, typography, rounded, spacing, components). The sidecar at `DESIGN.json` carries **what Stitch's schema can't hold**: tonal ramps per color, shadow/elevation tokens, motion tokens, breakpoints, full component HTML/CSS snippets (the panel renders these into a shadow DOM), and narrative (north star, rules, do's/don'ts). It extends the frontmatter, it doesn't duplicate it. Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks to refresh the sidecar (e.g., from the live panel's stale-hint), preserve DESIGN.md and write only DESIGN.json. @@ -183,42 +247,32 @@ Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks ```json { - "schemaVersion": 1, + "schemaVersion": 2, "generatedAt": "ISO-8601 string", "title": "Design System: [Project Title]", - "tokens": { - "colors": [ - { - "role": "primary | secondary | tertiary | neutral | accent", - "name": "Descriptive Name", - "value": "#HEX or oklch(...) or rgba(...)", - "description": "Short role explanation (one sentence).", - "tonalRamp": ["...", "...", "..."] - } + "extensions": { + "colorMeta": { + "primary": { "role": "primary", "displayName": "Editorial Magenta", "canonical": "oklch(60% 0.25 350)", "tonalRamp": ["...", "...", "..."] }, + "warm-ash-cream": { "role": "neutral", "displayName": "Warm Ash Cream", "canonical": "oklch(96% 0.005 350)", "tonalRamp": ["...", "...", "..."] } + }, + "typographyMeta": { + "display": { "displayName": "Display", "purpose": "Hero headlines only." } + }, + "shadows": [ + { "name": "ambient-low", "value": "0 4px 24px rgba(0,0,0,0.12)", "purpose": "Diffuse hover glow under accent elements." } ], - "typography": [ - { - "role": "display | headline | title | body | label | mono", - "name": "Display", - "family": "Cormorant Garamond", - "fallback": "Georgia, serif", - "weight": 300, - "style": "normal | italic", - "sampleSize": "clamp(2.5rem, 7vw, 4.5rem) or 1rem", - "lineHeight": "1 | 1.2 | 1.6", - "letterSpacing": "normal | 0.05em", - "textTransform": "none | uppercase", - "purpose": "Short description of where this role is used." - } + "motion": [ + { "name": "ease-standard", "value": "cubic-bezier(0.4, 0, 0.2, 1)", "purpose": "Default easing for state transitions." } ], - "radii": [{ "name": "sm|md|lg|xl|full", "value": "4px" }], - "shadows": [{ "name": "Descriptive Name", "value": "0 4px 24px rgba(0,0,0,0.12)", "purpose": "..." }], - "spacing": [{ "name": "xs|sm|md|lg|xl|2xl|3xl", "value": "8px" }] + "breakpoints": [ + { "name": "sm", "value": "640px" } + ] }, "components": [ { "name": "Primary Button", "kind": "button | input | nav | chip | card | custom", + "refersTo": "button-primary", "description": "One-line what and when.", "html": "", "css": ".ds-btn-primary { background: #191c1d; color: #fff; padding: 16px 48px; letter-spacing: 0.05em; text-transform: uppercase; font-weight: 500; border: none; border-radius: 0; transition: background 0.2s, transform 0.2s; } .ds-btn-primary:hover { background: oklch(60% 0.25 350); transform: translateY(-2px); }" @@ -235,6 +289,8 @@ Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks } ``` +**What changed from schemaVersion 1.** The old sidecar carried token primitive arrays (`tokens.colors[]`, `tokens.typography[]`, etc.). Those values now live in the frontmatter. The sidecar only carries metadata that can't live in the frontmatter — tonal ramps, canonical OKLCH when the hex is an approximation, display names, role hints — keyed by the frontmatter token name (`colorMeta.`, `typographyMeta.`). Components still carry full HTML/CSS because Stitch's 8-prop set can't hold them. + #### Component translation rules The `html` and `css` fields must be **self-contained, drop-in snippets** that render correctly when injected into a shadow DOM. The panel applies them directly — no post-processing, no framework runtime. @@ -336,7 +392,7 @@ Per-section guidance in seed mode: - **Components**: omit entirely — no components exist yet. - **Do's and Don'ts**: carry PRODUCT.md's anti-references directly plus the anti-reference named in Q5. -Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens and components to render; there is nothing to show yet. The sidecar gets generated on the next Scan-mode run. +Seed mode writes a minimal frontmatter with `name` and `description` only — no colors, typography, rounded, spacing, or components yet. Real tokens land on the next Scan-mode run. Skip the `DESIGN.json` sidecar in seed mode for the same reason: nothing to render. ### Step 4: Confirm and refresh session cache @@ -346,6 +402,8 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an ## Style guidelines +- **Frontmatter first, prose second.** Tokens go in the YAML frontmatter; prose contextualizes them. Don't redefine a token value in two places — the frontmatter is normative. +- **Cite PRODUCT.md anti-references by name** in the Do's and Don'ts section. If PRODUCT.md lists "SaaS landing-page clichés" or "generic AI tool marketing" as anti-references, the DESIGN.md Don'ts should repeat those phrases verbatim so the visual spec enforces the strategic line. - **Match the spec, don't invent new sections.** The six section names are fixed. If you have Layout/Motion/Responsive content to document, fold it into Overview (philosophy-level rules) or Components (per-component behavior). - **Descriptive > technical**: "Gently curved edges (8px radius)" > "rounded-lg". Include the technical value in parens, lead with the description. - **Functional > decorative**: for each token, explain WHERE and WHY it's used, not just WHAT it is. @@ -365,3 +423,5 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an - Don't duplicate content from PRODUCT.md. DESIGN.md is strictly visual. - Don't add a "Layout Principles" or "Motion" or "Responsive Behavior" top-level section. The spec has six, not nine. Fold that content where it belongs. - Don't rename sections even slightly. "Colors" not "Color Palette & Roles". "Typography" not "Typography Rules". Tooling parsing depends on exact headers. +- Don't duplicate token values between frontmatter and prose. If a color is in `colors.primary` as hex, the prose can name it and describe its role but should not reassert a different hex. The frontmatter is normative. +- Don't invent frontmatter token groups outside Stitch's schema (no `motion:`, `breakpoints:`, `shadows:` at the top level). Stitch's Zod schema only accepts `colors`, `typography`, `rounded`, `spacing`, `components`. Anything else belongs in the sidecar's `extensions`. diff --git a/.github/skills/impeccable/scripts/design-parser.mjs b/.github/skills/impeccable/scripts/design-parser.mjs index 6220f67e7..b68756640 100644 --- a/.github/skills/impeccable/scripts/design-parser.mjs +++ b/.github/skills/impeccable/scripts/design-parser.mjs @@ -1,5 +1,10 @@ // Parse a DESIGN.md (Stitch-spec format) into a structured JSON model that // the live-mode design-system panel can render. Deterministic, dependency-free. +// +// Two-layer: YAML frontmatter (machine-readable tokens) + markdown body +// (prose with six canonical H2 sections). When frontmatter is present, it's +// exposed on `model.frontmatter` alongside the prose-scraped sections; +// consumers can prefer frontmatter values and fall back to prose. const CANONICAL_SECTIONS = [ 'Overview', @@ -10,6 +15,97 @@ const CANONICAL_SECTIONS = [ "Do's and Don'ts", ]; +// ---------- Frontmatter (Stitch YAML subset) ---------- + +function parseFrontmatter(md) { + const lines = md.split(/\r?\n/); + if (lines[0]?.trim() !== '---') return { frontmatter: null, body: md }; + + let end = -1; + for (let i = 1; i < lines.length; i++) { + if (lines[i].trim() === '---') { end = i; break; } + } + if (end === -1) return { frontmatter: null, body: md }; + + const yaml = lines.slice(1, end).join('\n'); + const body = lines.slice(end + 1).join('\n'); + try { + return { frontmatter: parseYamlSubset(yaml), body }; + } catch { + return { frontmatter: null, body: md }; + } +} + +// Minimal YAML reader for the Stitch frontmatter subset: scalar maps with +// one level of nested objects (typography roles, components). Indent-based, +// 2-space convention. No arrays, no anchors, no multi-line scalars — Stitch's +// schema doesn't need them and accepting them would require a real YAML +// dependency we don't want to vendor. +function parseYamlSubset(yaml) { + const lines = yaml.split(/\r?\n/); + const root = {}; + const stack = [{ indent: -1, obj: root }]; + + for (const raw of lines) { + // Skip blanks and line-only comments. Don't strip inline comments: + // unquoted hex values start with `#` and can't be safely distinguished + // from a comment after whitespace. + if (!raw.trim() || /^\s*#/.test(raw)) continue; + + const indent = raw.match(/^\s*/)[0].length; + const content = raw.slice(indent); + + const colonIdx = findTopLevelColon(content); + if (colonIdx === -1) continue; + + while (stack.length > 1 && stack[stack.length - 1].indent >= indent) { + stack.pop(); + } + + const key = content.slice(0, colonIdx).trim(); + const rest = content.slice(colonIdx + 1).trim(); + const parent = stack[stack.length - 1].obj; + + if (rest === '') { + const obj = {}; + parent[key] = obj; + stack.push({ indent, obj }); + } else { + parent[key] = parseScalar(rest); + } + } + + return root; +} + +function findTopLevelColon(s) { + let inQuote = null; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (inQuote) { + if (ch === inQuote && s[i - 1] !== '\\') inQuote = null; + } else if (ch === '"' || ch === "'") { + inQuote = ch; + } else if (ch === ':') { + return i; + } + } + return -1; +} + +function parseScalar(raw) { + const s = raw.trim(); + if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { + return s.slice(1, -1); + } + if (s === 'true') return true; + if (s === 'false') return false; + if (s === 'null' || s === '~') return null; + if (/^-?\d+$/.test(s)) return Number(s); + if (/^-?\d*\.\d+$/.test(s)) return Number(s); + return s; +} + const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g; const OKLCH_RE = /oklch\([^)]+\)/gi; const RGBA_RE = /rgba?\([^)]+\)/gi; @@ -706,10 +802,12 @@ function assessCoverage(model) { // ---------- Main ---------- export function parseDesignMd(md) { - const { title, sections } = splitSections(md); + const { frontmatter, body } = parseFrontmatter(md); + const { title, sections } = splitSections(body); return { - schemaVersion: 1, + schemaVersion: 2, title, + frontmatter, overview: extractOverview(sections['Overview']), colors: extractColors(sections['Colors']), typography: extractTypography(sections['Typography']), diff --git a/.github/skills/impeccable/scripts/live-browser.js b/.github/skills/impeccable/scripts/live-browser.js index 32ea9a191..d36af3536 100644 --- a/.github/skills/impeccable/scripts/live-browser.js +++ b/.github/skills/impeccable/scripts/live-browser.js @@ -3958,7 +3958,7 @@ void main() { function renderParsedMdCta() { const box = document.createElement('div'); box.className = 'parsed-md-cta'; - box.innerHTML = `Basic viewRunning /impeccable document generates DESIGN.json alongside your DESIGN.md, which lets this panel render your project's actual button, input, and nav primitives — not generic approximations.`; + box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running /impeccable document also generates a DESIGN.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; return box; } diff --git a/.kiro/skills/impeccable/reference/document.md b/.kiro/skills/impeccable/reference/document.md index 14058d733..dde6f7bcc 100644 --- a/.kiro/skills/impeccable/reference/document.md +++ b/.kiro/skills/impeccable/reference/document.md @@ -1,8 +1,54 @@ Generate a `DESIGN.md` file at the project root that captures the current visual design system, so AI agents generating new screens stay on-brand. -DESIGN.md follows the [official Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/): exactly six sections in a fixed order. Sections may be omitted when not relevant, but **do not reorder them and do not rename them**. Section headers must match the spec character-for-character so the file stays parseable by other DESIGN.md-aware tools (Stitch itself, awesome-design-md, skill-rest, etc.). +DESIGN.md follows the [official Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/): YAML frontmatter carrying machine-readable design tokens, followed by a markdown body with exactly six sections in a fixed order. **Tokens are normative; prose provides context for how to apply them.** Sections may be omitted when not relevant, but **do not reorder them and do not rename them**. Section headers must match the spec character-for-character so the file stays parseable by other DESIGN.md-aware tools (Stitch itself, awesome-design-md, skill-rest, etc.). -## The six sections (exact order) +## The frontmatter: token schema + +The YAML frontmatter is the machine-readable layer. It's what Stitch's linter validates and what the live panel renders tiles from. Keep it tight; every entry should correspond to a token the project actually uses. + +```yaml +--- +name: +description: +colors: + primary: "#b8422e" + neutral-bg: "#faf7f2" + # ...one entry per extracted color; key = descriptive slug +typography: + display: + fontFamily: "Cormorant Garamond, Georgia, serif" + fontSize: "clamp(2.5rem, 7vw, 4.5rem)" + fontWeight: 300 + lineHeight: 1 + letterSpacing: "normal" + body: + # ... +rounded: + sm: "4px" + md: "8px" +spacing: + sm: "8px" + md: "16px" +components: + button-primary: + backgroundColor: "{colors.primary}" + textColor: "{colors.neutral-bg}" + rounded: "{rounded.sm}" + padding: "16px 48px" + button-primary-hover: + backgroundColor: "{colors.primary-deep}" +--- +``` + +Rules that matter: + +- **Token refs** use `{path.to.token}` (e.g. `{colors.primary}`, `{rounded.md}`). Components may reference primitives; primitives may not reference each other. +- **Stitch validates colors as hex sRGB only** (`#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`); OKLCH/HSL/P3 trigger a linter warning, not a hard error. YAML accepts the string either way and our own parser is format-agnostic. Choose based on project posture: (a) if the project has an "OKLCH-only" doctrine or uses Display-P3 values that don't round-trip through sRGB, put OKLCH directly in the frontmatter and accept the Stitch linter warning; (b) if the project wants strict Stitch compliance or plans to use their Tailwind/DTCG export pipeline, put hex in the frontmatter and keep OKLCH in prose as the canonical reference. Never split the source of truth without explicit reason. +- **Component sub-tokens** are limited to 8 props: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, `width`. Shadows, motion, focus rings, backdrop-filter — none of those fit. Carry them in the sidecar (Step 4b). +- **Scale keys are open-ended.** Use whatever names the project already uses (`warm-ash-cream`, `surface-container-low`). Don't rename to Material defaults. +- **Variants are naming convention, not schema.** `button-primary` / `button-primary-hover` / `button-primary-active` as sibling keys. + +## The markdown body: six sections (exact order) 1. `## Overview` 2. `## Colors` @@ -53,6 +99,17 @@ Build a structured draft from the discovered tokens. For each token class: - **Components**: For each common component (button, card, input, chip, list item, tooltip, nav), extract shape (radius), color assignment, hover/focus treatment, internal padding. - **Spacing + layout**: Fold into Overview or relevant Components. The spec does NOT have a Layout section. +### Step 2b: Stage the frontmatter + +From the auto-extracted tokens, draft the YAML frontmatter now (you'll write it at the top of DESIGN.md in Step 4). This is the machine-readable layer — what the live panel and Stitch's linter consume. + +- **Colors**: one entry per extracted color. Key = descriptive slug (`warm-ash-cream`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex — see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. +- **Typography**: one entry per role (`display`, `headline`, `title`, `body`, `label`). Typography is an object; include only the props that are real for the project (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`). +- **Rounded / Spacing**: whatever scale steps the project actually uses, keyed by whatever scale name the project uses (`sm` / `md` / `lg`, or `surface-sm`, or numeric steps). +- **Components**: one entry per variant (`button-primary`, `button-primary-hover`, `button-ghost`). Reference primitives via `{colors.X}`, `{rounded.Y}`. If a variant needs a property Stitch's 8-prop set doesn't cover (shadow, focus ring, backdrop-filter), carry the full snippet in the sidecar instead. + +Skip anything the project doesn't have. Empty scale keys or fabricated tokens pollute the spec. + ### Step 3: Ask the user for qualitative language The following require creative input that cannot be auto-extracted. Group them into one `AskUserQuestion` interaction: @@ -67,9 +124,16 @@ Quote a line from PRODUCT.md when possible so the user sees their own strategic ### Step 4: Write DESIGN.md -Use this exact structure. Headers must match character-for-character. Optional evocative subtitles (e.g. `## 2. Colors: The Coastal Palette`) are allowed. +The file opens with the YAML frontmatter staged in Step 2b (schema documented at the top of this reference), then the markdown body using the structure below. Headers must match character-for-character. Optional evocative subtitles (e.g. `## 2. Colors: The Coastal Palette`) are allowed. ```markdown +--- +name: [Project Title] +description: [one-line tagline] +colors: + # ... staged frontmatter from Step 2b +--- + # Design System: [Project Title] ## 1. Overview @@ -161,7 +225,7 @@ For each component, lead with a short character line, then specify shape, color ## 6. Do's and Don'ts -Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — include exact colors, pixel values, and named anti-patterns the user mentioned in PRODUCT.md. +Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — include exact colors, pixel values, and named anti-patterns the user mentioned in PRODUCT.md. **Every anti-reference in PRODUCT.md should show up here as a "Don't" with the same language**, so the visual spec carries the strategic line through. Quote PRODUCT.md directly where possible: if PRODUCT.md says *"avoid dark mode with purple gradients, neon accents, glassmorphism"*, the Don'ts here should repeat that by name. ### Do: - **Do** [specific prescription with exact values / named rule]. @@ -173,9 +237,9 @@ Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — i - **Don't** [...] ``` -### Step 4b: Write DESIGN.json sidecar +### Step 4b: Write DESIGN.json sidecar (extensions only) -After the Markdown is written, produce a machine-readable sidecar at `DESIGN.json` next to `DESIGN.md`. This powers the `/impeccable live` design-system panel, which renders a tile-based visualization of the system — color swatches with tonal ramps, Aa type specimens, live component previews. **The sidecar is how the panel shows *this project's* actual button/input/nav/card, not a generic approximation.** +The frontmatter owns token primitives (colors, typography, rounded, spacing, components). The sidecar at `DESIGN.json` carries **what Stitch's schema can't hold**: tonal ramps per color, shadow/elevation tokens, motion tokens, breakpoints, full component HTML/CSS snippets (the panel renders these into a shadow DOM), and narrative (north star, rules, do's/don'ts). It extends the frontmatter, it doesn't duplicate it. Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks to refresh the sidecar (e.g., from the live panel's stale-hint), preserve DESIGN.md and write only DESIGN.json. @@ -183,42 +247,32 @@ Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks ```json { - "schemaVersion": 1, + "schemaVersion": 2, "generatedAt": "ISO-8601 string", "title": "Design System: [Project Title]", - "tokens": { - "colors": [ - { - "role": "primary | secondary | tertiary | neutral | accent", - "name": "Descriptive Name", - "value": "#HEX or oklch(...) or rgba(...)", - "description": "Short role explanation (one sentence).", - "tonalRamp": ["...", "...", "..."] - } + "extensions": { + "colorMeta": { + "primary": { "role": "primary", "displayName": "Editorial Magenta", "canonical": "oklch(60% 0.25 350)", "tonalRamp": ["...", "...", "..."] }, + "warm-ash-cream": { "role": "neutral", "displayName": "Warm Ash Cream", "canonical": "oklch(96% 0.005 350)", "tonalRamp": ["...", "...", "..."] } + }, + "typographyMeta": { + "display": { "displayName": "Display", "purpose": "Hero headlines only." } + }, + "shadows": [ + { "name": "ambient-low", "value": "0 4px 24px rgba(0,0,0,0.12)", "purpose": "Diffuse hover glow under accent elements." } ], - "typography": [ - { - "role": "display | headline | title | body | label | mono", - "name": "Display", - "family": "Cormorant Garamond", - "fallback": "Georgia, serif", - "weight": 300, - "style": "normal | italic", - "sampleSize": "clamp(2.5rem, 7vw, 4.5rem) or 1rem", - "lineHeight": "1 | 1.2 | 1.6", - "letterSpacing": "normal | 0.05em", - "textTransform": "none | uppercase", - "purpose": "Short description of where this role is used." - } + "motion": [ + { "name": "ease-standard", "value": "cubic-bezier(0.4, 0, 0.2, 1)", "purpose": "Default easing for state transitions." } ], - "radii": [{ "name": "sm|md|lg|xl|full", "value": "4px" }], - "shadows": [{ "name": "Descriptive Name", "value": "0 4px 24px rgba(0,0,0,0.12)", "purpose": "..." }], - "spacing": [{ "name": "xs|sm|md|lg|xl|2xl|3xl", "value": "8px" }] + "breakpoints": [ + { "name": "sm", "value": "640px" } + ] }, "components": [ { "name": "Primary Button", "kind": "button | input | nav | chip | card | custom", + "refersTo": "button-primary", "description": "One-line what and when.", "html": "", "css": ".ds-btn-primary { background: #191c1d; color: #fff; padding: 16px 48px; letter-spacing: 0.05em; text-transform: uppercase; font-weight: 500; border: none; border-radius: 0; transition: background 0.2s, transform 0.2s; } .ds-btn-primary:hover { background: oklch(60% 0.25 350); transform: translateY(-2px); }" @@ -235,6 +289,8 @@ Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks } ``` +**What changed from schemaVersion 1.** The old sidecar carried token primitive arrays (`tokens.colors[]`, `tokens.typography[]`, etc.). Those values now live in the frontmatter. The sidecar only carries metadata that can't live in the frontmatter — tonal ramps, canonical OKLCH when the hex is an approximation, display names, role hints — keyed by the frontmatter token name (`colorMeta.`, `typographyMeta.`). Components still carry full HTML/CSS because Stitch's 8-prop set can't hold them. + #### Component translation rules The `html` and `css` fields must be **self-contained, drop-in snippets** that render correctly when injected into a shadow DOM. The panel applies them directly — no post-processing, no framework runtime. @@ -336,7 +392,7 @@ Per-section guidance in seed mode: - **Components**: omit entirely — no components exist yet. - **Do's and Don'ts**: carry PRODUCT.md's anti-references directly plus the anti-reference named in Q5. -Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens and components to render; there is nothing to show yet. The sidecar gets generated on the next Scan-mode run. +Seed mode writes a minimal frontmatter with `name` and `description` only — no colors, typography, rounded, spacing, or components yet. Real tokens land on the next Scan-mode run. Skip the `DESIGN.json` sidecar in seed mode for the same reason: nothing to render. ### Step 4: Confirm and refresh session cache @@ -346,6 +402,8 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an ## Style guidelines +- **Frontmatter first, prose second.** Tokens go in the YAML frontmatter; prose contextualizes them. Don't redefine a token value in two places — the frontmatter is normative. +- **Cite PRODUCT.md anti-references by name** in the Do's and Don'ts section. If PRODUCT.md lists "SaaS landing-page clichés" or "generic AI tool marketing" as anti-references, the DESIGN.md Don'ts should repeat those phrases verbatim so the visual spec enforces the strategic line. - **Match the spec, don't invent new sections.** The six section names are fixed. If you have Layout/Motion/Responsive content to document, fold it into Overview (philosophy-level rules) or Components (per-component behavior). - **Descriptive > technical**: "Gently curved edges (8px radius)" > "rounded-lg". Include the technical value in parens, lead with the description. - **Functional > decorative**: for each token, explain WHERE and WHY it's used, not just WHAT it is. @@ -365,3 +423,5 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an - Don't duplicate content from PRODUCT.md. DESIGN.md is strictly visual. - Don't add a "Layout Principles" or "Motion" or "Responsive Behavior" top-level section. The spec has six, not nine. Fold that content where it belongs. - Don't rename sections even slightly. "Colors" not "Color Palette & Roles". "Typography" not "Typography Rules". Tooling parsing depends on exact headers. +- Don't duplicate token values between frontmatter and prose. If a color is in `colors.primary` as hex, the prose can name it and describe its role but should not reassert a different hex. The frontmatter is normative. +- Don't invent frontmatter token groups outside Stitch's schema (no `motion:`, `breakpoints:`, `shadows:` at the top level). Stitch's Zod schema only accepts `colors`, `typography`, `rounded`, `spacing`, `components`. Anything else belongs in the sidecar's `extensions`. diff --git a/.kiro/skills/impeccable/scripts/design-parser.mjs b/.kiro/skills/impeccable/scripts/design-parser.mjs index 6220f67e7..b68756640 100644 --- a/.kiro/skills/impeccable/scripts/design-parser.mjs +++ b/.kiro/skills/impeccable/scripts/design-parser.mjs @@ -1,5 +1,10 @@ // Parse a DESIGN.md (Stitch-spec format) into a structured JSON model that // the live-mode design-system panel can render. Deterministic, dependency-free. +// +// Two-layer: YAML frontmatter (machine-readable tokens) + markdown body +// (prose with six canonical H2 sections). When frontmatter is present, it's +// exposed on `model.frontmatter` alongside the prose-scraped sections; +// consumers can prefer frontmatter values and fall back to prose. const CANONICAL_SECTIONS = [ 'Overview', @@ -10,6 +15,97 @@ const CANONICAL_SECTIONS = [ "Do's and Don'ts", ]; +// ---------- Frontmatter (Stitch YAML subset) ---------- + +function parseFrontmatter(md) { + const lines = md.split(/\r?\n/); + if (lines[0]?.trim() !== '---') return { frontmatter: null, body: md }; + + let end = -1; + for (let i = 1; i < lines.length; i++) { + if (lines[i].trim() === '---') { end = i; break; } + } + if (end === -1) return { frontmatter: null, body: md }; + + const yaml = lines.slice(1, end).join('\n'); + const body = lines.slice(end + 1).join('\n'); + try { + return { frontmatter: parseYamlSubset(yaml), body }; + } catch { + return { frontmatter: null, body: md }; + } +} + +// Minimal YAML reader for the Stitch frontmatter subset: scalar maps with +// one level of nested objects (typography roles, components). Indent-based, +// 2-space convention. No arrays, no anchors, no multi-line scalars — Stitch's +// schema doesn't need them and accepting them would require a real YAML +// dependency we don't want to vendor. +function parseYamlSubset(yaml) { + const lines = yaml.split(/\r?\n/); + const root = {}; + const stack = [{ indent: -1, obj: root }]; + + for (const raw of lines) { + // Skip blanks and line-only comments. Don't strip inline comments: + // unquoted hex values start with `#` and can't be safely distinguished + // from a comment after whitespace. + if (!raw.trim() || /^\s*#/.test(raw)) continue; + + const indent = raw.match(/^\s*/)[0].length; + const content = raw.slice(indent); + + const colonIdx = findTopLevelColon(content); + if (colonIdx === -1) continue; + + while (stack.length > 1 && stack[stack.length - 1].indent >= indent) { + stack.pop(); + } + + const key = content.slice(0, colonIdx).trim(); + const rest = content.slice(colonIdx + 1).trim(); + const parent = stack[stack.length - 1].obj; + + if (rest === '') { + const obj = {}; + parent[key] = obj; + stack.push({ indent, obj }); + } else { + parent[key] = parseScalar(rest); + } + } + + return root; +} + +function findTopLevelColon(s) { + let inQuote = null; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (inQuote) { + if (ch === inQuote && s[i - 1] !== '\\') inQuote = null; + } else if (ch === '"' || ch === "'") { + inQuote = ch; + } else if (ch === ':') { + return i; + } + } + return -1; +} + +function parseScalar(raw) { + const s = raw.trim(); + if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { + return s.slice(1, -1); + } + if (s === 'true') return true; + if (s === 'false') return false; + if (s === 'null' || s === '~') return null; + if (/^-?\d+$/.test(s)) return Number(s); + if (/^-?\d*\.\d+$/.test(s)) return Number(s); + return s; +} + const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g; const OKLCH_RE = /oklch\([^)]+\)/gi; const RGBA_RE = /rgba?\([^)]+\)/gi; @@ -706,10 +802,12 @@ function assessCoverage(model) { // ---------- Main ---------- export function parseDesignMd(md) { - const { title, sections } = splitSections(md); + const { frontmatter, body } = parseFrontmatter(md); + const { title, sections } = splitSections(body); return { - schemaVersion: 1, + schemaVersion: 2, title, + frontmatter, overview: extractOverview(sections['Overview']), colors: extractColors(sections['Colors']), typography: extractTypography(sections['Typography']), diff --git a/.kiro/skills/impeccable/scripts/live-browser.js b/.kiro/skills/impeccable/scripts/live-browser.js index 32ea9a191..d36af3536 100644 --- a/.kiro/skills/impeccable/scripts/live-browser.js +++ b/.kiro/skills/impeccable/scripts/live-browser.js @@ -3958,7 +3958,7 @@ void main() { function renderParsedMdCta() { const box = document.createElement('div'); box.className = 'parsed-md-cta'; - box.innerHTML = `Basic viewRunning /impeccable document generates DESIGN.json alongside your DESIGN.md, which lets this panel render your project's actual button, input, and nav primitives — not generic approximations.`; + box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running /impeccable document also generates a DESIGN.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; return box; } diff --git a/.opencode/skills/impeccable/reference/document.md b/.opencode/skills/impeccable/reference/document.md index 045b40fa7..40be231b6 100644 --- a/.opencode/skills/impeccable/reference/document.md +++ b/.opencode/skills/impeccable/reference/document.md @@ -1,8 +1,54 @@ Generate a `DESIGN.md` file at the project root that captures the current visual design system, so AI agents generating new screens stay on-brand. -DESIGN.md follows the [official Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/): exactly six sections in a fixed order. Sections may be omitted when not relevant, but **do not reorder them and do not rename them**. Section headers must match the spec character-for-character so the file stays parseable by other DESIGN.md-aware tools (Stitch itself, awesome-design-md, skill-rest, etc.). +DESIGN.md follows the [official Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/): YAML frontmatter carrying machine-readable design tokens, followed by a markdown body with exactly six sections in a fixed order. **Tokens are normative; prose provides context for how to apply them.** Sections may be omitted when not relevant, but **do not reorder them and do not rename them**. Section headers must match the spec character-for-character so the file stays parseable by other DESIGN.md-aware tools (Stitch itself, awesome-design-md, skill-rest, etc.). -## The six sections (exact order) +## The frontmatter: token schema + +The YAML frontmatter is the machine-readable layer. It's what Stitch's linter validates and what the live panel renders tiles from. Keep it tight; every entry should correspond to a token the project actually uses. + +```yaml +--- +name: +description: +colors: + primary: "#b8422e" + neutral-bg: "#faf7f2" + # ...one entry per extracted color; key = descriptive slug +typography: + display: + fontFamily: "Cormorant Garamond, Georgia, serif" + fontSize: "clamp(2.5rem, 7vw, 4.5rem)" + fontWeight: 300 + lineHeight: 1 + letterSpacing: "normal" + body: + # ... +rounded: + sm: "4px" + md: "8px" +spacing: + sm: "8px" + md: "16px" +components: + button-primary: + backgroundColor: "{colors.primary}" + textColor: "{colors.neutral-bg}" + rounded: "{rounded.sm}" + padding: "16px 48px" + button-primary-hover: + backgroundColor: "{colors.primary-deep}" +--- +``` + +Rules that matter: + +- **Token refs** use `{path.to.token}` (e.g. `{colors.primary}`, `{rounded.md}`). Components may reference primitives; primitives may not reference each other. +- **Stitch validates colors as hex sRGB only** (`#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`); OKLCH/HSL/P3 trigger a linter warning, not a hard error. YAML accepts the string either way and our own parser is format-agnostic. Choose based on project posture: (a) if the project has an "OKLCH-only" doctrine or uses Display-P3 values that don't round-trip through sRGB, put OKLCH directly in the frontmatter and accept the Stitch linter warning; (b) if the project wants strict Stitch compliance or plans to use their Tailwind/DTCG export pipeline, put hex in the frontmatter and keep OKLCH in prose as the canonical reference. Never split the source of truth without explicit reason. +- **Component sub-tokens** are limited to 8 props: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, `width`. Shadows, motion, focus rings, backdrop-filter — none of those fit. Carry them in the sidecar (Step 4b). +- **Scale keys are open-ended.** Use whatever names the project already uses (`warm-ash-cream`, `surface-container-low`). Don't rename to Material defaults. +- **Variants are naming convention, not schema.** `button-primary` / `button-primary-hover` / `button-primary-active` as sibling keys. + +## The markdown body: six sections (exact order) 1. `## Overview` 2. `## Colors` @@ -53,6 +99,17 @@ Build a structured draft from the discovered tokens. For each token class: - **Components**: For each common component (button, card, input, chip, list item, tooltip, nav), extract shape (radius), color assignment, hover/focus treatment, internal padding. - **Spacing + layout**: Fold into Overview or relevant Components. The spec does NOT have a Layout section. +### Step 2b: Stage the frontmatter + +From the auto-extracted tokens, draft the YAML frontmatter now (you'll write it at the top of DESIGN.md in Step 4). This is the machine-readable layer — what the live panel and Stitch's linter consume. + +- **Colors**: one entry per extracted color. Key = descriptive slug (`warm-ash-cream`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex — see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. +- **Typography**: one entry per role (`display`, `headline`, `title`, `body`, `label`). Typography is an object; include only the props that are real for the project (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`). +- **Rounded / Spacing**: whatever scale steps the project actually uses, keyed by whatever scale name the project uses (`sm` / `md` / `lg`, or `surface-sm`, or numeric steps). +- **Components**: one entry per variant (`button-primary`, `button-primary-hover`, `button-ghost`). Reference primitives via `{colors.X}`, `{rounded.Y}`. If a variant needs a property Stitch's 8-prop set doesn't cover (shadow, focus ring, backdrop-filter), carry the full snippet in the sidecar instead. + +Skip anything the project doesn't have. Empty scale keys or fabricated tokens pollute the spec. + ### Step 3: Ask the user for qualitative language The following require creative input that cannot be auto-extracted. Group them into one `AskUserQuestion` interaction: @@ -67,9 +124,16 @@ Quote a line from PRODUCT.md when possible so the user sees their own strategic ### Step 4: Write DESIGN.md -Use this exact structure. Headers must match character-for-character. Optional evocative subtitles (e.g. `## 2. Colors: The Coastal Palette`) are allowed. +The file opens with the YAML frontmatter staged in Step 2b (schema documented at the top of this reference), then the markdown body using the structure below. Headers must match character-for-character. Optional evocative subtitles (e.g. `## 2. Colors: The Coastal Palette`) are allowed. ```markdown +--- +name: [Project Title] +description: [one-line tagline] +colors: + # ... staged frontmatter from Step 2b +--- + # Design System: [Project Title] ## 1. Overview @@ -161,7 +225,7 @@ For each component, lead with a short character line, then specify shape, color ## 6. Do's and Don'ts -Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — include exact colors, pixel values, and named anti-patterns the user mentioned in PRODUCT.md. +Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — include exact colors, pixel values, and named anti-patterns the user mentioned in PRODUCT.md. **Every anti-reference in PRODUCT.md should show up here as a "Don't" with the same language**, so the visual spec carries the strategic line through. Quote PRODUCT.md directly where possible: if PRODUCT.md says *"avoid dark mode with purple gradients, neon accents, glassmorphism"*, the Don'ts here should repeat that by name. ### Do: - **Do** [specific prescription with exact values / named rule]. @@ -173,9 +237,9 @@ Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — i - **Don't** [...] ``` -### Step 4b: Write DESIGN.json sidecar +### Step 4b: Write DESIGN.json sidecar (extensions only) -After the Markdown is written, produce a machine-readable sidecar at `DESIGN.json` next to `DESIGN.md`. This powers the `/impeccable live` design-system panel, which renders a tile-based visualization of the system — color swatches with tonal ramps, Aa type specimens, live component previews. **The sidecar is how the panel shows *this project's* actual button/input/nav/card, not a generic approximation.** +The frontmatter owns token primitives (colors, typography, rounded, spacing, components). The sidecar at `DESIGN.json` carries **what Stitch's schema can't hold**: tonal ramps per color, shadow/elevation tokens, motion tokens, breakpoints, full component HTML/CSS snippets (the panel renders these into a shadow DOM), and narrative (north star, rules, do's/don'ts). It extends the frontmatter, it doesn't duplicate it. Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks to refresh the sidecar (e.g., from the live panel's stale-hint), preserve DESIGN.md and write only DESIGN.json. @@ -183,42 +247,32 @@ Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks ```json { - "schemaVersion": 1, + "schemaVersion": 2, "generatedAt": "ISO-8601 string", "title": "Design System: [Project Title]", - "tokens": { - "colors": [ - { - "role": "primary | secondary | tertiary | neutral | accent", - "name": "Descriptive Name", - "value": "#HEX or oklch(...) or rgba(...)", - "description": "Short role explanation (one sentence).", - "tonalRamp": ["...", "...", "..."] - } + "extensions": { + "colorMeta": { + "primary": { "role": "primary", "displayName": "Editorial Magenta", "canonical": "oklch(60% 0.25 350)", "tonalRamp": ["...", "...", "..."] }, + "warm-ash-cream": { "role": "neutral", "displayName": "Warm Ash Cream", "canonical": "oklch(96% 0.005 350)", "tonalRamp": ["...", "...", "..."] } + }, + "typographyMeta": { + "display": { "displayName": "Display", "purpose": "Hero headlines only." } + }, + "shadows": [ + { "name": "ambient-low", "value": "0 4px 24px rgba(0,0,0,0.12)", "purpose": "Diffuse hover glow under accent elements." } ], - "typography": [ - { - "role": "display | headline | title | body | label | mono", - "name": "Display", - "family": "Cormorant Garamond", - "fallback": "Georgia, serif", - "weight": 300, - "style": "normal | italic", - "sampleSize": "clamp(2.5rem, 7vw, 4.5rem) or 1rem", - "lineHeight": "1 | 1.2 | 1.6", - "letterSpacing": "normal | 0.05em", - "textTransform": "none | uppercase", - "purpose": "Short description of where this role is used." - } + "motion": [ + { "name": "ease-standard", "value": "cubic-bezier(0.4, 0, 0.2, 1)", "purpose": "Default easing for state transitions." } ], - "radii": [{ "name": "sm|md|lg|xl|full", "value": "4px" }], - "shadows": [{ "name": "Descriptive Name", "value": "0 4px 24px rgba(0,0,0,0.12)", "purpose": "..." }], - "spacing": [{ "name": "xs|sm|md|lg|xl|2xl|3xl", "value": "8px" }] + "breakpoints": [ + { "name": "sm", "value": "640px" } + ] }, "components": [ { "name": "Primary Button", "kind": "button | input | nav | chip | card | custom", + "refersTo": "button-primary", "description": "One-line what and when.", "html": "", "css": ".ds-btn-primary { background: #191c1d; color: #fff; padding: 16px 48px; letter-spacing: 0.05em; text-transform: uppercase; font-weight: 500; border: none; border-radius: 0; transition: background 0.2s, transform 0.2s; } .ds-btn-primary:hover { background: oklch(60% 0.25 350); transform: translateY(-2px); }" @@ -235,6 +289,8 @@ Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks } ``` +**What changed from schemaVersion 1.** The old sidecar carried token primitive arrays (`tokens.colors[]`, `tokens.typography[]`, etc.). Those values now live in the frontmatter. The sidecar only carries metadata that can't live in the frontmatter — tonal ramps, canonical OKLCH when the hex is an approximation, display names, role hints — keyed by the frontmatter token name (`colorMeta.`, `typographyMeta.`). Components still carry full HTML/CSS because Stitch's 8-prop set can't hold them. + #### Component translation rules The `html` and `css` fields must be **self-contained, drop-in snippets** that render correctly when injected into a shadow DOM. The panel applies them directly — no post-processing, no framework runtime. @@ -336,7 +392,7 @@ Per-section guidance in seed mode: - **Components**: omit entirely — no components exist yet. - **Do's and Don'ts**: carry PRODUCT.md's anti-references directly plus the anti-reference named in Q5. -Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens and components to render; there is nothing to show yet. The sidecar gets generated on the next Scan-mode run. +Seed mode writes a minimal frontmatter with `name` and `description` only — no colors, typography, rounded, spacing, or components yet. Real tokens land on the next Scan-mode run. Skip the `DESIGN.json` sidecar in seed mode for the same reason: nothing to render. ### Step 4: Confirm and refresh session cache @@ -346,6 +402,8 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an ## Style guidelines +- **Frontmatter first, prose second.** Tokens go in the YAML frontmatter; prose contextualizes them. Don't redefine a token value in two places — the frontmatter is normative. +- **Cite PRODUCT.md anti-references by name** in the Do's and Don'ts section. If PRODUCT.md lists "SaaS landing-page clichés" or "generic AI tool marketing" as anti-references, the DESIGN.md Don'ts should repeat those phrases verbatim so the visual spec enforces the strategic line. - **Match the spec, don't invent new sections.** The six section names are fixed. If you have Layout/Motion/Responsive content to document, fold it into Overview (philosophy-level rules) or Components (per-component behavior). - **Descriptive > technical**: "Gently curved edges (8px radius)" > "rounded-lg". Include the technical value in parens, lead with the description. - **Functional > decorative**: for each token, explain WHERE and WHY it's used, not just WHAT it is. @@ -365,3 +423,5 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an - Don't duplicate content from PRODUCT.md. DESIGN.md is strictly visual. - Don't add a "Layout Principles" or "Motion" or "Responsive Behavior" top-level section. The spec has six, not nine. Fold that content where it belongs. - Don't rename sections even slightly. "Colors" not "Color Palette & Roles". "Typography" not "Typography Rules". Tooling parsing depends on exact headers. +- Don't duplicate token values between frontmatter and prose. If a color is in `colors.primary` as hex, the prose can name it and describe its role but should not reassert a different hex. The frontmatter is normative. +- Don't invent frontmatter token groups outside Stitch's schema (no `motion:`, `breakpoints:`, `shadows:` at the top level). Stitch's Zod schema only accepts `colors`, `typography`, `rounded`, `spacing`, `components`. Anything else belongs in the sidecar's `extensions`. diff --git a/.opencode/skills/impeccable/scripts/design-parser.mjs b/.opencode/skills/impeccable/scripts/design-parser.mjs index 6220f67e7..b68756640 100644 --- a/.opencode/skills/impeccable/scripts/design-parser.mjs +++ b/.opencode/skills/impeccable/scripts/design-parser.mjs @@ -1,5 +1,10 @@ // Parse a DESIGN.md (Stitch-spec format) into a structured JSON model that // the live-mode design-system panel can render. Deterministic, dependency-free. +// +// Two-layer: YAML frontmatter (machine-readable tokens) + markdown body +// (prose with six canonical H2 sections). When frontmatter is present, it's +// exposed on `model.frontmatter` alongside the prose-scraped sections; +// consumers can prefer frontmatter values and fall back to prose. const CANONICAL_SECTIONS = [ 'Overview', @@ -10,6 +15,97 @@ const CANONICAL_SECTIONS = [ "Do's and Don'ts", ]; +// ---------- Frontmatter (Stitch YAML subset) ---------- + +function parseFrontmatter(md) { + const lines = md.split(/\r?\n/); + if (lines[0]?.trim() !== '---') return { frontmatter: null, body: md }; + + let end = -1; + for (let i = 1; i < lines.length; i++) { + if (lines[i].trim() === '---') { end = i; break; } + } + if (end === -1) return { frontmatter: null, body: md }; + + const yaml = lines.slice(1, end).join('\n'); + const body = lines.slice(end + 1).join('\n'); + try { + return { frontmatter: parseYamlSubset(yaml), body }; + } catch { + return { frontmatter: null, body: md }; + } +} + +// Minimal YAML reader for the Stitch frontmatter subset: scalar maps with +// one level of nested objects (typography roles, components). Indent-based, +// 2-space convention. No arrays, no anchors, no multi-line scalars — Stitch's +// schema doesn't need them and accepting them would require a real YAML +// dependency we don't want to vendor. +function parseYamlSubset(yaml) { + const lines = yaml.split(/\r?\n/); + const root = {}; + const stack = [{ indent: -1, obj: root }]; + + for (const raw of lines) { + // Skip blanks and line-only comments. Don't strip inline comments: + // unquoted hex values start with `#` and can't be safely distinguished + // from a comment after whitespace. + if (!raw.trim() || /^\s*#/.test(raw)) continue; + + const indent = raw.match(/^\s*/)[0].length; + const content = raw.slice(indent); + + const colonIdx = findTopLevelColon(content); + if (colonIdx === -1) continue; + + while (stack.length > 1 && stack[stack.length - 1].indent >= indent) { + stack.pop(); + } + + const key = content.slice(0, colonIdx).trim(); + const rest = content.slice(colonIdx + 1).trim(); + const parent = stack[stack.length - 1].obj; + + if (rest === '') { + const obj = {}; + parent[key] = obj; + stack.push({ indent, obj }); + } else { + parent[key] = parseScalar(rest); + } + } + + return root; +} + +function findTopLevelColon(s) { + let inQuote = null; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (inQuote) { + if (ch === inQuote && s[i - 1] !== '\\') inQuote = null; + } else if (ch === '"' || ch === "'") { + inQuote = ch; + } else if (ch === ':') { + return i; + } + } + return -1; +} + +function parseScalar(raw) { + const s = raw.trim(); + if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { + return s.slice(1, -1); + } + if (s === 'true') return true; + if (s === 'false') return false; + if (s === 'null' || s === '~') return null; + if (/^-?\d+$/.test(s)) return Number(s); + if (/^-?\d*\.\d+$/.test(s)) return Number(s); + return s; +} + const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g; const OKLCH_RE = /oklch\([^)]+\)/gi; const RGBA_RE = /rgba?\([^)]+\)/gi; @@ -706,10 +802,12 @@ function assessCoverage(model) { // ---------- Main ---------- export function parseDesignMd(md) { - const { title, sections } = splitSections(md); + const { frontmatter, body } = parseFrontmatter(md); + const { title, sections } = splitSections(body); return { - schemaVersion: 1, + schemaVersion: 2, title, + frontmatter, overview: extractOverview(sections['Overview']), colors: extractColors(sections['Colors']), typography: extractTypography(sections['Typography']), diff --git a/.opencode/skills/impeccable/scripts/live-browser.js b/.opencode/skills/impeccable/scripts/live-browser.js index 32ea9a191..d36af3536 100644 --- a/.opencode/skills/impeccable/scripts/live-browser.js +++ b/.opencode/skills/impeccable/scripts/live-browser.js @@ -3958,7 +3958,7 @@ void main() { function renderParsedMdCta() { const box = document.createElement('div'); box.className = 'parsed-md-cta'; - box.innerHTML = `Basic viewRunning /impeccable document generates DESIGN.json alongside your DESIGN.md, which lets this panel render your project's actual button, input, and nav primitives — not generic approximations.`; + box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running /impeccable document also generates a DESIGN.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; return box; } diff --git a/.pi/skills/impeccable/reference/document.md b/.pi/skills/impeccable/reference/document.md index 01ec2d140..614be9a15 100644 --- a/.pi/skills/impeccable/reference/document.md +++ b/.pi/skills/impeccable/reference/document.md @@ -1,8 +1,54 @@ Generate a `DESIGN.md` file at the project root that captures the current visual design system, so AI agents generating new screens stay on-brand. -DESIGN.md follows the [official Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/): exactly six sections in a fixed order. Sections may be omitted when not relevant, but **do not reorder them and do not rename them**. Section headers must match the spec character-for-character so the file stays parseable by other DESIGN.md-aware tools (Stitch itself, awesome-design-md, skill-rest, etc.). +DESIGN.md follows the [official Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/): YAML frontmatter carrying machine-readable design tokens, followed by a markdown body with exactly six sections in a fixed order. **Tokens are normative; prose provides context for how to apply them.** Sections may be omitted when not relevant, but **do not reorder them and do not rename them**. Section headers must match the spec character-for-character so the file stays parseable by other DESIGN.md-aware tools (Stitch itself, awesome-design-md, skill-rest, etc.). -## The six sections (exact order) +## The frontmatter: token schema + +The YAML frontmatter is the machine-readable layer. It's what Stitch's linter validates and what the live panel renders tiles from. Keep it tight; every entry should correspond to a token the project actually uses. + +```yaml +--- +name: +description: +colors: + primary: "#b8422e" + neutral-bg: "#faf7f2" + # ...one entry per extracted color; key = descriptive slug +typography: + display: + fontFamily: "Cormorant Garamond, Georgia, serif" + fontSize: "clamp(2.5rem, 7vw, 4.5rem)" + fontWeight: 300 + lineHeight: 1 + letterSpacing: "normal" + body: + # ... +rounded: + sm: "4px" + md: "8px" +spacing: + sm: "8px" + md: "16px" +components: + button-primary: + backgroundColor: "{colors.primary}" + textColor: "{colors.neutral-bg}" + rounded: "{rounded.sm}" + padding: "16px 48px" + button-primary-hover: + backgroundColor: "{colors.primary-deep}" +--- +``` + +Rules that matter: + +- **Token refs** use `{path.to.token}` (e.g. `{colors.primary}`, `{rounded.md}`). Components may reference primitives; primitives may not reference each other. +- **Stitch validates colors as hex sRGB only** (`#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`); OKLCH/HSL/P3 trigger a linter warning, not a hard error. YAML accepts the string either way and our own parser is format-agnostic. Choose based on project posture: (a) if the project has an "OKLCH-only" doctrine or uses Display-P3 values that don't round-trip through sRGB, put OKLCH directly in the frontmatter and accept the Stitch linter warning; (b) if the project wants strict Stitch compliance or plans to use their Tailwind/DTCG export pipeline, put hex in the frontmatter and keep OKLCH in prose as the canonical reference. Never split the source of truth without explicit reason. +- **Component sub-tokens** are limited to 8 props: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, `width`. Shadows, motion, focus rings, backdrop-filter — none of those fit. Carry them in the sidecar (Step 4b). +- **Scale keys are open-ended.** Use whatever names the project already uses (`warm-ash-cream`, `surface-container-low`). Don't rename to Material defaults. +- **Variants are naming convention, not schema.** `button-primary` / `button-primary-hover` / `button-primary-active` as sibling keys. + +## The markdown body: six sections (exact order) 1. `## Overview` 2. `## Colors` @@ -53,6 +99,17 @@ Build a structured draft from the discovered tokens. For each token class: - **Components**: For each common component (button, card, input, chip, list item, tooltip, nav), extract shape (radius), color assignment, hover/focus treatment, internal padding. - **Spacing + layout**: Fold into Overview or relevant Components. The spec does NOT have a Layout section. +### Step 2b: Stage the frontmatter + +From the auto-extracted tokens, draft the YAML frontmatter now (you'll write it at the top of DESIGN.md in Step 4). This is the machine-readable layer — what the live panel and Stitch's linter consume. + +- **Colors**: one entry per extracted color. Key = descriptive slug (`warm-ash-cream`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex — see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. +- **Typography**: one entry per role (`display`, `headline`, `title`, `body`, `label`). Typography is an object; include only the props that are real for the project (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`). +- **Rounded / Spacing**: whatever scale steps the project actually uses, keyed by whatever scale name the project uses (`sm` / `md` / `lg`, or `surface-sm`, or numeric steps). +- **Components**: one entry per variant (`button-primary`, `button-primary-hover`, `button-ghost`). Reference primitives via `{colors.X}`, `{rounded.Y}`. If a variant needs a property Stitch's 8-prop set doesn't cover (shadow, focus ring, backdrop-filter), carry the full snippet in the sidecar instead. + +Skip anything the project doesn't have. Empty scale keys or fabricated tokens pollute the spec. + ### Step 3: Ask the user for qualitative language The following require creative input that cannot be auto-extracted. Group them into one `AskUserQuestion` interaction: @@ -67,9 +124,16 @@ Quote a line from PRODUCT.md when possible so the user sees their own strategic ### Step 4: Write DESIGN.md -Use this exact structure. Headers must match character-for-character. Optional evocative subtitles (e.g. `## 2. Colors: The Coastal Palette`) are allowed. +The file opens with the YAML frontmatter staged in Step 2b (schema documented at the top of this reference), then the markdown body using the structure below. Headers must match character-for-character. Optional evocative subtitles (e.g. `## 2. Colors: The Coastal Palette`) are allowed. ```markdown +--- +name: [Project Title] +description: [one-line tagline] +colors: + # ... staged frontmatter from Step 2b +--- + # Design System: [Project Title] ## 1. Overview @@ -161,7 +225,7 @@ For each component, lead with a short character line, then specify shape, color ## 6. Do's and Don'ts -Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — include exact colors, pixel values, and named anti-patterns the user mentioned in PRODUCT.md. +Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — include exact colors, pixel values, and named anti-patterns the user mentioned in PRODUCT.md. **Every anti-reference in PRODUCT.md should show up here as a "Don't" with the same language**, so the visual spec carries the strategic line through. Quote PRODUCT.md directly where possible: if PRODUCT.md says *"avoid dark mode with purple gradients, neon accents, glassmorphism"*, the Don'ts here should repeat that by name. ### Do: - **Do** [specific prescription with exact values / named rule]. @@ -173,9 +237,9 @@ Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — i - **Don't** [...] ``` -### Step 4b: Write DESIGN.json sidecar +### Step 4b: Write DESIGN.json sidecar (extensions only) -After the Markdown is written, produce a machine-readable sidecar at `DESIGN.json` next to `DESIGN.md`. This powers the `/impeccable live` design-system panel, which renders a tile-based visualization of the system — color swatches with tonal ramps, Aa type specimens, live component previews. **The sidecar is how the panel shows *this project's* actual button/input/nav/card, not a generic approximation.** +The frontmatter owns token primitives (colors, typography, rounded, spacing, components). The sidecar at `DESIGN.json` carries **what Stitch's schema can't hold**: tonal ramps per color, shadow/elevation tokens, motion tokens, breakpoints, full component HTML/CSS snippets (the panel renders these into a shadow DOM), and narrative (north star, rules, do's/don'ts). It extends the frontmatter, it doesn't duplicate it. Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks to refresh the sidecar (e.g., from the live panel's stale-hint), preserve DESIGN.md and write only DESIGN.json. @@ -183,42 +247,32 @@ Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks ```json { - "schemaVersion": 1, + "schemaVersion": 2, "generatedAt": "ISO-8601 string", "title": "Design System: [Project Title]", - "tokens": { - "colors": [ - { - "role": "primary | secondary | tertiary | neutral | accent", - "name": "Descriptive Name", - "value": "#HEX or oklch(...) or rgba(...)", - "description": "Short role explanation (one sentence).", - "tonalRamp": ["...", "...", "..."] - } + "extensions": { + "colorMeta": { + "primary": { "role": "primary", "displayName": "Editorial Magenta", "canonical": "oklch(60% 0.25 350)", "tonalRamp": ["...", "...", "..."] }, + "warm-ash-cream": { "role": "neutral", "displayName": "Warm Ash Cream", "canonical": "oklch(96% 0.005 350)", "tonalRamp": ["...", "...", "..."] } + }, + "typographyMeta": { + "display": { "displayName": "Display", "purpose": "Hero headlines only." } + }, + "shadows": [ + { "name": "ambient-low", "value": "0 4px 24px rgba(0,0,0,0.12)", "purpose": "Diffuse hover glow under accent elements." } ], - "typography": [ - { - "role": "display | headline | title | body | label | mono", - "name": "Display", - "family": "Cormorant Garamond", - "fallback": "Georgia, serif", - "weight": 300, - "style": "normal | italic", - "sampleSize": "clamp(2.5rem, 7vw, 4.5rem) or 1rem", - "lineHeight": "1 | 1.2 | 1.6", - "letterSpacing": "normal | 0.05em", - "textTransform": "none | uppercase", - "purpose": "Short description of where this role is used." - } + "motion": [ + { "name": "ease-standard", "value": "cubic-bezier(0.4, 0, 0.2, 1)", "purpose": "Default easing for state transitions." } ], - "radii": [{ "name": "sm|md|lg|xl|full", "value": "4px" }], - "shadows": [{ "name": "Descriptive Name", "value": "0 4px 24px rgba(0,0,0,0.12)", "purpose": "..." }], - "spacing": [{ "name": "xs|sm|md|lg|xl|2xl|3xl", "value": "8px" }] + "breakpoints": [ + { "name": "sm", "value": "640px" } + ] }, "components": [ { "name": "Primary Button", "kind": "button | input | nav | chip | card | custom", + "refersTo": "button-primary", "description": "One-line what and when.", "html": "", "css": ".ds-btn-primary { background: #191c1d; color: #fff; padding: 16px 48px; letter-spacing: 0.05em; text-transform: uppercase; font-weight: 500; border: none; border-radius: 0; transition: background 0.2s, transform 0.2s; } .ds-btn-primary:hover { background: oklch(60% 0.25 350); transform: translateY(-2px); }" @@ -235,6 +289,8 @@ Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks } ``` +**What changed from schemaVersion 1.** The old sidecar carried token primitive arrays (`tokens.colors[]`, `tokens.typography[]`, etc.). Those values now live in the frontmatter. The sidecar only carries metadata that can't live in the frontmatter — tonal ramps, canonical OKLCH when the hex is an approximation, display names, role hints — keyed by the frontmatter token name (`colorMeta.`, `typographyMeta.`). Components still carry full HTML/CSS because Stitch's 8-prop set can't hold them. + #### Component translation rules The `html` and `css` fields must be **self-contained, drop-in snippets** that render correctly when injected into a shadow DOM. The panel applies them directly — no post-processing, no framework runtime. @@ -336,7 +392,7 @@ Per-section guidance in seed mode: - **Components**: omit entirely — no components exist yet. - **Do's and Don'ts**: carry PRODUCT.md's anti-references directly plus the anti-reference named in Q5. -Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens and components to render; there is nothing to show yet. The sidecar gets generated on the next Scan-mode run. +Seed mode writes a minimal frontmatter with `name` and `description` only — no colors, typography, rounded, spacing, or components yet. Real tokens land on the next Scan-mode run. Skip the `DESIGN.json` sidecar in seed mode for the same reason: nothing to render. ### Step 4: Confirm and refresh session cache @@ -346,6 +402,8 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an ## Style guidelines +- **Frontmatter first, prose second.** Tokens go in the YAML frontmatter; prose contextualizes them. Don't redefine a token value in two places — the frontmatter is normative. +- **Cite PRODUCT.md anti-references by name** in the Do's and Don'ts section. If PRODUCT.md lists "SaaS landing-page clichés" or "generic AI tool marketing" as anti-references, the DESIGN.md Don'ts should repeat those phrases verbatim so the visual spec enforces the strategic line. - **Match the spec, don't invent new sections.** The six section names are fixed. If you have Layout/Motion/Responsive content to document, fold it into Overview (philosophy-level rules) or Components (per-component behavior). - **Descriptive > technical**: "Gently curved edges (8px radius)" > "rounded-lg". Include the technical value in parens, lead with the description. - **Functional > decorative**: for each token, explain WHERE and WHY it's used, not just WHAT it is. @@ -365,3 +423,5 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an - Don't duplicate content from PRODUCT.md. DESIGN.md is strictly visual. - Don't add a "Layout Principles" or "Motion" or "Responsive Behavior" top-level section. The spec has six, not nine. Fold that content where it belongs. - Don't rename sections even slightly. "Colors" not "Color Palette & Roles". "Typography" not "Typography Rules". Tooling parsing depends on exact headers. +- Don't duplicate token values between frontmatter and prose. If a color is in `colors.primary` as hex, the prose can name it and describe its role but should not reassert a different hex. The frontmatter is normative. +- Don't invent frontmatter token groups outside Stitch's schema (no `motion:`, `breakpoints:`, `shadows:` at the top level). Stitch's Zod schema only accepts `colors`, `typography`, `rounded`, `spacing`, `components`. Anything else belongs in the sidecar's `extensions`. diff --git a/.pi/skills/impeccable/scripts/design-parser.mjs b/.pi/skills/impeccable/scripts/design-parser.mjs index 6220f67e7..b68756640 100644 --- a/.pi/skills/impeccable/scripts/design-parser.mjs +++ b/.pi/skills/impeccable/scripts/design-parser.mjs @@ -1,5 +1,10 @@ // Parse a DESIGN.md (Stitch-spec format) into a structured JSON model that // the live-mode design-system panel can render. Deterministic, dependency-free. +// +// Two-layer: YAML frontmatter (machine-readable tokens) + markdown body +// (prose with six canonical H2 sections). When frontmatter is present, it's +// exposed on `model.frontmatter` alongside the prose-scraped sections; +// consumers can prefer frontmatter values and fall back to prose. const CANONICAL_SECTIONS = [ 'Overview', @@ -10,6 +15,97 @@ const CANONICAL_SECTIONS = [ "Do's and Don'ts", ]; +// ---------- Frontmatter (Stitch YAML subset) ---------- + +function parseFrontmatter(md) { + const lines = md.split(/\r?\n/); + if (lines[0]?.trim() !== '---') return { frontmatter: null, body: md }; + + let end = -1; + for (let i = 1; i < lines.length; i++) { + if (lines[i].trim() === '---') { end = i; break; } + } + if (end === -1) return { frontmatter: null, body: md }; + + const yaml = lines.slice(1, end).join('\n'); + const body = lines.slice(end + 1).join('\n'); + try { + return { frontmatter: parseYamlSubset(yaml), body }; + } catch { + return { frontmatter: null, body: md }; + } +} + +// Minimal YAML reader for the Stitch frontmatter subset: scalar maps with +// one level of nested objects (typography roles, components). Indent-based, +// 2-space convention. No arrays, no anchors, no multi-line scalars — Stitch's +// schema doesn't need them and accepting them would require a real YAML +// dependency we don't want to vendor. +function parseYamlSubset(yaml) { + const lines = yaml.split(/\r?\n/); + const root = {}; + const stack = [{ indent: -1, obj: root }]; + + for (const raw of lines) { + // Skip blanks and line-only comments. Don't strip inline comments: + // unquoted hex values start with `#` and can't be safely distinguished + // from a comment after whitespace. + if (!raw.trim() || /^\s*#/.test(raw)) continue; + + const indent = raw.match(/^\s*/)[0].length; + const content = raw.slice(indent); + + const colonIdx = findTopLevelColon(content); + if (colonIdx === -1) continue; + + while (stack.length > 1 && stack[stack.length - 1].indent >= indent) { + stack.pop(); + } + + const key = content.slice(0, colonIdx).trim(); + const rest = content.slice(colonIdx + 1).trim(); + const parent = stack[stack.length - 1].obj; + + if (rest === '') { + const obj = {}; + parent[key] = obj; + stack.push({ indent, obj }); + } else { + parent[key] = parseScalar(rest); + } + } + + return root; +} + +function findTopLevelColon(s) { + let inQuote = null; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (inQuote) { + if (ch === inQuote && s[i - 1] !== '\\') inQuote = null; + } else if (ch === '"' || ch === "'") { + inQuote = ch; + } else if (ch === ':') { + return i; + } + } + return -1; +} + +function parseScalar(raw) { + const s = raw.trim(); + if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { + return s.slice(1, -1); + } + if (s === 'true') return true; + if (s === 'false') return false; + if (s === 'null' || s === '~') return null; + if (/^-?\d+$/.test(s)) return Number(s); + if (/^-?\d*\.\d+$/.test(s)) return Number(s); + return s; +} + const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g; const OKLCH_RE = /oklch\([^)]+\)/gi; const RGBA_RE = /rgba?\([^)]+\)/gi; @@ -706,10 +802,12 @@ function assessCoverage(model) { // ---------- Main ---------- export function parseDesignMd(md) { - const { title, sections } = splitSections(md); + const { frontmatter, body } = parseFrontmatter(md); + const { title, sections } = splitSections(body); return { - schemaVersion: 1, + schemaVersion: 2, title, + frontmatter, overview: extractOverview(sections['Overview']), colors: extractColors(sections['Colors']), typography: extractTypography(sections['Typography']), diff --git a/.pi/skills/impeccable/scripts/live-browser.js b/.pi/skills/impeccable/scripts/live-browser.js index 32ea9a191..d36af3536 100644 --- a/.pi/skills/impeccable/scripts/live-browser.js +++ b/.pi/skills/impeccable/scripts/live-browser.js @@ -3958,7 +3958,7 @@ void main() { function renderParsedMdCta() { const box = document.createElement('div'); box.className = 'parsed-md-cta'; - box.innerHTML = `Basic viewRunning /impeccable document generates DESIGN.json alongside your DESIGN.md, which lets this panel render your project's actual button, input, and nav primitives — not generic approximations.`; + box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running /impeccable document also generates a DESIGN.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; return box; } diff --git a/.rovodev/skills/impeccable/reference/document.md b/.rovodev/skills/impeccable/reference/document.md index e66dc4b64..b4e9a8c9d 100644 --- a/.rovodev/skills/impeccable/reference/document.md +++ b/.rovodev/skills/impeccable/reference/document.md @@ -1,8 +1,54 @@ Generate a `DESIGN.md` file at the project root that captures the current visual design system, so AI agents generating new screens stay on-brand. -DESIGN.md follows the [official Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/): exactly six sections in a fixed order. Sections may be omitted when not relevant, but **do not reorder them and do not rename them**. Section headers must match the spec character-for-character so the file stays parseable by other DESIGN.md-aware tools (Stitch itself, awesome-design-md, skill-rest, etc.). +DESIGN.md follows the [official Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/): YAML frontmatter carrying machine-readable design tokens, followed by a markdown body with exactly six sections in a fixed order. **Tokens are normative; prose provides context for how to apply them.** Sections may be omitted when not relevant, but **do not reorder them and do not rename them**. Section headers must match the spec character-for-character so the file stays parseable by other DESIGN.md-aware tools (Stitch itself, awesome-design-md, skill-rest, etc.). -## The six sections (exact order) +## The frontmatter: token schema + +The YAML frontmatter is the machine-readable layer. It's what Stitch's linter validates and what the live panel renders tiles from. Keep it tight; every entry should correspond to a token the project actually uses. + +```yaml +--- +name: +description: +colors: + primary: "#b8422e" + neutral-bg: "#faf7f2" + # ...one entry per extracted color; key = descriptive slug +typography: + display: + fontFamily: "Cormorant Garamond, Georgia, serif" + fontSize: "clamp(2.5rem, 7vw, 4.5rem)" + fontWeight: 300 + lineHeight: 1 + letterSpacing: "normal" + body: + # ... +rounded: + sm: "4px" + md: "8px" +spacing: + sm: "8px" + md: "16px" +components: + button-primary: + backgroundColor: "{colors.primary}" + textColor: "{colors.neutral-bg}" + rounded: "{rounded.sm}" + padding: "16px 48px" + button-primary-hover: + backgroundColor: "{colors.primary-deep}" +--- +``` + +Rules that matter: + +- **Token refs** use `{path.to.token}` (e.g. `{colors.primary}`, `{rounded.md}`). Components may reference primitives; primitives may not reference each other. +- **Stitch validates colors as hex sRGB only** (`#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`); OKLCH/HSL/P3 trigger a linter warning, not a hard error. YAML accepts the string either way and our own parser is format-agnostic. Choose based on project posture: (a) if the project has an "OKLCH-only" doctrine or uses Display-P3 values that don't round-trip through sRGB, put OKLCH directly in the frontmatter and accept the Stitch linter warning; (b) if the project wants strict Stitch compliance or plans to use their Tailwind/DTCG export pipeline, put hex in the frontmatter and keep OKLCH in prose as the canonical reference. Never split the source of truth without explicit reason. +- **Component sub-tokens** are limited to 8 props: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, `width`. Shadows, motion, focus rings, backdrop-filter — none of those fit. Carry them in the sidecar (Step 4b). +- **Scale keys are open-ended.** Use whatever names the project already uses (`warm-ash-cream`, `surface-container-low`). Don't rename to Material defaults. +- **Variants are naming convention, not schema.** `button-primary` / `button-primary-hover` / `button-primary-active` as sibling keys. + +## The markdown body: six sections (exact order) 1. `## Overview` 2. `## Colors` @@ -53,6 +99,17 @@ Build a structured draft from the discovered tokens. For each token class: - **Components**: For each common component (button, card, input, chip, list item, tooltip, nav), extract shape (radius), color assignment, hover/focus treatment, internal padding. - **Spacing + layout**: Fold into Overview or relevant Components. The spec does NOT have a Layout section. +### Step 2b: Stage the frontmatter + +From the auto-extracted tokens, draft the YAML frontmatter now (you'll write it at the top of DESIGN.md in Step 4). This is the machine-readable layer — what the live panel and Stitch's linter consume. + +- **Colors**: one entry per extracted color. Key = descriptive slug (`warm-ash-cream`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex — see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. +- **Typography**: one entry per role (`display`, `headline`, `title`, `body`, `label`). Typography is an object; include only the props that are real for the project (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`). +- **Rounded / Spacing**: whatever scale steps the project actually uses, keyed by whatever scale name the project uses (`sm` / `md` / `lg`, or `surface-sm`, or numeric steps). +- **Components**: one entry per variant (`button-primary`, `button-primary-hover`, `button-ghost`). Reference primitives via `{colors.X}`, `{rounded.Y}`. If a variant needs a property Stitch's 8-prop set doesn't cover (shadow, focus ring, backdrop-filter), carry the full snippet in the sidecar instead. + +Skip anything the project doesn't have. Empty scale keys or fabricated tokens pollute the spec. + ### Step 3: Ask the user for qualitative language The following require creative input that cannot be auto-extracted. Group them into one `AskUserQuestion` interaction: @@ -67,9 +124,16 @@ Quote a line from PRODUCT.md when possible so the user sees their own strategic ### Step 4: Write DESIGN.md -Use this exact structure. Headers must match character-for-character. Optional evocative subtitles (e.g. `## 2. Colors: The Coastal Palette`) are allowed. +The file opens with the YAML frontmatter staged in Step 2b (schema documented at the top of this reference), then the markdown body using the structure below. Headers must match character-for-character. Optional evocative subtitles (e.g. `## 2. Colors: The Coastal Palette`) are allowed. ```markdown +--- +name: [Project Title] +description: [one-line tagline] +colors: + # ... staged frontmatter from Step 2b +--- + # Design System: [Project Title] ## 1. Overview @@ -161,7 +225,7 @@ For each component, lead with a short character line, then specify shape, color ## 6. Do's and Don'ts -Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — include exact colors, pixel values, and named anti-patterns the user mentioned in PRODUCT.md. +Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — include exact colors, pixel values, and named anti-patterns the user mentioned in PRODUCT.md. **Every anti-reference in PRODUCT.md should show up here as a "Don't" with the same language**, so the visual spec carries the strategic line through. Quote PRODUCT.md directly where possible: if PRODUCT.md says *"avoid dark mode with purple gradients, neon accents, glassmorphism"*, the Don'ts here should repeat that by name. ### Do: - **Do** [specific prescription with exact values / named rule]. @@ -173,9 +237,9 @@ Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — i - **Don't** [...] ``` -### Step 4b: Write DESIGN.json sidecar +### Step 4b: Write DESIGN.json sidecar (extensions only) -After the Markdown is written, produce a machine-readable sidecar at `DESIGN.json` next to `DESIGN.md`. This powers the `/impeccable live` design-system panel, which renders a tile-based visualization of the system — color swatches with tonal ramps, Aa type specimens, live component previews. **The sidecar is how the panel shows *this project's* actual button/input/nav/card, not a generic approximation.** +The frontmatter owns token primitives (colors, typography, rounded, spacing, components). The sidecar at `DESIGN.json` carries **what Stitch's schema can't hold**: tonal ramps per color, shadow/elevation tokens, motion tokens, breakpoints, full component HTML/CSS snippets (the panel renders these into a shadow DOM), and narrative (north star, rules, do's/don'ts). It extends the frontmatter, it doesn't duplicate it. Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks to refresh the sidecar (e.g., from the live panel's stale-hint), preserve DESIGN.md and write only DESIGN.json. @@ -183,42 +247,32 @@ Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks ```json { - "schemaVersion": 1, + "schemaVersion": 2, "generatedAt": "ISO-8601 string", "title": "Design System: [Project Title]", - "tokens": { - "colors": [ - { - "role": "primary | secondary | tertiary | neutral | accent", - "name": "Descriptive Name", - "value": "#HEX or oklch(...) or rgba(...)", - "description": "Short role explanation (one sentence).", - "tonalRamp": ["...", "...", "..."] - } + "extensions": { + "colorMeta": { + "primary": { "role": "primary", "displayName": "Editorial Magenta", "canonical": "oklch(60% 0.25 350)", "tonalRamp": ["...", "...", "..."] }, + "warm-ash-cream": { "role": "neutral", "displayName": "Warm Ash Cream", "canonical": "oklch(96% 0.005 350)", "tonalRamp": ["...", "...", "..."] } + }, + "typographyMeta": { + "display": { "displayName": "Display", "purpose": "Hero headlines only." } + }, + "shadows": [ + { "name": "ambient-low", "value": "0 4px 24px rgba(0,0,0,0.12)", "purpose": "Diffuse hover glow under accent elements." } ], - "typography": [ - { - "role": "display | headline | title | body | label | mono", - "name": "Display", - "family": "Cormorant Garamond", - "fallback": "Georgia, serif", - "weight": 300, - "style": "normal | italic", - "sampleSize": "clamp(2.5rem, 7vw, 4.5rem) or 1rem", - "lineHeight": "1 | 1.2 | 1.6", - "letterSpacing": "normal | 0.05em", - "textTransform": "none | uppercase", - "purpose": "Short description of where this role is used." - } + "motion": [ + { "name": "ease-standard", "value": "cubic-bezier(0.4, 0, 0.2, 1)", "purpose": "Default easing for state transitions." } ], - "radii": [{ "name": "sm|md|lg|xl|full", "value": "4px" }], - "shadows": [{ "name": "Descriptive Name", "value": "0 4px 24px rgba(0,0,0,0.12)", "purpose": "..." }], - "spacing": [{ "name": "xs|sm|md|lg|xl|2xl|3xl", "value": "8px" }] + "breakpoints": [ + { "name": "sm", "value": "640px" } + ] }, "components": [ { "name": "Primary Button", "kind": "button | input | nav | chip | card | custom", + "refersTo": "button-primary", "description": "One-line what and when.", "html": "", "css": ".ds-btn-primary { background: #191c1d; color: #fff; padding: 16px 48px; letter-spacing: 0.05em; text-transform: uppercase; font-weight: 500; border: none; border-radius: 0; transition: background 0.2s, transform 0.2s; } .ds-btn-primary:hover { background: oklch(60% 0.25 350); transform: translateY(-2px); }" @@ -235,6 +289,8 @@ Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks } ``` +**What changed from schemaVersion 1.** The old sidecar carried token primitive arrays (`tokens.colors[]`, `tokens.typography[]`, etc.). Those values now live in the frontmatter. The sidecar only carries metadata that can't live in the frontmatter — tonal ramps, canonical OKLCH when the hex is an approximation, display names, role hints — keyed by the frontmatter token name (`colorMeta.`, `typographyMeta.`). Components still carry full HTML/CSS because Stitch's 8-prop set can't hold them. + #### Component translation rules The `html` and `css` fields must be **self-contained, drop-in snippets** that render correctly when injected into a shadow DOM. The panel applies them directly — no post-processing, no framework runtime. @@ -336,7 +392,7 @@ Per-section guidance in seed mode: - **Components**: omit entirely — no components exist yet. - **Do's and Don'ts**: carry PRODUCT.md's anti-references directly plus the anti-reference named in Q5. -Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens and components to render; there is nothing to show yet. The sidecar gets generated on the next Scan-mode run. +Seed mode writes a minimal frontmatter with `name` and `description` only — no colors, typography, rounded, spacing, or components yet. Real tokens land on the next Scan-mode run. Skip the `DESIGN.json` sidecar in seed mode for the same reason: nothing to render. ### Step 4: Confirm and refresh session cache @@ -346,6 +402,8 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an ## Style guidelines +- **Frontmatter first, prose second.** Tokens go in the YAML frontmatter; prose contextualizes them. Don't redefine a token value in two places — the frontmatter is normative. +- **Cite PRODUCT.md anti-references by name** in the Do's and Don'ts section. If PRODUCT.md lists "SaaS landing-page clichés" or "generic AI tool marketing" as anti-references, the DESIGN.md Don'ts should repeat those phrases verbatim so the visual spec enforces the strategic line. - **Match the spec, don't invent new sections.** The six section names are fixed. If you have Layout/Motion/Responsive content to document, fold it into Overview (philosophy-level rules) or Components (per-component behavior). - **Descriptive > technical**: "Gently curved edges (8px radius)" > "rounded-lg". Include the technical value in parens, lead with the description. - **Functional > decorative**: for each token, explain WHERE and WHY it's used, not just WHAT it is. @@ -365,3 +423,5 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an - Don't duplicate content from PRODUCT.md. DESIGN.md is strictly visual. - Don't add a "Layout Principles" or "Motion" or "Responsive Behavior" top-level section. The spec has six, not nine. Fold that content where it belongs. - Don't rename sections even slightly. "Colors" not "Color Palette & Roles". "Typography" not "Typography Rules". Tooling parsing depends on exact headers. +- Don't duplicate token values between frontmatter and prose. If a color is in `colors.primary` as hex, the prose can name it and describe its role but should not reassert a different hex. The frontmatter is normative. +- Don't invent frontmatter token groups outside Stitch's schema (no `motion:`, `breakpoints:`, `shadows:` at the top level). Stitch's Zod schema only accepts `colors`, `typography`, `rounded`, `spacing`, `components`. Anything else belongs in the sidecar's `extensions`. diff --git a/.rovodev/skills/impeccable/scripts/design-parser.mjs b/.rovodev/skills/impeccable/scripts/design-parser.mjs index 6220f67e7..b68756640 100644 --- a/.rovodev/skills/impeccable/scripts/design-parser.mjs +++ b/.rovodev/skills/impeccable/scripts/design-parser.mjs @@ -1,5 +1,10 @@ // Parse a DESIGN.md (Stitch-spec format) into a structured JSON model that // the live-mode design-system panel can render. Deterministic, dependency-free. +// +// Two-layer: YAML frontmatter (machine-readable tokens) + markdown body +// (prose with six canonical H2 sections). When frontmatter is present, it's +// exposed on `model.frontmatter` alongside the prose-scraped sections; +// consumers can prefer frontmatter values and fall back to prose. const CANONICAL_SECTIONS = [ 'Overview', @@ -10,6 +15,97 @@ const CANONICAL_SECTIONS = [ "Do's and Don'ts", ]; +// ---------- Frontmatter (Stitch YAML subset) ---------- + +function parseFrontmatter(md) { + const lines = md.split(/\r?\n/); + if (lines[0]?.trim() !== '---') return { frontmatter: null, body: md }; + + let end = -1; + for (let i = 1; i < lines.length; i++) { + if (lines[i].trim() === '---') { end = i; break; } + } + if (end === -1) return { frontmatter: null, body: md }; + + const yaml = lines.slice(1, end).join('\n'); + const body = lines.slice(end + 1).join('\n'); + try { + return { frontmatter: parseYamlSubset(yaml), body }; + } catch { + return { frontmatter: null, body: md }; + } +} + +// Minimal YAML reader for the Stitch frontmatter subset: scalar maps with +// one level of nested objects (typography roles, components). Indent-based, +// 2-space convention. No arrays, no anchors, no multi-line scalars — Stitch's +// schema doesn't need them and accepting them would require a real YAML +// dependency we don't want to vendor. +function parseYamlSubset(yaml) { + const lines = yaml.split(/\r?\n/); + const root = {}; + const stack = [{ indent: -1, obj: root }]; + + for (const raw of lines) { + // Skip blanks and line-only comments. Don't strip inline comments: + // unquoted hex values start with `#` and can't be safely distinguished + // from a comment after whitespace. + if (!raw.trim() || /^\s*#/.test(raw)) continue; + + const indent = raw.match(/^\s*/)[0].length; + const content = raw.slice(indent); + + const colonIdx = findTopLevelColon(content); + if (colonIdx === -1) continue; + + while (stack.length > 1 && stack[stack.length - 1].indent >= indent) { + stack.pop(); + } + + const key = content.slice(0, colonIdx).trim(); + const rest = content.slice(colonIdx + 1).trim(); + const parent = stack[stack.length - 1].obj; + + if (rest === '') { + const obj = {}; + parent[key] = obj; + stack.push({ indent, obj }); + } else { + parent[key] = parseScalar(rest); + } + } + + return root; +} + +function findTopLevelColon(s) { + let inQuote = null; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (inQuote) { + if (ch === inQuote && s[i - 1] !== '\\') inQuote = null; + } else if (ch === '"' || ch === "'") { + inQuote = ch; + } else if (ch === ':') { + return i; + } + } + return -1; +} + +function parseScalar(raw) { + const s = raw.trim(); + if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { + return s.slice(1, -1); + } + if (s === 'true') return true; + if (s === 'false') return false; + if (s === 'null' || s === '~') return null; + if (/^-?\d+$/.test(s)) return Number(s); + if (/^-?\d*\.\d+$/.test(s)) return Number(s); + return s; +} + const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g; const OKLCH_RE = /oklch\([^)]+\)/gi; const RGBA_RE = /rgba?\([^)]+\)/gi; @@ -706,10 +802,12 @@ function assessCoverage(model) { // ---------- Main ---------- export function parseDesignMd(md) { - const { title, sections } = splitSections(md); + const { frontmatter, body } = parseFrontmatter(md); + const { title, sections } = splitSections(body); return { - schemaVersion: 1, + schemaVersion: 2, title, + frontmatter, overview: extractOverview(sections['Overview']), colors: extractColors(sections['Colors']), typography: extractTypography(sections['Typography']), diff --git a/.rovodev/skills/impeccable/scripts/live-browser.js b/.rovodev/skills/impeccable/scripts/live-browser.js index 32ea9a191..d36af3536 100644 --- a/.rovodev/skills/impeccable/scripts/live-browser.js +++ b/.rovodev/skills/impeccable/scripts/live-browser.js @@ -3958,7 +3958,7 @@ void main() { function renderParsedMdCta() { const box = document.createElement('div'); box.className = 'parsed-md-cta'; - box.innerHTML = `Basic viewRunning /impeccable document generates DESIGN.json alongside your DESIGN.md, which lets this panel render your project's actual button, input, and nav primitives — not generic approximations.`; + box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running /impeccable document also generates a DESIGN.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; return box; } diff --git a/.trae-cn/skills/impeccable/reference/document.md b/.trae-cn/skills/impeccable/reference/document.md index 5e25e7129..6420693d6 100644 --- a/.trae-cn/skills/impeccable/reference/document.md +++ b/.trae-cn/skills/impeccable/reference/document.md @@ -1,8 +1,54 @@ Generate a `DESIGN.md` file at the project root that captures the current visual design system, so AI agents generating new screens stay on-brand. -DESIGN.md follows the [official Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/): exactly six sections in a fixed order. Sections may be omitted when not relevant, but **do not reorder them and do not rename them**. Section headers must match the spec character-for-character so the file stays parseable by other DESIGN.md-aware tools (Stitch itself, awesome-design-md, skill-rest, etc.). +DESIGN.md follows the [official Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/): YAML frontmatter carrying machine-readable design tokens, followed by a markdown body with exactly six sections in a fixed order. **Tokens are normative; prose provides context for how to apply them.** Sections may be omitted when not relevant, but **do not reorder them and do not rename them**. Section headers must match the spec character-for-character so the file stays parseable by other DESIGN.md-aware tools (Stitch itself, awesome-design-md, skill-rest, etc.). -## The six sections (exact order) +## The frontmatter: token schema + +The YAML frontmatter is the machine-readable layer. It's what Stitch's linter validates and what the live panel renders tiles from. Keep it tight; every entry should correspond to a token the project actually uses. + +```yaml +--- +name: +description: +colors: + primary: "#b8422e" + neutral-bg: "#faf7f2" + # ...one entry per extracted color; key = descriptive slug +typography: + display: + fontFamily: "Cormorant Garamond, Georgia, serif" + fontSize: "clamp(2.5rem, 7vw, 4.5rem)" + fontWeight: 300 + lineHeight: 1 + letterSpacing: "normal" + body: + # ... +rounded: + sm: "4px" + md: "8px" +spacing: + sm: "8px" + md: "16px" +components: + button-primary: + backgroundColor: "{colors.primary}" + textColor: "{colors.neutral-bg}" + rounded: "{rounded.sm}" + padding: "16px 48px" + button-primary-hover: + backgroundColor: "{colors.primary-deep}" +--- +``` + +Rules that matter: + +- **Token refs** use `{path.to.token}` (e.g. `{colors.primary}`, `{rounded.md}`). Components may reference primitives; primitives may not reference each other. +- **Stitch validates colors as hex sRGB only** (`#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`); OKLCH/HSL/P3 trigger a linter warning, not a hard error. YAML accepts the string either way and our own parser is format-agnostic. Choose based on project posture: (a) if the project has an "OKLCH-only" doctrine or uses Display-P3 values that don't round-trip through sRGB, put OKLCH directly in the frontmatter and accept the Stitch linter warning; (b) if the project wants strict Stitch compliance or plans to use their Tailwind/DTCG export pipeline, put hex in the frontmatter and keep OKLCH in prose as the canonical reference. Never split the source of truth without explicit reason. +- **Component sub-tokens** are limited to 8 props: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, `width`. Shadows, motion, focus rings, backdrop-filter — none of those fit. Carry them in the sidecar (Step 4b). +- **Scale keys are open-ended.** Use whatever names the project already uses (`warm-ash-cream`, `surface-container-low`). Don't rename to Material defaults. +- **Variants are naming convention, not schema.** `button-primary` / `button-primary-hover` / `button-primary-active` as sibling keys. + +## The markdown body: six sections (exact order) 1. `## Overview` 2. `## Colors` @@ -53,6 +99,17 @@ Build a structured draft from the discovered tokens. For each token class: - **Components**: For each common component (button, card, input, chip, list item, tooltip, nav), extract shape (radius), color assignment, hover/focus treatment, internal padding. - **Spacing + layout**: Fold into Overview or relevant Components. The spec does NOT have a Layout section. +### Step 2b: Stage the frontmatter + +From the auto-extracted tokens, draft the YAML frontmatter now (you'll write it at the top of DESIGN.md in Step 4). This is the machine-readable layer — what the live panel and Stitch's linter consume. + +- **Colors**: one entry per extracted color. Key = descriptive slug (`warm-ash-cream`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex — see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. +- **Typography**: one entry per role (`display`, `headline`, `title`, `body`, `label`). Typography is an object; include only the props that are real for the project (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`). +- **Rounded / Spacing**: whatever scale steps the project actually uses, keyed by whatever scale name the project uses (`sm` / `md` / `lg`, or `surface-sm`, or numeric steps). +- **Components**: one entry per variant (`button-primary`, `button-primary-hover`, `button-ghost`). Reference primitives via `{colors.X}`, `{rounded.Y}`. If a variant needs a property Stitch's 8-prop set doesn't cover (shadow, focus ring, backdrop-filter), carry the full snippet in the sidecar instead. + +Skip anything the project doesn't have. Empty scale keys or fabricated tokens pollute the spec. + ### Step 3: Ask the user for qualitative language The following require creative input that cannot be auto-extracted. Group them into one `AskUserQuestion` interaction: @@ -67,9 +124,16 @@ Quote a line from PRODUCT.md when possible so the user sees their own strategic ### Step 4: Write DESIGN.md -Use this exact structure. Headers must match character-for-character. Optional evocative subtitles (e.g. `## 2. Colors: The Coastal Palette`) are allowed. +The file opens with the YAML frontmatter staged in Step 2b (schema documented at the top of this reference), then the markdown body using the structure below. Headers must match character-for-character. Optional evocative subtitles (e.g. `## 2. Colors: The Coastal Palette`) are allowed. ```markdown +--- +name: [Project Title] +description: [one-line tagline] +colors: + # ... staged frontmatter from Step 2b +--- + # Design System: [Project Title] ## 1. Overview @@ -161,7 +225,7 @@ For each component, lead with a short character line, then specify shape, color ## 6. Do's and Don'ts -Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — include exact colors, pixel values, and named anti-patterns the user mentioned in PRODUCT.md. +Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — include exact colors, pixel values, and named anti-patterns the user mentioned in PRODUCT.md. **Every anti-reference in PRODUCT.md should show up here as a "Don't" with the same language**, so the visual spec carries the strategic line through. Quote PRODUCT.md directly where possible: if PRODUCT.md says *"avoid dark mode with purple gradients, neon accents, glassmorphism"*, the Don'ts here should repeat that by name. ### Do: - **Do** [specific prescription with exact values / named rule]. @@ -173,9 +237,9 @@ Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — i - **Don't** [...] ``` -### Step 4b: Write DESIGN.json sidecar +### Step 4b: Write DESIGN.json sidecar (extensions only) -After the Markdown is written, produce a machine-readable sidecar at `DESIGN.json` next to `DESIGN.md`. This powers the `/impeccable live` design-system panel, which renders a tile-based visualization of the system — color swatches with tonal ramps, Aa type specimens, live component previews. **The sidecar is how the panel shows *this project's* actual button/input/nav/card, not a generic approximation.** +The frontmatter owns token primitives (colors, typography, rounded, spacing, components). The sidecar at `DESIGN.json` carries **what Stitch's schema can't hold**: tonal ramps per color, shadow/elevation tokens, motion tokens, breakpoints, full component HTML/CSS snippets (the panel renders these into a shadow DOM), and narrative (north star, rules, do's/don'ts). It extends the frontmatter, it doesn't duplicate it. Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks to refresh the sidecar (e.g., from the live panel's stale-hint), preserve DESIGN.md and write only DESIGN.json. @@ -183,42 +247,32 @@ Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks ```json { - "schemaVersion": 1, + "schemaVersion": 2, "generatedAt": "ISO-8601 string", "title": "Design System: [Project Title]", - "tokens": { - "colors": [ - { - "role": "primary | secondary | tertiary | neutral | accent", - "name": "Descriptive Name", - "value": "#HEX or oklch(...) or rgba(...)", - "description": "Short role explanation (one sentence).", - "tonalRamp": ["...", "...", "..."] - } + "extensions": { + "colorMeta": { + "primary": { "role": "primary", "displayName": "Editorial Magenta", "canonical": "oklch(60% 0.25 350)", "tonalRamp": ["...", "...", "..."] }, + "warm-ash-cream": { "role": "neutral", "displayName": "Warm Ash Cream", "canonical": "oklch(96% 0.005 350)", "tonalRamp": ["...", "...", "..."] } + }, + "typographyMeta": { + "display": { "displayName": "Display", "purpose": "Hero headlines only." } + }, + "shadows": [ + { "name": "ambient-low", "value": "0 4px 24px rgba(0,0,0,0.12)", "purpose": "Diffuse hover glow under accent elements." } ], - "typography": [ - { - "role": "display | headline | title | body | label | mono", - "name": "Display", - "family": "Cormorant Garamond", - "fallback": "Georgia, serif", - "weight": 300, - "style": "normal | italic", - "sampleSize": "clamp(2.5rem, 7vw, 4.5rem) or 1rem", - "lineHeight": "1 | 1.2 | 1.6", - "letterSpacing": "normal | 0.05em", - "textTransform": "none | uppercase", - "purpose": "Short description of where this role is used." - } + "motion": [ + { "name": "ease-standard", "value": "cubic-bezier(0.4, 0, 0.2, 1)", "purpose": "Default easing for state transitions." } ], - "radii": [{ "name": "sm|md|lg|xl|full", "value": "4px" }], - "shadows": [{ "name": "Descriptive Name", "value": "0 4px 24px rgba(0,0,0,0.12)", "purpose": "..." }], - "spacing": [{ "name": "xs|sm|md|lg|xl|2xl|3xl", "value": "8px" }] + "breakpoints": [ + { "name": "sm", "value": "640px" } + ] }, "components": [ { "name": "Primary Button", "kind": "button | input | nav | chip | card | custom", + "refersTo": "button-primary", "description": "One-line what and when.", "html": "", "css": ".ds-btn-primary { background: #191c1d; color: #fff; padding: 16px 48px; letter-spacing: 0.05em; text-transform: uppercase; font-weight: 500; border: none; border-radius: 0; transition: background 0.2s, transform 0.2s; } .ds-btn-primary:hover { background: oklch(60% 0.25 350); transform: translateY(-2px); }" @@ -235,6 +289,8 @@ Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks } ``` +**What changed from schemaVersion 1.** The old sidecar carried token primitive arrays (`tokens.colors[]`, `tokens.typography[]`, etc.). Those values now live in the frontmatter. The sidecar only carries metadata that can't live in the frontmatter — tonal ramps, canonical OKLCH when the hex is an approximation, display names, role hints — keyed by the frontmatter token name (`colorMeta.`, `typographyMeta.`). Components still carry full HTML/CSS because Stitch's 8-prop set can't hold them. + #### Component translation rules The `html` and `css` fields must be **self-contained, drop-in snippets** that render correctly when injected into a shadow DOM. The panel applies them directly — no post-processing, no framework runtime. @@ -336,7 +392,7 @@ Per-section guidance in seed mode: - **Components**: omit entirely — no components exist yet. - **Do's and Don'ts**: carry PRODUCT.md's anti-references directly plus the anti-reference named in Q5. -Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens and components to render; there is nothing to show yet. The sidecar gets generated on the next Scan-mode run. +Seed mode writes a minimal frontmatter with `name` and `description` only — no colors, typography, rounded, spacing, or components yet. Real tokens land on the next Scan-mode run. Skip the `DESIGN.json` sidecar in seed mode for the same reason: nothing to render. ### Step 4: Confirm and refresh session cache @@ -346,6 +402,8 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an ## Style guidelines +- **Frontmatter first, prose second.** Tokens go in the YAML frontmatter; prose contextualizes them. Don't redefine a token value in two places — the frontmatter is normative. +- **Cite PRODUCT.md anti-references by name** in the Do's and Don'ts section. If PRODUCT.md lists "SaaS landing-page clichés" or "generic AI tool marketing" as anti-references, the DESIGN.md Don'ts should repeat those phrases verbatim so the visual spec enforces the strategic line. - **Match the spec, don't invent new sections.** The six section names are fixed. If you have Layout/Motion/Responsive content to document, fold it into Overview (philosophy-level rules) or Components (per-component behavior). - **Descriptive > technical**: "Gently curved edges (8px radius)" > "rounded-lg". Include the technical value in parens, lead with the description. - **Functional > decorative**: for each token, explain WHERE and WHY it's used, not just WHAT it is. @@ -365,3 +423,5 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an - Don't duplicate content from PRODUCT.md. DESIGN.md is strictly visual. - Don't add a "Layout Principles" or "Motion" or "Responsive Behavior" top-level section. The spec has six, not nine. Fold that content where it belongs. - Don't rename sections even slightly. "Colors" not "Color Palette & Roles". "Typography" not "Typography Rules". Tooling parsing depends on exact headers. +- Don't duplicate token values between frontmatter and prose. If a color is in `colors.primary` as hex, the prose can name it and describe its role but should not reassert a different hex. The frontmatter is normative. +- Don't invent frontmatter token groups outside Stitch's schema (no `motion:`, `breakpoints:`, `shadows:` at the top level). Stitch's Zod schema only accepts `colors`, `typography`, `rounded`, `spacing`, `components`. Anything else belongs in the sidecar's `extensions`. diff --git a/.trae-cn/skills/impeccable/scripts/design-parser.mjs b/.trae-cn/skills/impeccable/scripts/design-parser.mjs index 6220f67e7..b68756640 100644 --- a/.trae-cn/skills/impeccable/scripts/design-parser.mjs +++ b/.trae-cn/skills/impeccable/scripts/design-parser.mjs @@ -1,5 +1,10 @@ // Parse a DESIGN.md (Stitch-spec format) into a structured JSON model that // the live-mode design-system panel can render. Deterministic, dependency-free. +// +// Two-layer: YAML frontmatter (machine-readable tokens) + markdown body +// (prose with six canonical H2 sections). When frontmatter is present, it's +// exposed on `model.frontmatter` alongside the prose-scraped sections; +// consumers can prefer frontmatter values and fall back to prose. const CANONICAL_SECTIONS = [ 'Overview', @@ -10,6 +15,97 @@ const CANONICAL_SECTIONS = [ "Do's and Don'ts", ]; +// ---------- Frontmatter (Stitch YAML subset) ---------- + +function parseFrontmatter(md) { + const lines = md.split(/\r?\n/); + if (lines[0]?.trim() !== '---') return { frontmatter: null, body: md }; + + let end = -1; + for (let i = 1; i < lines.length; i++) { + if (lines[i].trim() === '---') { end = i; break; } + } + if (end === -1) return { frontmatter: null, body: md }; + + const yaml = lines.slice(1, end).join('\n'); + const body = lines.slice(end + 1).join('\n'); + try { + return { frontmatter: parseYamlSubset(yaml), body }; + } catch { + return { frontmatter: null, body: md }; + } +} + +// Minimal YAML reader for the Stitch frontmatter subset: scalar maps with +// one level of nested objects (typography roles, components). Indent-based, +// 2-space convention. No arrays, no anchors, no multi-line scalars — Stitch's +// schema doesn't need them and accepting them would require a real YAML +// dependency we don't want to vendor. +function parseYamlSubset(yaml) { + const lines = yaml.split(/\r?\n/); + const root = {}; + const stack = [{ indent: -1, obj: root }]; + + for (const raw of lines) { + // Skip blanks and line-only comments. Don't strip inline comments: + // unquoted hex values start with `#` and can't be safely distinguished + // from a comment after whitespace. + if (!raw.trim() || /^\s*#/.test(raw)) continue; + + const indent = raw.match(/^\s*/)[0].length; + const content = raw.slice(indent); + + const colonIdx = findTopLevelColon(content); + if (colonIdx === -1) continue; + + while (stack.length > 1 && stack[stack.length - 1].indent >= indent) { + stack.pop(); + } + + const key = content.slice(0, colonIdx).trim(); + const rest = content.slice(colonIdx + 1).trim(); + const parent = stack[stack.length - 1].obj; + + if (rest === '') { + const obj = {}; + parent[key] = obj; + stack.push({ indent, obj }); + } else { + parent[key] = parseScalar(rest); + } + } + + return root; +} + +function findTopLevelColon(s) { + let inQuote = null; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (inQuote) { + if (ch === inQuote && s[i - 1] !== '\\') inQuote = null; + } else if (ch === '"' || ch === "'") { + inQuote = ch; + } else if (ch === ':') { + return i; + } + } + return -1; +} + +function parseScalar(raw) { + const s = raw.trim(); + if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { + return s.slice(1, -1); + } + if (s === 'true') return true; + if (s === 'false') return false; + if (s === 'null' || s === '~') return null; + if (/^-?\d+$/.test(s)) return Number(s); + if (/^-?\d*\.\d+$/.test(s)) return Number(s); + return s; +} + const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g; const OKLCH_RE = /oklch\([^)]+\)/gi; const RGBA_RE = /rgba?\([^)]+\)/gi; @@ -706,10 +802,12 @@ function assessCoverage(model) { // ---------- Main ---------- export function parseDesignMd(md) { - const { title, sections } = splitSections(md); + const { frontmatter, body } = parseFrontmatter(md); + const { title, sections } = splitSections(body); return { - schemaVersion: 1, + schemaVersion: 2, title, + frontmatter, overview: extractOverview(sections['Overview']), colors: extractColors(sections['Colors']), typography: extractTypography(sections['Typography']), diff --git a/.trae-cn/skills/impeccable/scripts/live-browser.js b/.trae-cn/skills/impeccable/scripts/live-browser.js index 32ea9a191..d36af3536 100644 --- a/.trae-cn/skills/impeccable/scripts/live-browser.js +++ b/.trae-cn/skills/impeccable/scripts/live-browser.js @@ -3958,7 +3958,7 @@ void main() { function renderParsedMdCta() { const box = document.createElement('div'); box.className = 'parsed-md-cta'; - box.innerHTML = `Basic viewRunning /impeccable document generates DESIGN.json alongside your DESIGN.md, which lets this panel render your project's actual button, input, and nav primitives — not generic approximations.`; + box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running /impeccable document also generates a DESIGN.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; return box; } diff --git a/.trae/skills/impeccable/reference/document.md b/.trae/skills/impeccable/reference/document.md index 6b0b3b224..453d346a8 100644 --- a/.trae/skills/impeccable/reference/document.md +++ b/.trae/skills/impeccable/reference/document.md @@ -1,8 +1,54 @@ Generate a `DESIGN.md` file at the project root that captures the current visual design system, so AI agents generating new screens stay on-brand. -DESIGN.md follows the [official Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/): exactly six sections in a fixed order. Sections may be omitted when not relevant, but **do not reorder them and do not rename them**. Section headers must match the spec character-for-character so the file stays parseable by other DESIGN.md-aware tools (Stitch itself, awesome-design-md, skill-rest, etc.). +DESIGN.md follows the [official Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/): YAML frontmatter carrying machine-readable design tokens, followed by a markdown body with exactly six sections in a fixed order. **Tokens are normative; prose provides context for how to apply them.** Sections may be omitted when not relevant, but **do not reorder them and do not rename them**. Section headers must match the spec character-for-character so the file stays parseable by other DESIGN.md-aware tools (Stitch itself, awesome-design-md, skill-rest, etc.). -## The six sections (exact order) +## The frontmatter: token schema + +The YAML frontmatter is the machine-readable layer. It's what Stitch's linter validates and what the live panel renders tiles from. Keep it tight; every entry should correspond to a token the project actually uses. + +```yaml +--- +name: +description: +colors: + primary: "#b8422e" + neutral-bg: "#faf7f2" + # ...one entry per extracted color; key = descriptive slug +typography: + display: + fontFamily: "Cormorant Garamond, Georgia, serif" + fontSize: "clamp(2.5rem, 7vw, 4.5rem)" + fontWeight: 300 + lineHeight: 1 + letterSpacing: "normal" + body: + # ... +rounded: + sm: "4px" + md: "8px" +spacing: + sm: "8px" + md: "16px" +components: + button-primary: + backgroundColor: "{colors.primary}" + textColor: "{colors.neutral-bg}" + rounded: "{rounded.sm}" + padding: "16px 48px" + button-primary-hover: + backgroundColor: "{colors.primary-deep}" +--- +``` + +Rules that matter: + +- **Token refs** use `{path.to.token}` (e.g. `{colors.primary}`, `{rounded.md}`). Components may reference primitives; primitives may not reference each other. +- **Stitch validates colors as hex sRGB only** (`#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`); OKLCH/HSL/P3 trigger a linter warning, not a hard error. YAML accepts the string either way and our own parser is format-agnostic. Choose based on project posture: (a) if the project has an "OKLCH-only" doctrine or uses Display-P3 values that don't round-trip through sRGB, put OKLCH directly in the frontmatter and accept the Stitch linter warning; (b) if the project wants strict Stitch compliance or plans to use their Tailwind/DTCG export pipeline, put hex in the frontmatter and keep OKLCH in prose as the canonical reference. Never split the source of truth without explicit reason. +- **Component sub-tokens** are limited to 8 props: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, `width`. Shadows, motion, focus rings, backdrop-filter — none of those fit. Carry them in the sidecar (Step 4b). +- **Scale keys are open-ended.** Use whatever names the project already uses (`warm-ash-cream`, `surface-container-low`). Don't rename to Material defaults. +- **Variants are naming convention, not schema.** `button-primary` / `button-primary-hover` / `button-primary-active` as sibling keys. + +## The markdown body: six sections (exact order) 1. `## Overview` 2. `## Colors` @@ -53,6 +99,17 @@ Build a structured draft from the discovered tokens. For each token class: - **Components**: For each common component (button, card, input, chip, list item, tooltip, nav), extract shape (radius), color assignment, hover/focus treatment, internal padding. - **Spacing + layout**: Fold into Overview or relevant Components. The spec does NOT have a Layout section. +### Step 2b: Stage the frontmatter + +From the auto-extracted tokens, draft the YAML frontmatter now (you'll write it at the top of DESIGN.md in Step 4). This is the machine-readable layer — what the live panel and Stitch's linter consume. + +- **Colors**: one entry per extracted color. Key = descriptive slug (`warm-ash-cream`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex — see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. +- **Typography**: one entry per role (`display`, `headline`, `title`, `body`, `label`). Typography is an object; include only the props that are real for the project (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`). +- **Rounded / Spacing**: whatever scale steps the project actually uses, keyed by whatever scale name the project uses (`sm` / `md` / `lg`, or `surface-sm`, or numeric steps). +- **Components**: one entry per variant (`button-primary`, `button-primary-hover`, `button-ghost`). Reference primitives via `{colors.X}`, `{rounded.Y}`. If a variant needs a property Stitch's 8-prop set doesn't cover (shadow, focus ring, backdrop-filter), carry the full snippet in the sidecar instead. + +Skip anything the project doesn't have. Empty scale keys or fabricated tokens pollute the spec. + ### Step 3: Ask the user for qualitative language The following require creative input that cannot be auto-extracted. Group them into one `AskUserQuestion` interaction: @@ -67,9 +124,16 @@ Quote a line from PRODUCT.md when possible so the user sees their own strategic ### Step 4: Write DESIGN.md -Use this exact structure. Headers must match character-for-character. Optional evocative subtitles (e.g. `## 2. Colors: The Coastal Palette`) are allowed. +The file opens with the YAML frontmatter staged in Step 2b (schema documented at the top of this reference), then the markdown body using the structure below. Headers must match character-for-character. Optional evocative subtitles (e.g. `## 2. Colors: The Coastal Palette`) are allowed. ```markdown +--- +name: [Project Title] +description: [one-line tagline] +colors: + # ... staged frontmatter from Step 2b +--- + # Design System: [Project Title] ## 1. Overview @@ -161,7 +225,7 @@ For each component, lead with a short character line, then specify shape, color ## 6. Do's and Don'ts -Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — include exact colors, pixel values, and named anti-patterns the user mentioned in PRODUCT.md. +Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — include exact colors, pixel values, and named anti-patterns the user mentioned in PRODUCT.md. **Every anti-reference in PRODUCT.md should show up here as a "Don't" with the same language**, so the visual spec carries the strategic line through. Quote PRODUCT.md directly where possible: if PRODUCT.md says *"avoid dark mode with purple gradients, neon accents, glassmorphism"*, the Don'ts here should repeat that by name. ### Do: - **Do** [specific prescription with exact values / named rule]. @@ -173,9 +237,9 @@ Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — i - **Don't** [...] ``` -### Step 4b: Write DESIGN.json sidecar +### Step 4b: Write DESIGN.json sidecar (extensions only) -After the Markdown is written, produce a machine-readable sidecar at `DESIGN.json` next to `DESIGN.md`. This powers the `/impeccable live` design-system panel, which renders a tile-based visualization of the system — color swatches with tonal ramps, Aa type specimens, live component previews. **The sidecar is how the panel shows *this project's* actual button/input/nav/card, not a generic approximation.** +The frontmatter owns token primitives (colors, typography, rounded, spacing, components). The sidecar at `DESIGN.json` carries **what Stitch's schema can't hold**: tonal ramps per color, shadow/elevation tokens, motion tokens, breakpoints, full component HTML/CSS snippets (the panel renders these into a shadow DOM), and narrative (north star, rules, do's/don'ts). It extends the frontmatter, it doesn't duplicate it. Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks to refresh the sidecar (e.g., from the live panel's stale-hint), preserve DESIGN.md and write only DESIGN.json. @@ -183,42 +247,32 @@ Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks ```json { - "schemaVersion": 1, + "schemaVersion": 2, "generatedAt": "ISO-8601 string", "title": "Design System: [Project Title]", - "tokens": { - "colors": [ - { - "role": "primary | secondary | tertiary | neutral | accent", - "name": "Descriptive Name", - "value": "#HEX or oklch(...) or rgba(...)", - "description": "Short role explanation (one sentence).", - "tonalRamp": ["...", "...", "..."] - } + "extensions": { + "colorMeta": { + "primary": { "role": "primary", "displayName": "Editorial Magenta", "canonical": "oklch(60% 0.25 350)", "tonalRamp": ["...", "...", "..."] }, + "warm-ash-cream": { "role": "neutral", "displayName": "Warm Ash Cream", "canonical": "oklch(96% 0.005 350)", "tonalRamp": ["...", "...", "..."] } + }, + "typographyMeta": { + "display": { "displayName": "Display", "purpose": "Hero headlines only." } + }, + "shadows": [ + { "name": "ambient-low", "value": "0 4px 24px rgba(0,0,0,0.12)", "purpose": "Diffuse hover glow under accent elements." } ], - "typography": [ - { - "role": "display | headline | title | body | label | mono", - "name": "Display", - "family": "Cormorant Garamond", - "fallback": "Georgia, serif", - "weight": 300, - "style": "normal | italic", - "sampleSize": "clamp(2.5rem, 7vw, 4.5rem) or 1rem", - "lineHeight": "1 | 1.2 | 1.6", - "letterSpacing": "normal | 0.05em", - "textTransform": "none | uppercase", - "purpose": "Short description of where this role is used." - } + "motion": [ + { "name": "ease-standard", "value": "cubic-bezier(0.4, 0, 0.2, 1)", "purpose": "Default easing for state transitions." } ], - "radii": [{ "name": "sm|md|lg|xl|full", "value": "4px" }], - "shadows": [{ "name": "Descriptive Name", "value": "0 4px 24px rgba(0,0,0,0.12)", "purpose": "..." }], - "spacing": [{ "name": "xs|sm|md|lg|xl|2xl|3xl", "value": "8px" }] + "breakpoints": [ + { "name": "sm", "value": "640px" } + ] }, "components": [ { "name": "Primary Button", "kind": "button | input | nav | chip | card | custom", + "refersTo": "button-primary", "description": "One-line what and when.", "html": "", "css": ".ds-btn-primary { background: #191c1d; color: #fff; padding: 16px 48px; letter-spacing: 0.05em; text-transform: uppercase; font-weight: 500; border: none; border-radius: 0; transition: background 0.2s, transform 0.2s; } .ds-btn-primary:hover { background: oklch(60% 0.25 350); transform: translateY(-2px); }" @@ -235,6 +289,8 @@ Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks } ``` +**What changed from schemaVersion 1.** The old sidecar carried token primitive arrays (`tokens.colors[]`, `tokens.typography[]`, etc.). Those values now live in the frontmatter. The sidecar only carries metadata that can't live in the frontmatter — tonal ramps, canonical OKLCH when the hex is an approximation, display names, role hints — keyed by the frontmatter token name (`colorMeta.`, `typographyMeta.`). Components still carry full HTML/CSS because Stitch's 8-prop set can't hold them. + #### Component translation rules The `html` and `css` fields must be **self-contained, drop-in snippets** that render correctly when injected into a shadow DOM. The panel applies them directly — no post-processing, no framework runtime. @@ -336,7 +392,7 @@ Per-section guidance in seed mode: - **Components**: omit entirely — no components exist yet. - **Do's and Don'ts**: carry PRODUCT.md's anti-references directly plus the anti-reference named in Q5. -Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens and components to render; there is nothing to show yet. The sidecar gets generated on the next Scan-mode run. +Seed mode writes a minimal frontmatter with `name` and `description` only — no colors, typography, rounded, spacing, or components yet. Real tokens land on the next Scan-mode run. Skip the `DESIGN.json` sidecar in seed mode for the same reason: nothing to render. ### Step 4: Confirm and refresh session cache @@ -346,6 +402,8 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an ## Style guidelines +- **Frontmatter first, prose second.** Tokens go in the YAML frontmatter; prose contextualizes them. Don't redefine a token value in two places — the frontmatter is normative. +- **Cite PRODUCT.md anti-references by name** in the Do's and Don'ts section. If PRODUCT.md lists "SaaS landing-page clichés" or "generic AI tool marketing" as anti-references, the DESIGN.md Don'ts should repeat those phrases verbatim so the visual spec enforces the strategic line. - **Match the spec, don't invent new sections.** The six section names are fixed. If you have Layout/Motion/Responsive content to document, fold it into Overview (philosophy-level rules) or Components (per-component behavior). - **Descriptive > technical**: "Gently curved edges (8px radius)" > "rounded-lg". Include the technical value in parens, lead with the description. - **Functional > decorative**: for each token, explain WHERE and WHY it's used, not just WHAT it is. @@ -365,3 +423,5 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an - Don't duplicate content from PRODUCT.md. DESIGN.md is strictly visual. - Don't add a "Layout Principles" or "Motion" or "Responsive Behavior" top-level section. The spec has six, not nine. Fold that content where it belongs. - Don't rename sections even slightly. "Colors" not "Color Palette & Roles". "Typography" not "Typography Rules". Tooling parsing depends on exact headers. +- Don't duplicate token values between frontmatter and prose. If a color is in `colors.primary` as hex, the prose can name it and describe its role but should not reassert a different hex. The frontmatter is normative. +- Don't invent frontmatter token groups outside Stitch's schema (no `motion:`, `breakpoints:`, `shadows:` at the top level). Stitch's Zod schema only accepts `colors`, `typography`, `rounded`, `spacing`, `components`. Anything else belongs in the sidecar's `extensions`. diff --git a/.trae/skills/impeccable/scripts/design-parser.mjs b/.trae/skills/impeccable/scripts/design-parser.mjs index 6220f67e7..b68756640 100644 --- a/.trae/skills/impeccable/scripts/design-parser.mjs +++ b/.trae/skills/impeccable/scripts/design-parser.mjs @@ -1,5 +1,10 @@ // Parse a DESIGN.md (Stitch-spec format) into a structured JSON model that // the live-mode design-system panel can render. Deterministic, dependency-free. +// +// Two-layer: YAML frontmatter (machine-readable tokens) + markdown body +// (prose with six canonical H2 sections). When frontmatter is present, it's +// exposed on `model.frontmatter` alongside the prose-scraped sections; +// consumers can prefer frontmatter values and fall back to prose. const CANONICAL_SECTIONS = [ 'Overview', @@ -10,6 +15,97 @@ const CANONICAL_SECTIONS = [ "Do's and Don'ts", ]; +// ---------- Frontmatter (Stitch YAML subset) ---------- + +function parseFrontmatter(md) { + const lines = md.split(/\r?\n/); + if (lines[0]?.trim() !== '---') return { frontmatter: null, body: md }; + + let end = -1; + for (let i = 1; i < lines.length; i++) { + if (lines[i].trim() === '---') { end = i; break; } + } + if (end === -1) return { frontmatter: null, body: md }; + + const yaml = lines.slice(1, end).join('\n'); + const body = lines.slice(end + 1).join('\n'); + try { + return { frontmatter: parseYamlSubset(yaml), body }; + } catch { + return { frontmatter: null, body: md }; + } +} + +// Minimal YAML reader for the Stitch frontmatter subset: scalar maps with +// one level of nested objects (typography roles, components). Indent-based, +// 2-space convention. No arrays, no anchors, no multi-line scalars — Stitch's +// schema doesn't need them and accepting them would require a real YAML +// dependency we don't want to vendor. +function parseYamlSubset(yaml) { + const lines = yaml.split(/\r?\n/); + const root = {}; + const stack = [{ indent: -1, obj: root }]; + + for (const raw of lines) { + // Skip blanks and line-only comments. Don't strip inline comments: + // unquoted hex values start with `#` and can't be safely distinguished + // from a comment after whitespace. + if (!raw.trim() || /^\s*#/.test(raw)) continue; + + const indent = raw.match(/^\s*/)[0].length; + const content = raw.slice(indent); + + const colonIdx = findTopLevelColon(content); + if (colonIdx === -1) continue; + + while (stack.length > 1 && stack[stack.length - 1].indent >= indent) { + stack.pop(); + } + + const key = content.slice(0, colonIdx).trim(); + const rest = content.slice(colonIdx + 1).trim(); + const parent = stack[stack.length - 1].obj; + + if (rest === '') { + const obj = {}; + parent[key] = obj; + stack.push({ indent, obj }); + } else { + parent[key] = parseScalar(rest); + } + } + + return root; +} + +function findTopLevelColon(s) { + let inQuote = null; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (inQuote) { + if (ch === inQuote && s[i - 1] !== '\\') inQuote = null; + } else if (ch === '"' || ch === "'") { + inQuote = ch; + } else if (ch === ':') { + return i; + } + } + return -1; +} + +function parseScalar(raw) { + const s = raw.trim(); + if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { + return s.slice(1, -1); + } + if (s === 'true') return true; + if (s === 'false') return false; + if (s === 'null' || s === '~') return null; + if (/^-?\d+$/.test(s)) return Number(s); + if (/^-?\d*\.\d+$/.test(s)) return Number(s); + return s; +} + const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g; const OKLCH_RE = /oklch\([^)]+\)/gi; const RGBA_RE = /rgba?\([^)]+\)/gi; @@ -706,10 +802,12 @@ function assessCoverage(model) { // ---------- Main ---------- export function parseDesignMd(md) { - const { title, sections } = splitSections(md); + const { frontmatter, body } = parseFrontmatter(md); + const { title, sections } = splitSections(body); return { - schemaVersion: 1, + schemaVersion: 2, title, + frontmatter, overview: extractOverview(sections['Overview']), colors: extractColors(sections['Colors']), typography: extractTypography(sections['Typography']), diff --git a/.trae/skills/impeccable/scripts/live-browser.js b/.trae/skills/impeccable/scripts/live-browser.js index 32ea9a191..d36af3536 100644 --- a/.trae/skills/impeccable/scripts/live-browser.js +++ b/.trae/skills/impeccable/scripts/live-browser.js @@ -3958,7 +3958,7 @@ void main() { function renderParsedMdCta() { const box = document.createElement('div'); box.className = 'parsed-md-cta'; - box.innerHTML = `Basic viewRunning /impeccable document generates DESIGN.json alongside your DESIGN.md, which lets this panel render your project's actual button, input, and nav primitives — not generic approximations.`; + box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running /impeccable document also generates a DESIGN.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; return box; } diff --git a/DESIGN.md b/DESIGN.md index 5b4be58fe..4f4e052aa 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -1,3 +1,116 @@ +--- +name: Impeccable +description: Warm-paper editorial sanctuary — committed serif display, one decisive magenta, flat surfaces at rest. + +# Colors use OKLCH per `The OKLCH-Only Rule` in §2. Stitch's linter validates +# hex sRGB only, so it will warn on these entries — deliberate trade for one +# source of truth and full wide-gamut fidelity. Our own parser accepts strings. +colors: + editorial-magenta: "oklch(60% 0.25 350)" + editorial-magenta-deep: "oklch(52% 0.25 350)" + warm-ash-cream: "oklch(96% 0.005 350)" + crisp-paper-white: "oklch(98% 0 0)" + deep-graphite: "oklch(10% 0 0)" + soft-charcoal: "oklch(25% 0 0)" + mid-ash: "oklch(55% 0 0)" + paper-mist: "oklch(92% 0 0)" + magenta-whisper: "oklch(60% 0.25 350 / 0.15)" + magenta-veil: "oklch(60% 0.25 350 / 0.25)" + +typography: + display: + fontFamily: "Cormorant Garamond, Georgia, serif" + fontSize: "clamp(2.5rem, 7vw, 4.5rem)" + fontWeight: 300 + lineHeight: 1 + headline: + fontFamily: "Cormorant Garamond, Georgia, serif" + fontSize: "clamp(1.75rem, 4vw, 2.5rem)" + fontWeight: 400 + lineHeight: 1.2 + title: + fontFamily: "Cormorant Garamond, Georgia, serif" + fontSize: "clamp(1.125rem, 2.5vw, 1.75rem)" + fontWeight: 400 + lineHeight: 1.3 + body: + fontFamily: "Instrument Sans, system-ui, sans-serif" + fontSize: "1rem" + fontWeight: 400 + lineHeight: 1.6 + body-lead: + fontFamily: "Instrument Sans, system-ui, sans-serif" + fontSize: "1.0625rem" + fontWeight: 400 + lineHeight: 1.65 + supporting: + fontFamily: "Instrument Sans, system-ui, sans-serif" + fontSize: "0.875rem" + fontWeight: 400 + lineHeight: 1.6 + label: + fontFamily: "Instrument Sans, system-ui, sans-serif" + fontSize: "0.9rem" + fontWeight: 500 + letterSpacing: "0.05em" + micro-label: + fontFamily: "Instrument Sans, system-ui, sans-serif" + fontSize: "0.6875rem" + fontWeight: 500 + letterSpacing: "0.1em" + mono: + fontFamily: "Space Grotesk, monospace" + fontSize: "0.75rem" + fontWeight: 400 + +rounded: + none: "0" + sm: "4px" + md: "8px" + lg: "12px" + xl: "16px" + +spacing: + xs: "8px" + sm: "16px" + md: "24px" + lg: "32px" + xl: "48px" + "2xl": "80px" + "3xl": "120px" + +components: + button-primary: + backgroundColor: "{colors.deep-graphite}" + textColor: "{colors.crisp-paper-white}" + typography: "{typography.label}" + rounded: "{rounded.none}" + padding: "16px 48px" + button-primary-hover: + backgroundColor: "{colors.editorial-magenta}" + textColor: "{colors.crisp-paper-white}" + input-text: + backgroundColor: "transparent" + textColor: "{colors.deep-graphite}" + rounded: "{rounded.sm}" + padding: "8px 12px" + card: + backgroundColor: "{colors.warm-ash-cream}" + textColor: "{colors.deep-graphite}" + rounded: "{rounded.md}" + padding: "24px" + card-feature: + backgroundColor: "{colors.crisp-paper-white}" + textColor: "{colors.deep-graphite}" + rounded: "{rounded.lg}" + padding: "48px" + nav-link: + textColor: "{colors.deep-graphite}" + typography: "{typography.body}" + nav-link-hover: + textColor: "{colors.editorial-magenta}" +--- + # Design System: Impeccable ## 1. Overview: The Editorial Sanctuary diff --git a/source/skills/impeccable/reference/document.md b/source/skills/impeccable/reference/document.md index abb9c4346..c1191b1f2 100644 --- a/source/skills/impeccable/reference/document.md +++ b/source/skills/impeccable/reference/document.md @@ -1,8 +1,54 @@ Generate a `DESIGN.md` file at the project root that captures the current visual design system, so AI agents generating new screens stay on-brand. -DESIGN.md follows the [official Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/): exactly six sections in a fixed order. Sections may be omitted when not relevant, but **do not reorder them and do not rename them**. Section headers must match the spec character-for-character so the file stays parseable by other DESIGN.md-aware tools (Stitch itself, awesome-design-md, skill-rest, etc.). +DESIGN.md follows the [official Google Stitch DESIGN.md format](https://stitch.withgoogle.com/docs/design-md/format/): YAML frontmatter carrying machine-readable design tokens, followed by a markdown body with exactly six sections in a fixed order. **Tokens are normative; prose provides context for how to apply them.** Sections may be omitted when not relevant, but **do not reorder them and do not rename them**. Section headers must match the spec character-for-character so the file stays parseable by other DESIGN.md-aware tools (Stitch itself, awesome-design-md, skill-rest, etc.). -## The six sections (exact order) +## The frontmatter: token schema + +The YAML frontmatter is the machine-readable layer. It's what Stitch's linter validates and what the live panel renders tiles from. Keep it tight; every entry should correspond to a token the project actually uses. + +```yaml +--- +name: +description: +colors: + primary: "#b8422e" + neutral-bg: "#faf7f2" + # ...one entry per extracted color; key = descriptive slug +typography: + display: + fontFamily: "Cormorant Garamond, Georgia, serif" + fontSize: "clamp(2.5rem, 7vw, 4.5rem)" + fontWeight: 300 + lineHeight: 1 + letterSpacing: "normal" + body: + # ... +rounded: + sm: "4px" + md: "8px" +spacing: + sm: "8px" + md: "16px" +components: + button-primary: + backgroundColor: "{colors.primary}" + textColor: "{colors.neutral-bg}" + rounded: "{rounded.sm}" + padding: "16px 48px" + button-primary-hover: + backgroundColor: "{colors.primary-deep}" +--- +``` + +Rules that matter: + +- **Token refs** use `{path.to.token}` (e.g. `{colors.primary}`, `{rounded.md}`). Components may reference primitives; primitives may not reference each other. +- **Stitch validates colors as hex sRGB only** (`#RGB` / `#RGBA` / `#RRGGBB` / `#RRGGBBAA`); OKLCH/HSL/P3 trigger a linter warning, not a hard error. YAML accepts the string either way and our own parser is format-agnostic. Choose based on project posture: (a) if the project has an "OKLCH-only" doctrine or uses Display-P3 values that don't round-trip through sRGB, put OKLCH directly in the frontmatter and accept the Stitch linter warning; (b) if the project wants strict Stitch compliance or plans to use their Tailwind/DTCG export pipeline, put hex in the frontmatter and keep OKLCH in prose as the canonical reference. Never split the source of truth without explicit reason. +- **Component sub-tokens** are limited to 8 props: `backgroundColor`, `textColor`, `typography`, `rounded`, `padding`, `size`, `height`, `width`. Shadows, motion, focus rings, backdrop-filter — none of those fit. Carry them in the sidecar (Step 4b). +- **Scale keys are open-ended.** Use whatever names the project already uses (`warm-ash-cream`, `surface-container-low`). Don't rename to Material defaults. +- **Variants are naming convention, not schema.** `button-primary` / `button-primary-hover` / `button-primary-active` as sibling keys. + +## The markdown body: six sections (exact order) 1. `## Overview` 2. `## Colors` @@ -53,6 +99,17 @@ Build a structured draft from the discovered tokens. For each token class: - **Components**: For each common component (button, card, input, chip, list item, tooltip, nav), extract shape (radius), color assignment, hover/focus treatment, internal padding. - **Spacing + layout**: Fold into Overview or relevant Components. The spec does NOT have a Layout section. +### Step 2b: Stage the frontmatter + +From the auto-extracted tokens, draft the YAML frontmatter now (you'll write it at the top of DESIGN.md in Step 4). This is the machine-readable layer — what the live panel and Stitch's linter consume. + +- **Colors**: one entry per extracted color. Key = descriptive slug (`warm-ash-cream`, `editorial-magenta`, not `blue-800`). Value = whichever format the project treats as canonical (OKLCH or hex — see the frontmatter rules above). Don't split the source of truth: one format in the frontmatter, don't redefine the same token in prose with a different value. +- **Typography**: one entry per role (`display`, `headline`, `title`, `body`, `label`). Typography is an object; include only the props that are real for the project (`fontFamily`, `fontSize`, `fontWeight`, `lineHeight`, `letterSpacing`, `fontFeature`, `fontVariation`). +- **Rounded / Spacing**: whatever scale steps the project actually uses, keyed by whatever scale name the project uses (`sm` / `md` / `lg`, or `surface-sm`, or numeric steps). +- **Components**: one entry per variant (`button-primary`, `button-primary-hover`, `button-ghost`). Reference primitives via `{colors.X}`, `{rounded.Y}`. If a variant needs a property Stitch's 8-prop set doesn't cover (shadow, focus ring, backdrop-filter), carry the full snippet in the sidecar instead. + +Skip anything the project doesn't have. Empty scale keys or fabricated tokens pollute the spec. + ### Step 3: Ask the user for qualitative language The following require creative input that cannot be auto-extracted. Group them into one `AskUserQuestion` interaction: @@ -67,9 +124,16 @@ Quote a line from PRODUCT.md when possible so the user sees their own strategic ### Step 4: Write DESIGN.md -Use this exact structure. Headers must match character-for-character. Optional evocative subtitles (e.g. `## 2. Colors: The Coastal Palette`) are allowed. +The file opens with the YAML frontmatter staged in Step 2b (schema documented at the top of this reference), then the markdown body using the structure below. Headers must match character-for-character. Optional evocative subtitles (e.g. `## 2. Colors: The Coastal Palette`) are allowed. ```markdown +--- +name: [Project Title] +description: [one-line tagline] +colors: + # ... staged frontmatter from Step 2b +--- + # Design System: [Project Title] ## 1. Overview @@ -161,7 +225,7 @@ For each component, lead with a short character line, then specify shape, color ## 6. Do's and Don'ts -Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — include exact colors, pixel values, and named anti-patterns the user mentioned in PRODUCT.md. +Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — include exact colors, pixel values, and named anti-patterns the user mentioned in PRODUCT.md. **Every anti-reference in PRODUCT.md should show up here as a "Don't" with the same language**, so the visual spec carries the strategic line through. Quote PRODUCT.md directly where possible: if PRODUCT.md says *"avoid dark mode with purple gradients, neon accents, glassmorphism"*, the Don'ts here should repeat that by name. ### Do: - **Do** [specific prescription with exact values / named rule]. @@ -173,9 +237,9 @@ Concrete, forceful guardrails. Lead each with "Do" or "Don't". Be specific — i - **Don't** [...] ``` -### Step 4b: Write DESIGN.json sidecar +### Step 4b: Write DESIGN.json sidecar (extensions only) -After the Markdown is written, produce a machine-readable sidecar at `DESIGN.json` next to `DESIGN.md`. This powers the `/impeccable live` design-system panel, which renders a tile-based visualization of the system — color swatches with tonal ramps, Aa type specimens, live component previews. **The sidecar is how the panel shows *this project's* actual button/input/nav/card, not a generic approximation.** +The frontmatter owns token primitives (colors, typography, rounded, spacing, components). The sidecar at `DESIGN.json` carries **what Stitch's schema can't hold**: tonal ramps per color, shadow/elevation tokens, motion tokens, breakpoints, full component HTML/CSS snippets (the panel renders these into a shadow DOM), and narrative (north star, rules, do's/don'ts). It extends the frontmatter, it doesn't duplicate it. Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks to refresh the sidecar (e.g., from the live panel's stale-hint), preserve DESIGN.md and write only DESIGN.json. @@ -183,42 +247,32 @@ Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks ```json { - "schemaVersion": 1, + "schemaVersion": 2, "generatedAt": "ISO-8601 string", "title": "Design System: [Project Title]", - "tokens": { - "colors": [ - { - "role": "primary | secondary | tertiary | neutral | accent", - "name": "Descriptive Name", - "value": "#HEX or oklch(...) or rgba(...)", - "description": "Short role explanation (one sentence).", - "tonalRamp": ["...", "...", "..."] - } + "extensions": { + "colorMeta": { + "primary": { "role": "primary", "displayName": "Editorial Magenta", "canonical": "oklch(60% 0.25 350)", "tonalRamp": ["...", "...", "..."] }, + "warm-ash-cream": { "role": "neutral", "displayName": "Warm Ash Cream", "canonical": "oklch(96% 0.005 350)", "tonalRamp": ["...", "...", "..."] } + }, + "typographyMeta": { + "display": { "displayName": "Display", "purpose": "Hero headlines only." } + }, + "shadows": [ + { "name": "ambient-low", "value": "0 4px 24px rgba(0,0,0,0.12)", "purpose": "Diffuse hover glow under accent elements." } ], - "typography": [ - { - "role": "display | headline | title | body | label | mono", - "name": "Display", - "family": "Cormorant Garamond", - "fallback": "Georgia, serif", - "weight": 300, - "style": "normal | italic", - "sampleSize": "clamp(2.5rem, 7vw, 4.5rem) or 1rem", - "lineHeight": "1 | 1.2 | 1.6", - "letterSpacing": "normal | 0.05em", - "textTransform": "none | uppercase", - "purpose": "Short description of where this role is used." - } + "motion": [ + { "name": "ease-standard", "value": "cubic-bezier(0.4, 0, 0.2, 1)", "purpose": "Default easing for state transitions." } ], - "radii": [{ "name": "sm|md|lg|xl|full", "value": "4px" }], - "shadows": [{ "name": "Descriptive Name", "value": "0 4px 24px rgba(0,0,0,0.12)", "purpose": "..." }], - "spacing": [{ "name": "xs|sm|md|lg|xl|2xl|3xl", "value": "8px" }] + "breakpoints": [ + { "name": "sm", "value": "640px" } + ] }, "components": [ { "name": "Primary Button", "kind": "button | input | nav | chip | card | custom", + "refersTo": "button-primary", "description": "One-line what and when.", "html": "", "css": ".ds-btn-primary { background: #191c1d; color: #fff; padding: 16px 48px; letter-spacing: 0.05em; text-transform: uppercase; font-weight: 500; border: none; border-radius: 0; transition: background 0.2s, transform 0.2s; } .ds-btn-primary:hover { background: oklch(60% 0.25 350); transform: translateY(-2px); }" @@ -235,6 +289,8 @@ Regenerate the sidecar whenever you regenerate DESIGN.md. If the user only asks } ``` +**What changed from schemaVersion 1.** The old sidecar carried token primitive arrays (`tokens.colors[]`, `tokens.typography[]`, etc.). Those values now live in the frontmatter. The sidecar only carries metadata that can't live in the frontmatter — tonal ramps, canonical OKLCH when the hex is an approximation, display names, role hints — keyed by the frontmatter token name (`colorMeta.`, `typographyMeta.`). Components still carry full HTML/CSS because Stitch's 8-prop set can't hold them. + #### Component translation rules The `html` and `css` fields must be **self-contained, drop-in snippets** that render correctly when injected into a shadow DOM. The panel applies them directly — no post-processing, no framework runtime. @@ -336,7 +392,7 @@ Per-section guidance in seed mode: - **Components**: omit entirely — no components exist yet. - **Do's and Don'ts**: carry PRODUCT.md's anti-references directly plus the anti-reference named in Q5. -Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens and components to render; there is nothing to show yet. The sidecar gets generated on the next Scan-mode run. +Seed mode writes a minimal frontmatter with `name` and `description` only — no colors, typography, rounded, spacing, or components yet. Real tokens land on the next Scan-mode run. Skip the `DESIGN.json` sidecar in seed mode for the same reason: nothing to render. ### Step 4: Confirm and refresh session cache @@ -346,6 +402,8 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an ## Style guidelines +- **Frontmatter first, prose second.** Tokens go in the YAML frontmatter; prose contextualizes them. Don't redefine a token value in two places — the frontmatter is normative. +- **Cite PRODUCT.md anti-references by name** in the Do's and Don'ts section. If PRODUCT.md lists "SaaS landing-page clichés" or "generic AI tool marketing" as anti-references, the DESIGN.md Don'ts should repeat those phrases verbatim so the visual spec enforces the strategic line. - **Match the spec, don't invent new sections.** The six section names are fixed. If you have Layout/Motion/Responsive content to document, fold it into Overview (philosophy-level rules) or Components (per-component behavior). - **Descriptive > technical**: "Gently curved edges (8px radius)" > "rounded-lg". Include the technical value in parens, lead with the description. - **Functional > decorative**: for each token, explain WHERE and WHY it's used, not just WHAT it is. @@ -365,3 +423,5 @@ Skip the `DESIGN.json` sidecar in seed mode. The live panel needs real tokens an - Don't duplicate content from PRODUCT.md. DESIGN.md is strictly visual. - Don't add a "Layout Principles" or "Motion" or "Responsive Behavior" top-level section. The spec has six, not nine. Fold that content where it belongs. - Don't rename sections even slightly. "Colors" not "Color Palette & Roles". "Typography" not "Typography Rules". Tooling parsing depends on exact headers. +- Don't duplicate token values between frontmatter and prose. If a color is in `colors.primary` as hex, the prose can name it and describe its role but should not reassert a different hex. The frontmatter is normative. +- Don't invent frontmatter token groups outside Stitch's schema (no `motion:`, `breakpoints:`, `shadows:` at the top level). Stitch's Zod schema only accepts `colors`, `typography`, `rounded`, `spacing`, `components`. Anything else belongs in the sidecar's `extensions`. diff --git a/source/skills/impeccable/scripts/design-parser.mjs b/source/skills/impeccable/scripts/design-parser.mjs index 6220f67e7..b68756640 100644 --- a/source/skills/impeccable/scripts/design-parser.mjs +++ b/source/skills/impeccable/scripts/design-parser.mjs @@ -1,5 +1,10 @@ // Parse a DESIGN.md (Stitch-spec format) into a structured JSON model that // the live-mode design-system panel can render. Deterministic, dependency-free. +// +// Two-layer: YAML frontmatter (machine-readable tokens) + markdown body +// (prose with six canonical H2 sections). When frontmatter is present, it's +// exposed on `model.frontmatter` alongside the prose-scraped sections; +// consumers can prefer frontmatter values and fall back to prose. const CANONICAL_SECTIONS = [ 'Overview', @@ -10,6 +15,97 @@ const CANONICAL_SECTIONS = [ "Do's and Don'ts", ]; +// ---------- Frontmatter (Stitch YAML subset) ---------- + +function parseFrontmatter(md) { + const lines = md.split(/\r?\n/); + if (lines[0]?.trim() !== '---') return { frontmatter: null, body: md }; + + let end = -1; + for (let i = 1; i < lines.length; i++) { + if (lines[i].trim() === '---') { end = i; break; } + } + if (end === -1) return { frontmatter: null, body: md }; + + const yaml = lines.slice(1, end).join('\n'); + const body = lines.slice(end + 1).join('\n'); + try { + return { frontmatter: parseYamlSubset(yaml), body }; + } catch { + return { frontmatter: null, body: md }; + } +} + +// Minimal YAML reader for the Stitch frontmatter subset: scalar maps with +// one level of nested objects (typography roles, components). Indent-based, +// 2-space convention. No arrays, no anchors, no multi-line scalars — Stitch's +// schema doesn't need them and accepting them would require a real YAML +// dependency we don't want to vendor. +function parseYamlSubset(yaml) { + const lines = yaml.split(/\r?\n/); + const root = {}; + const stack = [{ indent: -1, obj: root }]; + + for (const raw of lines) { + // Skip blanks and line-only comments. Don't strip inline comments: + // unquoted hex values start with `#` and can't be safely distinguished + // from a comment after whitespace. + if (!raw.trim() || /^\s*#/.test(raw)) continue; + + const indent = raw.match(/^\s*/)[0].length; + const content = raw.slice(indent); + + const colonIdx = findTopLevelColon(content); + if (colonIdx === -1) continue; + + while (stack.length > 1 && stack[stack.length - 1].indent >= indent) { + stack.pop(); + } + + const key = content.slice(0, colonIdx).trim(); + const rest = content.slice(colonIdx + 1).trim(); + const parent = stack[stack.length - 1].obj; + + if (rest === '') { + const obj = {}; + parent[key] = obj; + stack.push({ indent, obj }); + } else { + parent[key] = parseScalar(rest); + } + } + + return root; +} + +function findTopLevelColon(s) { + let inQuote = null; + for (let i = 0; i < s.length; i++) { + const ch = s[i]; + if (inQuote) { + if (ch === inQuote && s[i - 1] !== '\\') inQuote = null; + } else if (ch === '"' || ch === "'") { + inQuote = ch; + } else if (ch === ':') { + return i; + } + } + return -1; +} + +function parseScalar(raw) { + const s = raw.trim(); + if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) { + return s.slice(1, -1); + } + if (s === 'true') return true; + if (s === 'false') return false; + if (s === 'null' || s === '~') return null; + if (/^-?\d+$/.test(s)) return Number(s); + if (/^-?\d*\.\d+$/.test(s)) return Number(s); + return s; +} + const HEX_RE = /#[0-9a-fA-F]{3,8}\b/g; const OKLCH_RE = /oklch\([^)]+\)/gi; const RGBA_RE = /rgba?\([^)]+\)/gi; @@ -706,10 +802,12 @@ function assessCoverage(model) { // ---------- Main ---------- export function parseDesignMd(md) { - const { title, sections } = splitSections(md); + const { frontmatter, body } = parseFrontmatter(md); + const { title, sections } = splitSections(body); return { - schemaVersion: 1, + schemaVersion: 2, title, + frontmatter, overview: extractOverview(sections['Overview']), colors: extractColors(sections['Colors']), typography: extractTypography(sections['Typography']), diff --git a/source/skills/impeccable/scripts/live-browser.js b/source/skills/impeccable/scripts/live-browser.js index 32ea9a191..d36af3536 100644 --- a/source/skills/impeccable/scripts/live-browser.js +++ b/source/skills/impeccable/scripts/live-browser.js @@ -3958,7 +3958,7 @@ void main() { function renderParsedMdCta() { const box = document.createElement('div'); box.className = 'parsed-md-cta'; - box.innerHTML = `Basic viewRunning /impeccable document generates DESIGN.json alongside your DESIGN.md, which lets this panel render your project's actual button, input, and nav primitives — not generic approximations.`; + box.innerHTML = `Basic viewThis panel reads the tokens in your DESIGN.md frontmatter. Running /impeccable document also generates a DESIGN.json sidecar with your project's actual component snippets (button, input, nav) and tonal ramps, rendered live below the tokens.`; return box; } diff --git a/tests/design-parser.test.mjs b/tests/design-parser.test.mjs new file mode 100644 index 000000000..de0989239 --- /dev/null +++ b/tests/design-parser.test.mjs @@ -0,0 +1,105 @@ +/** + * Tests for design-parser.mjs — frontmatter + body extraction. + * Run with: node --test tests/design-parser.test.mjs + */ + +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { parseDesignMd } from '../source/skills/impeccable/scripts/design-parser.mjs'; + +describe('parseDesignMd frontmatter branch', () => { + it('returns null frontmatter when the file has no YAML header', () => { + const md = `# Design System: Demo + +## 1. Overview + +Some prose. +`; + const model = parseDesignMd(md); + assert.equal(model.schemaVersion, 2); + assert.equal(model.frontmatter, null); + assert.equal(model.title, 'Design System: Demo'); + }); + + it('parses a Stitch-shaped frontmatter and strips it from the body', () => { + const md = `--- +name: Demo System +description: A quiet editorial look. +colors: + primary: "#b8422e" + neutral-bg: "#faf7f2" +typography: + display: + fontFamily: "Cormorant Garamond, Georgia, serif" + fontWeight: 300 + lineHeight: 1 + body: + fontFamily: "Inter, sans-serif" +rounded: + sm: "4px" + md: "8px" +components: + button-primary: + backgroundColor: "{colors.primary}" + textColor: "{colors.neutral-bg}" + rounded: "{rounded.sm}" +--- + +# Design System: Demo + +## 1. Overview + +Opening prose. +`; + const model = parseDesignMd(md); + assert.equal(model.schemaVersion, 2); + assert.equal(model.title, 'Design System: Demo'); + assert.ok(model.frontmatter); + assert.equal(model.frontmatter.name, 'Demo System'); + assert.equal(model.frontmatter.description, 'A quiet editorial look.'); + assert.equal(model.frontmatter.colors.primary, '#b8422e'); + assert.equal(model.frontmatter.colors['neutral-bg'], '#faf7f2'); + assert.equal(model.frontmatter.typography.display.fontFamily, 'Cormorant Garamond, Georgia, serif'); + assert.equal(model.frontmatter.typography.display.fontWeight, 300); + assert.equal(model.frontmatter.typography.display.lineHeight, 1); + assert.equal(model.frontmatter.rounded.md, '8px'); + assert.equal(model.frontmatter.components['button-primary'].backgroundColor, '{colors.primary}'); + }); + + it('recovers gracefully when frontmatter has no closing marker', () => { + // No `---` terminator: the whole file is treated as body, not partial + // frontmatter. The H1 title still resolves from the body. + const md = `--- +this is not valid yaml : : : +no closing marker +# Design System: Broken + +## 1. Overview + +Prose. +`; + const model = parseDesignMd(md); + assert.equal(model.frontmatter, null); + assert.equal(model.title, 'Design System: Broken'); + }); + + it('ignores line-only comments but preserves unquoted hex values', () => { + const md = `--- +# Top-level comment +colors: + primary: #b8422e + # mid-block comment + accent: "#ec4899" +--- + +# Design System: Commented + +## 1. Overview + +Prose. +`; + const model = parseDesignMd(md); + assert.equal(model.frontmatter.colors.primary, '#b8422e'); + assert.equal(model.frontmatter.colors.accent, '#ec4899'); + }); +});