From 0d4c24ad3f71782a018c07268476031ce9493cce Mon Sep 17 00:00:00 2001 From: Emp1500 Date: Thu, 25 Jun 2026 11:12:09 +0000 Subject: [PATCH 1/7] docs: add design spec for data-driven primitive type definitions (issue #97) --- ...026-06-25-spec-config-color-type-design.md | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-25-spec-config-color-type-design.md diff --git a/docs/superpowers/specs/2026-06-25-spec-config-color-type-design.md b/docs/superpowers/specs/2026-06-25-spec-config-color-type-design.md new file mode 100644 index 00000000..7372ea96 --- /dev/null +++ b/docs/superpowers/specs/2026-06-25-spec-config-color-type-design.md @@ -0,0 +1,173 @@ +--- +name: spec-config-color-type +description: Add a data-driven `types` section to spec-config.yaml so Color and Dimension definitions are generated from config rather than hardcoded in spec.mdx +metadata: + type: project + issue: 97 + branch: fix/spec-config-color-type +--- + +# Design: Data-driven primitive type definitions (issue #97) + +## Problem + +`spec-config.yaml` is the single source of truth for the DESIGN.md format specification, but the Color and Dimension type definitions are hardcoded as static prose in `spec.mdx`. Anyone reading only the YAML cannot learn what a Color value is. The goal is to make these definitions fully data-driven — the same pattern already used for typography properties and component sub-tokens. + +## Solution overview + +Add a `types:` map to `spec-config.yaml`. Each entry describes one primitive type (Color, Dimension) with its description, optional accepted-format list, and optional follow-up note. A new `typeDefinitions()` renderer in `renderers.ts` converts this map to markdown. The two hardcoded blocks in `spec.mdx` are replaced with a single `{typeDefinitions()}` call. + +--- + +## 1. `spec-config.yaml` — new `types:` section + +Inserted after `units:`, before `sections:`. + +```yaml +types: + Color: + description: "A color value is any valid CSS color string. Supported formats include:" + formats: + - "Hex: `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`" + - "Named colors: `red`, `cornflowerblue`, `transparent`" + - "Functional: `rgb()`, `rgba()`, `hsl()`, `hsla()`, `hwb()`" + - "Wide-gamut: `oklch()`, `oklab()`, `lch()`, `lab()`" + - "Mixing: `color-mix(in srgb, ...)`" + note: "All color values are internally converted to sRGB for WCAG contrast checking. The original format is preserved for display and export.\n\nHex notation (`#RRGGBB`) remains the recommended default for simplicity and broad tooling support." + Dimension: + description: "A dimension value is a string with a unit suffix." + # no formats — units are appended automatically from the top-level `units` array +``` + +**Key decision:** `Dimension` has no `units` field in the type definition; the renderer appends "Valid units are: px, em, rem." from the already-existing top-level `STANDARD_UNITS`. This avoids duplication. + +--- + +## 2. `spec-config.ts` — schema, interface, exports + +### Zod schema additions + +```ts +const TypeDefSchema = z.object({ + description: z.string(), + formats: z.array(z.string()).optional(), + note: z.string().optional(), +}); + +// inside ConfigSchema: +types: z.record(z.string(), TypeDefSchema).optional().default({}), +``` + +### New TypeScript interface + +```ts +export interface TypeDef { + description: string; + formats?: readonly string[]; + note?: string; +} +``` + +### New constant and SpecConfig extension + +```ts +export const PRIMITIVE_TYPES: Record = config.types; + +// Add to SpecConfig interface: +PRIMITIVE_TYPES: typeof PRIMITIVE_TYPES; + +// Add to SPEC_CONFIG bundle: +PRIMITIVE_TYPES, +``` + +--- + +## 3. `renderers.ts` — `typeDefinitions()` function + +New pure renderer. `Dimension` is the only type that gets units appended (by name match), keeping all other types generic. + +```ts +export function typeDefinitions(config: SpecConfig): string { + return Object.entries(config.PRIMITIVE_TYPES).map(([name, def]) => { + let block = `**${name}**: ${def.description}`; + if (def.formats?.length) { + block += '\n\n' + def.formats.map(f => `- ${f}`).join('\n'); + } + if (name === 'Dimension') { + block += ` Valid units are: ${config.STANDARD_UNITS.join(', ')}.`; + } + if (def.note) { + block += '\n\n' + def.note; + } + return block; + }).join('\n\n'); +} +``` + +--- + +## 4. `spec.mdx` — replace hardcoded blocks + +**Remove** lines 46–60 (the Color and Dimension static paragraphs). + +**Replace** with: + +```mdx +{typeDefinitions()} +``` + +Update the import at the top of `spec.mdx`: + +```mdx +import { ..., typeDefinitions } from './renderers.js' +``` + +--- + +## 5. `generate.ts` — bind renderer into scope + +```ts +typeDefinitions: () => renderers.typeDefinitions(cfg), +``` + +--- + +## 6. Tests + +### `spec-config.test.ts` — structural invariants + +- `PRIMITIVE_TYPES` contains at least `Color` and `Dimension`. +- Each entry has a non-empty `description`. +- Type names are unique (already guaranteed by `z.record`, but worth asserting). + +### `renderers.test.ts` (new file, or added to `compiler.test.ts`) + +- `typeDefinitions()` output includes `**Color**` and `**Dimension**`. +- `typeDefinitions()` output includes at least one format bullet for Color. +- `typeDefinitions()` output includes `STANDARD_UNITS` values for Dimension. +- `typeDefinitions()` output does NOT include a `units` bullet list for Dimension (units are appended inline, not as a list). + +### Regenerate spec + +Run `bun run spec:gen` and verify `bun run spec:gen --check` passes (CI gate). + +--- + +## Files changed + +| File | Change | +|---|---| +| `packages/cli/src/linter/spec-config.yaml` | Add `types:` section | +| `packages/cli/src/linter/spec-config.ts` | Add `TypeDefSchema`, `TypeDef`, `PRIMITIVE_TYPES`, extend `SpecConfig` | +| `packages/cli/src/linter/spec-gen/renderers.ts` | Add `typeDefinitions()` | +| `packages/cli/src/linter/spec-gen/spec.mdx` | Replace hardcoded Color+Dimension prose with `{typeDefinitions()}` | +| `packages/cli/src/linter/spec-gen/generate.ts` | Bind `typeDefinitions` in scope | +| `packages/cli/src/linter/spec-config.test.ts` | Add invariant tests for `PRIMITIVE_TYPES` | +| `packages/cli/src/linter/spec-gen/compiler.test.ts` | Add renderer output tests | +| `docs/spec.md` | Regenerated output | + +## Out of scope + +- Composite types (Typography) — not a primitive, not referenced in the issue. +- Runtime linter validation using the types map — no change to linter behaviour. +- Adding more types beyond Color and Dimension — YAGNI. From 979b5a9584c1c4ba7714764efc154a51ed3cee05 Mon Sep 17 00:00:00 2001 From: Emp1500 Date: Thu, 25 Jun 2026 11:19:20 +0000 Subject: [PATCH 2/7] docs: add implementation plan for spec-config primitive type definitions (issue #97) --- .../2026-06-25-spec-config-color-type.md | 552 ++++++++++++++++++ 1 file changed, 552 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-25-spec-config-color-type.md diff --git a/docs/superpowers/plans/2026-06-25-spec-config-color-type.md b/docs/superpowers/plans/2026-06-25-spec-config-color-type.md new file mode 100644 index 00000000..592332f9 --- /dev/null +++ b/docs/superpowers/plans/2026-06-25-spec-config-color-type.md @@ -0,0 +1,552 @@ +# Spec-Config Primitive Type Definitions Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Move the Color and Dimension type definitions from hardcoded prose in `spec.mdx` into a data-driven `types:` section in `spec-config.yaml`, rendered dynamically the same way typography properties already are. + +**Architecture:** Add a `types` map to the YAML config; extend the Zod schema and TypeScript exports in `spec-config.ts` to parse and expose it; add a `typeDefinitions()` pure renderer in `renderers.ts`; replace two hardcoded blocks in `spec.mdx` with a single `{typeDefinitions()}` call; wire the renderer into `generate.ts`; regenerate `docs/spec.md`. + +**Tech Stack:** TypeScript, Bun (test runner + script runner), Zod (schema validation), MDX (spec template), remark (markdown processing) + +## Global Constraints + +- All test files use `bun:test` (`import { describe, it, expect } from 'bun:test'`) +- Run tests from `packages/cli/` with `bun test` +- Run spec generation from `packages/cli/` with `bun run spec:gen` +- Check spec is up to date with `bun run spec:gen --check` +- No new runtime dependencies +- Follow existing Apache 2.0 license header in every new file +- `Dimension` units are NOT duplicated in `types.Dimension` — the renderer reads them from `STANDARD_UNITS` + +--- + +## File Map + +| File | Action | What changes | +|---|---|---| +| `packages/cli/src/linter/spec-config.yaml` | Modify | Add `types:` section with Color and Dimension | +| `packages/cli/src/linter/spec-config.ts` | Modify | Add `TypeDefSchema`, `TypeDef` interface, `PRIMITIVE_TYPES` export, extend `SpecConfig` | +| `packages/cli/src/linter/spec-config.test.ts` | Modify | Add structural invariant tests for `PRIMITIVE_TYPES` | +| `packages/cli/src/linter/spec-gen/renderers.ts` | Modify | Add `typeDefinitions()` function | +| `packages/cli/src/linter/spec-gen/renderers.test.ts` | Create | Unit tests for `typeDefinitions()` output | +| `packages/cli/src/linter/spec-gen/generate.ts` | Modify | Bind `typeDefinitions` in MDX scope | +| `packages/cli/src/linter/spec-gen/compiler.test.ts` | Modify | Add `typeDefinitions` to the full spec.mdx compile test | +| `packages/cli/src/linter/spec-gen/spec.mdx` | Modify | Remove hardcoded Color+Dimension prose; add `{typeDefinitions()}` call | +| `docs/spec.md` | Regenerate | Output of `bun run spec:gen` | + +--- + +## Task 1: Schema — add `types` to spec-config.yaml and spec-config.ts + +**Files:** +- Modify: `packages/cli/src/linter/spec-config.yaml` +- Modify: `packages/cli/src/linter/spec-config.ts` +- Modify: `packages/cli/src/linter/spec-config.test.ts` + +**Interfaces:** +- Produces: + - `TypeDef` interface: `{ description: string; formats?: readonly string[]; note?: string }` + - `PRIMITIVE_TYPES: Record` — exported constant + - `SpecConfig.PRIMITIVE_TYPES` — added to the aggregate type + - `SPEC_CONFIG.PRIMITIVE_TYPES` — added to the bundle object + +--- + +- [ ] **Step 1: Write the failing tests in spec-config.test.ts** + +Open `packages/cli/src/linter/spec-config.test.ts`. Add `PRIMITIVE_TYPES` to the import at the top of the file: + +```ts +import { + loadSpecConfig, + getSpecConfig, + STANDARD_UNITS, + SECTIONS, + TYPOGRAPHY_PROPERTIES, + COMPONENT_SUB_TOKENS, + CORE_COLOR_ROLES, + RECOMMENDED_TOKENS, + EXAMPLES, + CANONICAL_ORDER, + SECTION_ALIASES, + resolveAlias, + VALID_TYPOGRAPHY_PROPS, + VALID_COMPONENT_SUB_TOKENS, + PRIMITIVE_TYPES, +} from './spec-config.js'; +``` + +Then append this new `describe` block at the end of the file (before the final blank line): + +```ts +describe('spec-config PRIMITIVE_TYPES', () => { + it('contains Color and Dimension entries', () => { + expect(PRIMITIVE_TYPES).toHaveProperty('Color'); + expect(PRIMITIVE_TYPES).toHaveProperty('Dimension'); + }); + + it('every type has a non-empty description', () => { + for (const [, def] of Object.entries(PRIMITIVE_TYPES)) { + expect(def.description.length).toBeGreaterThan(0); + } + }); + + it('Color has at least one format entry', () => { + expect(PRIMITIVE_TYPES['Color']!.formats?.length).toBeGreaterThan(0); + }); + + it('Dimension has no formats list (units come from STANDARD_UNITS)', () => { + expect(PRIMITIVE_TYPES['Dimension']!.formats).toBeUndefined(); + }); +}); +``` + +- [ ] **Step 2: Run tests to confirm they fail** + +```bash +cd packages/cli && bun test src/linter/spec-config.test.ts +``` + +Expected: 4 new tests FAIL with something like `PRIMITIVE_TYPES is not exported` or `Cannot read properties of undefined`. + +- [ ] **Step 3: Add `types:` section to spec-config.yaml** + +Open `packages/cli/src/linter/spec-config.yaml`. Insert the following block **after the `units:` list and before the `sections:` key** (after line 30, before line 32): + +```yaml +types: + Color: + description: "A color value is any valid CSS color string. Supported formats include:" + formats: + - "Hex: `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`" + - "Named colors: `red`, `cornflowerblue`, `transparent`" + - "Functional: `rgb()`, `rgba()`, `hsl()`, `hsla()`, `hwb()`" + - "Wide-gamut: `oklch()`, `oklab()`, `lch()`, `lab()`" + - "Mixing: `color-mix(in srgb, ...)`" + note: | + All color values are internally converted to sRGB for WCAG contrast checking. The original format is preserved for display and export. + + Hex notation (`#RRGGBB`) remains the recommended default for simplicity and broad tooling support. + Dimension: + description: "A dimension value is a string with a unit suffix." +``` + +- [ ] **Step 4: Update spec-config.ts — add TypeDefSchema** + +Open `packages/cli/src/linter/spec-config.ts`. After the `PropertyDefSchema` definition (around line 38), add: + +```ts +const TypeDefSchema = z.object({ + description: z.string(), + formats: z.array(z.string()).optional(), + note: z.string().optional(), +}); +``` + +- [ ] **Step 5: Add `types` to ConfigSchema** + +In the same file, inside `ConfigSchema`, add a `types` field after the `color_roles` line: + +```ts + color_roles: z.array(z.string()).min(1), + types: z.record(z.string(), TypeDefSchema).optional().default({}), + recommended_tokens: z.record(z.string(), z.array(z.string())), +``` + +- [ ] **Step 6: Add TypeDef interface** + +In the `// ── Interfaces ───` section (around line 89), add after `ComponentSubTokenDef`: + +```ts +export interface TypeDef { + /** Human-readable description of the type. */ + description: string; + /** List of accepted format strings, rendered as bullets. */ + formats?: readonly string[]; + /** Follow-up note rendered as a separate paragraph. May contain \n\n for multiple paragraphs. */ + note?: string; +} +``` + +- [ ] **Step 7: Export PRIMITIVE_TYPES constant** + +In the `// ── Constant exports ─────` section, after the `EXAMPLES` export (around line 145), add: + +```ts +/** Primitive type definitions (Color, Dimension, etc.) for the spec document. */ +export const PRIMITIVE_TYPES: Record = config.types; +``` + +- [ ] **Step 8: Extend SpecConfig interface and SPEC_CONFIG bundle** + +In the `SpecConfig` interface (around line 173), add: + +```ts + PRIMITIVE_TYPES: typeof PRIMITIVE_TYPES; +``` + +In the `SPEC_CONFIG` object (around line 187), add: + +```ts + PRIMITIVE_TYPES, +``` + +- [ ] **Step 9: Run tests to confirm they pass** + +```bash +cd packages/cli && bun test src/linter/spec-config.test.ts +``` + +Expected: All tests PASS, including the 4 new `spec-config PRIMITIVE_TYPES` tests. + +- [ ] **Step 10: Commit** + +```bash +git add packages/cli/src/linter/spec-config.yaml packages/cli/src/linter/spec-config.ts packages/cli/src/linter/spec-config.test.ts +git commit -m "feat: add types section to spec-config for Color and Dimension definitions" +``` + +--- + +## Task 2: Renderer — add `typeDefinitions()` and wire into generate.ts + +**Files:** +- Modify: `packages/cli/src/linter/spec-gen/renderers.ts` +- Create: `packages/cli/src/linter/spec-gen/renderers.test.ts` +- Modify: `packages/cli/src/linter/spec-gen/generate.ts` +- Modify: `packages/cli/src/linter/spec-gen/compiler.test.ts` + +**Interfaces:** +- Consumes: `SpecConfig.PRIMITIVE_TYPES` (from Task 1), `SpecConfig.STANDARD_UNITS` +- Produces: `typeDefinitions(config: SpecConfig): string` exported from `renderers.ts` + +--- + +- [ ] **Step 1: Create renderers.test.ts with failing tests** + +Create `packages/cli/src/linter/spec-gen/renderers.test.ts`: + +```ts +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { describe, it, expect } from 'bun:test'; +import { typeDefinitions } from './renderers.js'; +import { SPEC_CONFIG } from '../spec-config.js'; + +describe('typeDefinitions', () => { + it('includes **Color** with its description', () => { + const result = typeDefinitions(SPEC_CONFIG); + expect(result).toContain('**Color**'); + expect(result).toContain('A color value is any valid CSS color string'); + }); + + it('renders Color formats as bullet list', () => { + const result = typeDefinitions(SPEC_CONFIG); + expect(result).toContain('- Hex:'); + expect(result).toContain('- Wide-gamut:'); + expect(result).toContain('- Mixing:'); + }); + + it('includes **Dimension** with its description', () => { + const result = typeDefinitions(SPEC_CONFIG); + expect(result).toContain('**Dimension**'); + expect(result).toContain('A dimension value is a string with a unit suffix'); + }); + + it('appends STANDARD_UNITS inline to Dimension', () => { + const result = typeDefinitions(SPEC_CONFIG); + expect(result).toContain('Valid units are:'); + for (const unit of SPEC_CONFIG.STANDARD_UNITS) { + expect(result).toContain(unit); + } + }); + + it('does not render units as a bullet list for Dimension', () => { + const result = typeDefinitions(SPEC_CONFIG); + const dimensionStart = result.indexOf('**Dimension**'); + const afterDimension = result.slice(dimensionStart); + expect(afterDimension).not.toMatch(/\n- px/); + expect(afterDimension).not.toMatch(/\n- em/); + }); + + it('renders Color note paragraph', () => { + const result = typeDefinitions(SPEC_CONFIG); + expect(result).toContain('internally converted to sRGB'); + expect(result).toContain('recommended default'); + }); +}); +``` + +- [ ] **Step 2: Run tests to confirm they fail** + +```bash +cd packages/cli && bun test src/linter/spec-gen/renderers.test.ts +``` + +Expected: 6 tests FAIL with `typeDefinitions is not a function` or similar. + +- [ ] **Step 3: Add typeDefinitions() to renderers.ts** + +Open `packages/cli/src/linter/spec-gen/renderers.ts`. Add `TypeDef` to the import at the top: + +```ts +import type { SpecConfig, TypographyPropertyDef, SectionDef, ComponentSubTokenDef, TypeDef } from '../spec-config.js'; +``` + +Then append the new function at the end of the file (after `recommendedTokens`): + +```ts +/** Primitive type definitions (Color, Dimension, etc.) for the schema section. */ +export function typeDefinitions(config: SpecConfig): string { + return Object.entries(config.PRIMITIVE_TYPES).map(([name, def]: [string, TypeDef]) => { + let block = `**${name}**: ${def.description}`; + if (def.formats?.length) { + block += '\n\n' + def.formats.map((f: string) => `- ${f}`).join('\n'); + } + if (name === 'Dimension') { + block += ` Valid units are: ${config.STANDARD_UNITS.join(', ')}.`; + } + if (def.note) { + block += '\n\n' + def.note.trim(); + } + return block; + }).join('\n\n'); +} +``` + +- [ ] **Step 4: Run renderer tests to confirm they pass** + +```bash +cd packages/cli && bun test src/linter/spec-gen/renderers.test.ts +``` + +Expected: All 6 tests PASS. + +- [ ] **Step 5: Update compiler.test.ts — add typeDefinitions to full spec compile test** + +Open `packages/cli/src/linter/spec-gen/compiler.test.ts`. In the `'compiles the full spec.mdx with spec-config scope'` test (around line 61), add `typeDefinitions` to the scope object: + +```ts + const scope = { + ...cfg, + frontmatterExample: () => renderers.frontmatterExample(cfg), + colorsExample: () => renderers.colorsExample(cfg), + typographyExample: () => renderers.typographyExample(cfg), + componentsExample: () => renderers.componentsExample(cfg), + typographyPropertyList: () => renderers.typographyPropertyList(cfg), + sectionOrderList: () => renderers.sectionOrderList(cfg), + componentSubTokenList: () => renderers.componentSubTokenList(cfg), + recommendedTokens: () => renderers.recommendedTokens(cfg), + typeDefinitions: () => renderers.typeDefinitions(cfg), + }; +``` + +Also add an assertion in that test to verify the Color definition appears in the output: + +```ts + expect(result).toContain('**Color**'); + expect(result).toContain('oklch()'); +``` + +- [ ] **Step 6: Wire typeDefinitions into generate.ts scope** + +Open `packages/cli/src/linter/spec-gen/generate.ts`. In the `scope` object (around line 42), add: + +```ts + typeDefinitions: () => renderers.typeDefinitions(cfg), +``` + +The full scope object should now be: + +```ts + const scope = { + ...cfg, + frontmatterExample: () => renderers.frontmatterExample(cfg), + colorsExample: () => renderers.colorsExample(cfg), + typographyExample: () => renderers.typographyExample(cfg), + componentsExample: () => renderers.componentsExample(cfg), + typographyPropertyList: () => renderers.typographyPropertyList(cfg), + sectionOrderList: () => renderers.sectionOrderList(cfg), + componentSubTokenList: () => renderers.componentSubTokenList(cfg), + recommendedTokens: () => renderers.recommendedTokens(cfg), + typeDefinitions: () => renderers.typeDefinitions(cfg), + }; +``` + +- [ ] **Step 7: Run all tests to confirm everything still passes** + +```bash +cd packages/cli && bun test +``` + +Expected: All tests PASS (the full spec.mdx compile test will still pass because spec.mdx hasn't been updated yet — `typeDefinitions` is now in scope but not called from the template). + +- [ ] **Step 8: Commit** + +```bash +git add packages/cli/src/linter/spec-gen/renderers.ts packages/cli/src/linter/spec-gen/renderers.test.ts packages/cli/src/linter/spec-gen/generate.ts packages/cli/src/linter/spec-gen/compiler.test.ts +git commit -m "feat: add typeDefinitions renderer and wire into spec generator scope" +``` + +--- + +## Task 3: Template — update spec.mdx and regenerate docs/spec.md + +**Files:** +- Modify: `packages/cli/src/linter/spec-gen/spec.mdx` +- Regenerate: `docs/spec.md` + +**Interfaces:** +- Consumes: `typeDefinitions()` from Task 2 (available in MDX scope) + +--- + +- [ ] **Step 1: Update the import lines in spec.mdx** + +Open `packages/cli/src/linter/spec-gen/spec.mdx`. Line 1 currently imports `STANDARD_UNITS` from spec-config — that direct usage is being removed. Update line 1 to remove `STANDARD_UNITS`: + +**Before (line 1):** +```mdx +import { SPEC_VERSION, STANDARD_UNITS, SECTIONS, TYPOGRAPHY_PROPERTIES, COMPONENT_SUB_TOKENS, CORE_COLOR_ROLES, RECOMMENDED_TOKENS, EXAMPLES } from '../spec-config.js' +``` + +**After (line 1):** +```mdx +import { SPEC_VERSION, SECTIONS, TYPOGRAPHY_PROPERTIES, COMPONENT_SUB_TOKENS, CORE_COLOR_ROLES, RECOMMENDED_TOKENS, EXAMPLES } from '../spec-config.js' +``` + +Update line 2 to add `typeDefinitions` to the renderers import: + +**Before (line 2):** +```mdx +import { frontmatterExample, colorsExample, typographyExample, componentsExample, typographyPropertyList, sectionOrderList, componentSubTokenList, recommendedTokens } from './renderers.js' +``` + +**After (line 2):** +```mdx +import { frontmatterExample, colorsExample, typographyExample, componentsExample, typographyPropertyList, sectionOrderList, componentSubTokenList, recommendedTokens, typeDefinitions } from './renderers.js' +``` + +- [ ] **Step 2: Replace hardcoded Color and Dimension prose in spec.mdx** + +In `spec.mdx`, find and replace the block from the hardcoded `**Color**` paragraph through the hardcoded `**Dimension**` line. The section to replace is lines 46–60: + +**Remove this (lines 46–60):** +```mdx +**Color**: A color value is any valid CSS color string. Supported formats include: + +- Hex: `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA` +- Named colors: `red`, `cornflowerblue`, `transparent` +- Functional: `rgb()`, `rgba()`, `hsl()`, `hsla()`, `hwb()` +- Wide-gamut: `oklch()`, `oklab()`, `lch()`, `lab()` +- Mixing: `color-mix(in srgb, ...)` + +All color values are internally converted to sRGB for WCAG contrast checking. The original format is preserved for display and export. + +Hex notation (`#RRGGBB`) remains the recommended default for simplicity and broad tooling support. + +{typographyPropertyList()} + +**Dimension**: A dimension value is a string with a unit suffix. Valid units are: {STANDARD_UNITS.join(', ')}. +``` + +**Replace with:** +```mdx +{typeDefinitions()} + +{typographyPropertyList()} +``` + +After this edit, the schema section should flow: `{typeDefinitions()}` (Color + Dimension) → `{typographyPropertyList()}` (Typography) → `**Token References**:`. + +- [ ] **Step 3: Run all tests to confirm nothing broke** + +```bash +cd packages/cli && bun test +``` + +Expected: All tests PASS. + +- [ ] **Step 4: Regenerate docs/spec.md** + +```bash +cd packages/cli && bun run spec:gen +``` + +Expected output: +``` +✅ Generated docs/spec.md (N lines) +``` + +- [ ] **Step 5: Verify the regenerated spec is accepted by the --check gate** + +```bash +cd packages/cli && bun run spec:gen --check +``` + +Expected output: +``` +✅ docs/spec.md is up to date. +``` + +- [ ] **Step 6: Inspect the generated output to confirm Color and Dimension appear correctly** + +```bash +grep -A3 "\*\*Color\*\*" docs/spec.md +grep -A1 "\*\*Dimension\*\*" docs/spec.md +``` + +Expected for Color: bold name, followed by format bullets, followed by sRGB note. +Expected for Dimension: bold name with inline "Valid units are: px, em, rem." on the same paragraph. + +- [ ] **Step 7: Commit** + +```bash +git add packages/cli/src/linter/spec-gen/spec.mdx docs/spec.md +git commit -m "feat: replace hardcoded Color/Dimension prose in spec.mdx with typeDefinitions() renderer" +``` + +--- + +## Self-Review + +**Spec coverage check:** + +| Spec requirement | Task that covers it | +|---|---| +| Add `types:` to spec-config.yaml with Color and Dimension | Task 1, Step 3 | +| Zod schema validates the new `types` field | Task 1, Steps 4–5 | +| `TypeDef` interface exported from spec-config.ts | Task 1, Step 6 | +| `PRIMITIVE_TYPES` constant exported | Task 1, Step 7 | +| `SpecConfig` interface and `SPEC_CONFIG` bundle include `PRIMITIVE_TYPES` | Task 1, Step 8 | +| Structural invariant tests for `PRIMITIVE_TYPES` | Task 1, Step 1 | +| `typeDefinitions()` renderer in renderers.ts | Task 2, Step 3 | +| Dimension units come from `STANDARD_UNITS`, not duplicated | Task 2, Step 3 (name === 'Dimension' branch) | +| Color note renders both paragraphs | Task 2, Step 3 (`def.note.trim()`) | +| `typeDefinitions` bound in generate.ts scope | Task 2, Step 6 | +| Full spec.mdx compile test updated | Task 2, Step 5 | +| Hardcoded Color prose removed from spec.mdx | Task 3, Step 2 | +| Hardcoded Dimension prose removed from spec.mdx | Task 3, Step 2 | +| `STANDARD_UNITS` removed from spec.mdx import | Task 3, Step 1 | +| `typeDefinitions` added to spec.mdx import | Task 3, Step 1 | +| docs/spec.md regenerated | Task 3, Steps 4–5 | + +**Placeholder scan:** No TBDs, no "similar to above", all code shown explicitly. ✓ + +**Type consistency:** +- `TypeDef` defined in Task 1 Step 6, imported in Task 2 Step 3 — matches. +- `PRIMITIVE_TYPES: Record` used in Task 2 renderer — matches Task 1 export. +- `typeDefinitions(config: SpecConfig): string` defined in Task 2, called in Task 3 — matches. +- `SpecConfig.PRIMITIVE_TYPES` added in Task 1, used in `renderers.ts` via `config.PRIMITIVE_TYPES` — matches. From da4e44772807ea7514acb26962b34bc0b06ead66 Mon Sep 17 00:00:00 2001 From: Emp1500 Date: Thu, 25 Jun 2026 11:25:43 +0000 Subject: [PATCH 3/7] feat: add types section to spec-config for Color and Dimension definitions --- packages/cli/src/linter/spec-config.test.ts | 440 +++++++++++--------- packages/cli/src/linter/spec-config.ts | 417 ++++++++++--------- packages/cli/src/linter/spec-config.yaml | 314 +++++++------- 3 files changed, 616 insertions(+), 555 deletions(-) diff --git a/packages/cli/src/linter/spec-config.test.ts b/packages/cli/src/linter/spec-config.test.ts index 0115b371..eb33682b 100644 --- a/packages/cli/src/linter/spec-config.test.ts +++ b/packages/cli/src/linter/spec-config.test.ts @@ -1,208 +1,232 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { describe, it, expect } from 'bun:test'; -import { writeFileSync, unlinkSync } from 'node:fs'; -import { - loadSpecConfig, - getSpecConfig, - STANDARD_UNITS, - SECTIONS, - TYPOGRAPHY_PROPERTIES, - COMPONENT_SUB_TOKENS, - CORE_COLOR_ROLES, - RECOMMENDED_TOKENS, - EXAMPLES, - CANONICAL_ORDER, - SECTION_ALIASES, - resolveAlias, - VALID_TYPOGRAPHY_PROPS, - VALID_COMPONENT_SUB_TOKENS, -} from './spec-config.js'; - -// ── Loader robustness ───────────────────────────────────────────────── - -describe('spec-config loader', () => { - it('throws when file does not exist', () => { - expect(() => loadSpecConfig('non-existent.yaml')).toThrow(); - }); - - it('throws when YAML is malformed', () => { - const path = '__test_malformed.yaml'; - writeFileSync(path, 'invalid: yaml: :'); - try { - expect(() => loadSpecConfig(path)).toThrow(); - } finally { - unlinkSync(path); - } - }); - - it('throws when required fields are missing', () => { - const path = '__test_incomplete.yaml'; - writeFileSync(path, 'version: alpha\nunits: [px]'); - try { - expect(() => loadSpecConfig(path)).toThrow(); - } finally { - unlinkSync(path); - } - }); - - it('throws when sections array is empty', () => { - const path = '__test_empty_sections.yaml'; - writeFileSync(path, [ - 'version: alpha', - 'units: [px]', - 'sections: []', - 'typography_properties: [{name: x, type: y}]', - 'component_sub_tokens: [{name: x, type: y}]', - 'color_roles: [primary]', - 'recommended_tokens: {a: [b]}', - 'examples:', - ' colors: {a: "#000"}', - ' typography: {a: {fontFamily: x}}', - ' components: {a: {bg: x}}', - ].join('\n')); - try { - expect(() => loadSpecConfig(path)).toThrow(); - } finally { - unlinkSync(path); - } - }); - - it('does not write to stdout or stderr', () => { - const originalLog = console.log; - const originalError = console.error; - const logs: string[] = []; - console.log = (...args: unknown[]) => logs.push(args.join(' ')); - console.error = (...args: unknown[]) => logs.push(args.join(' ')); - try { - loadSpecConfig(); - expect(logs.length).toBe(0); - } finally { - console.log = originalLog; - console.error = originalError; - } - }); -}); - -// ── Lazy loading ────────────────────────────────────────────────────── - -describe('spec-config lazy loading', () => { - it('getSpecConfig returns a valid config object', () => { - const config = getSpecConfig(); - expect(config.version).toBeString(); - expect(config.units.length).toBeGreaterThan(0); - expect(config.sections.length).toBeGreaterThan(0); - }); - - it('getSpecConfig returns the same cached instance on subsequent calls', () => { - const first = getSpecConfig(); - const second = getSpecConfig(); - expect(first).toBe(second); - }); -}); - -// ── Structural invariants ───────────────────────────────────────────── -// These never need updating when values change. -// They catch real bugs: duplicates, empty arrays, collision. - -describe('spec-config structural invariants', () => { - it('sections are non-empty', () => { - expect(SECTIONS.length).toBeGreaterThan(0); - }); - - it('section canonical names are unique', () => { - const names = SECTIONS.map(s => s.canonical); - expect(new Set(names).size).toBe(names.length); - }); - - it('no alias collides with a canonical name', () => { - const canonicals = new Set(SECTIONS.map(s => s.canonical)); - const aliases = SECTIONS.flatMap(s => s.aliases ?? []); - for (const alias of aliases) { - expect(canonicals.has(alias)).toBe(false); - } - }); - - it('aliases are unique across all sections', () => { - const aliases = SECTIONS.flatMap(s => s.aliases ?? []); - expect(new Set(aliases).size).toBe(aliases.length); - }); - - it('typography property names are unique', () => { - const names = TYPOGRAPHY_PROPERTIES.map(p => p.name); - expect(new Set(names).size).toBe(names.length); - }); - - it('component sub-token names are unique', () => { - const names = COMPONENT_SUB_TOKENS.map(p => p.name); - expect(new Set(names).size).toBe(names.length); - }); - - it('color roles are unique', () => { - expect(new Set(CORE_COLOR_ROLES).size).toBe(CORE_COLOR_ROLES.length); - }); - - it('units are non-empty and unique', () => { - expect(STANDARD_UNITS.length).toBeGreaterThan(0); - expect(new Set(STANDARD_UNITS).size).toBe(STANDARD_UNITS.length); - }); - - it('recommended token categories are non-empty', () => { - for (const [category, tokens] of Object.entries(RECOMMENDED_TOKENS)) { - expect(tokens.length).toBeGreaterThan(0); - } - }); - - it('examples covers colors, typography, and components', () => { - expect(Object.keys(EXAMPLES.colors).length).toBeGreaterThan(0); - expect(Object.keys(EXAMPLES.typography).length).toBeGreaterThan(0); - expect(Object.keys(EXAMPLES.components).length).toBeGreaterThan(0); - }); -}); - -// ── Derived constants ───────────────────────────────────────────────── - -describe('spec-config derived constants', () => { - it('CANONICAL_ORDER length matches SECTIONS length', () => { - expect(CANONICAL_ORDER.length).toBe(SECTIONS.length); - }); - - it('resolveAlias returns canonical for known alias', () => { - // Pick the first alias we can find - const sectionWithAlias = SECTIONS.find(s => s.aliases && s.aliases.length > 0); - const alias = sectionWithAlias?.aliases?.[0]; - if (alias) { - expect(resolveAlias(alias)).toBe(sectionWithAlias.canonical); - } - }); - - it('resolveAlias returns input for unknown heading', () => { - expect(resolveAlias('NonExistentSection')).toBe('NonExistentSection'); - }); - - it('VALID_TYPOGRAPHY_PROPS length matches TYPOGRAPHY_PROPERTIES', () => { - expect(VALID_TYPOGRAPHY_PROPS.length).toBe(TYPOGRAPHY_PROPERTIES.length); - }); - - it('VALID_COMPONENT_SUB_TOKENS length matches COMPONENT_SUB_TOKENS', () => { - expect(VALID_COMPONENT_SUB_TOKENS.length).toBe(COMPONENT_SUB_TOKENS.length); - }); - - it('SECTION_ALIASES maps every alias to a canonical name', () => { - for (const [alias, canonical] of Object.entries(SECTION_ALIASES)) { - expect(CANONICAL_ORDER).toContain(canonical); - } - }); -}); +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { describe, it, expect } from 'bun:test'; +import { writeFileSync, unlinkSync } from 'node:fs'; +import { + loadSpecConfig, + getSpecConfig, + STANDARD_UNITS, + SECTIONS, + TYPOGRAPHY_PROPERTIES, + COMPONENT_SUB_TOKENS, + CORE_COLOR_ROLES, + RECOMMENDED_TOKENS, + EXAMPLES, + CANONICAL_ORDER, + SECTION_ALIASES, + resolveAlias, + VALID_TYPOGRAPHY_PROPS, + VALID_COMPONENT_SUB_TOKENS, + PRIMITIVE_TYPES, +} from './spec-config.js'; + +// ── Loader robustness ───────────────────────────────────────────────── + +describe('spec-config loader', () => { + it('throws when file does not exist', () => { + expect(() => loadSpecConfig('non-existent.yaml')).toThrow(); + }); + + it('throws when YAML is malformed', () => { + const path = '__test_malformed.yaml'; + writeFileSync(path, 'invalid: yaml: :'); + try { + expect(() => loadSpecConfig(path)).toThrow(); + } finally { + unlinkSync(path); + } + }); + + it('throws when required fields are missing', () => { + const path = '__test_incomplete.yaml'; + writeFileSync(path, 'version: alpha\nunits: [px]'); + try { + expect(() => loadSpecConfig(path)).toThrow(); + } finally { + unlinkSync(path); + } + }); + + it('throws when sections array is empty', () => { + const path = '__test_empty_sections.yaml'; + writeFileSync(path, [ + 'version: alpha', + 'units: [px]', + 'sections: []', + 'typography_properties: [{name: x, type: y}]', + 'component_sub_tokens: [{name: x, type: y}]', + 'color_roles: [primary]', + 'recommended_tokens: {a: [b]}', + 'examples:', + ' colors: {a: "#000"}', + ' typography: {a: {fontFamily: x}}', + ' components: {a: {bg: x}}', + ].join('\n')); + try { + expect(() => loadSpecConfig(path)).toThrow(); + } finally { + unlinkSync(path); + } + }); + + it('does not write to stdout or stderr', () => { + const originalLog = console.log; + const originalError = console.error; + const logs: string[] = []; + console.log = (...args: unknown[]) => logs.push(args.join(' ')); + console.error = (...args: unknown[]) => logs.push(args.join(' ')); + try { + loadSpecConfig(); + expect(logs.length).toBe(0); + } finally { + console.log = originalLog; + console.error = originalError; + } + }); +}); + +// ── Lazy loading ────────────────────────────────────────────────────── + +describe('spec-config lazy loading', () => { + it('getSpecConfig returns a valid config object', () => { + const config = getSpecConfig(); + expect(config.version).toBeString(); + expect(config.units.length).toBeGreaterThan(0); + expect(config.sections.length).toBeGreaterThan(0); + }); + + it('getSpecConfig returns the same cached instance on subsequent calls', () => { + const first = getSpecConfig(); + const second = getSpecConfig(); + expect(first).toBe(second); + }); +}); + +// ── Structural invariants ───────────────────────────────────────────── +// These never need updating when values change. +// They catch real bugs: duplicates, empty arrays, collision. + +describe('spec-config structural invariants', () => { + it('sections are non-empty', () => { + expect(SECTIONS.length).toBeGreaterThan(0); + }); + + it('section canonical names are unique', () => { + const names = SECTIONS.map(s => s.canonical); + expect(new Set(names).size).toBe(names.length); + }); + + it('no alias collides with a canonical name', () => { + const canonicals = new Set(SECTIONS.map(s => s.canonical)); + const aliases = SECTIONS.flatMap(s => s.aliases ?? []); + for (const alias of aliases) { + expect(canonicals.has(alias)).toBe(false); + } + }); + + it('aliases are unique across all sections', () => { + const aliases = SECTIONS.flatMap(s => s.aliases ?? []); + expect(new Set(aliases).size).toBe(aliases.length); + }); + + it('typography property names are unique', () => { + const names = TYPOGRAPHY_PROPERTIES.map(p => p.name); + expect(new Set(names).size).toBe(names.length); + }); + + it('component sub-token names are unique', () => { + const names = COMPONENT_SUB_TOKENS.map(p => p.name); + expect(new Set(names).size).toBe(names.length); + }); + + it('color roles are unique', () => { + expect(new Set(CORE_COLOR_ROLES).size).toBe(CORE_COLOR_ROLES.length); + }); + + it('units are non-empty and unique', () => { + expect(STANDARD_UNITS.length).toBeGreaterThan(0); + expect(new Set(STANDARD_UNITS).size).toBe(STANDARD_UNITS.length); + }); + + it('recommended token categories are non-empty', () => { + for (const [category, tokens] of Object.entries(RECOMMENDED_TOKENS)) { + expect(tokens.length).toBeGreaterThan(0); + } + }); + + it('examples covers colors, typography, and components', () => { + expect(Object.keys(EXAMPLES.colors).length).toBeGreaterThan(0); + expect(Object.keys(EXAMPLES.typography).length).toBeGreaterThan(0); + expect(Object.keys(EXAMPLES.components).length).toBeGreaterThan(0); + }); +}); + +// ── Derived constants ───────────────────────────────────────────────── + +describe('spec-config derived constants', () => { + it('CANONICAL_ORDER length matches SECTIONS length', () => { + expect(CANONICAL_ORDER.length).toBe(SECTIONS.length); + }); + + it('resolveAlias returns canonical for known alias', () => { + // Pick the first alias we can find + const sectionWithAlias = SECTIONS.find(s => s.aliases && s.aliases.length > 0); + const alias = sectionWithAlias?.aliases?.[0]; + if (alias) { + expect(resolveAlias(alias)).toBe(sectionWithAlias.canonical); + } + }); + + it('resolveAlias returns input for unknown heading', () => { + expect(resolveAlias('NonExistentSection')).toBe('NonExistentSection'); + }); + + it('VALID_TYPOGRAPHY_PROPS length matches TYPOGRAPHY_PROPERTIES', () => { + expect(VALID_TYPOGRAPHY_PROPS.length).toBe(TYPOGRAPHY_PROPERTIES.length); + }); + + it('VALID_COMPONENT_SUB_TOKENS length matches COMPONENT_SUB_TOKENS', () => { + expect(VALID_COMPONENT_SUB_TOKENS.length).toBe(COMPONENT_SUB_TOKENS.length); + }); + + it('SECTION_ALIASES maps every alias to a canonical name', () => { + for (const [alias, canonical] of Object.entries(SECTION_ALIASES)) { + expect(CANONICAL_ORDER).toContain(canonical); + } + }); +}); + +// ── PRIMITIVE_TYPES ─────────────────────────────────────────────────── + +describe('spec-config PRIMITIVE_TYPES', () => { + it('contains Color and Dimension entries', () => { + expect(PRIMITIVE_TYPES).toHaveProperty('Color'); + expect(PRIMITIVE_TYPES).toHaveProperty('Dimension'); + }); + + it('every type has a non-empty description', () => { + for (const [, def] of Object.entries(PRIMITIVE_TYPES)) { + expect(def.description.length).toBeGreaterThan(0); + } + }); + + it('Color has at least one format entry', () => { + expect(PRIMITIVE_TYPES['Color']!.formats?.length).toBeGreaterThan(0); + }); + + it('Dimension has no formats list (units come from STANDARD_UNITS)', () => { + expect(PRIMITIVE_TYPES['Dimension']!.formats).toBeUndefined(); + }); +}); diff --git a/packages/cli/src/linter/spec-config.ts b/packages/cli/src/linter/spec-config.ts index b146a47b..6f00c1aa 100644 --- a/packages/cli/src/linter/spec-config.ts +++ b/packages/cli/src/linter/spec-config.ts @@ -1,198 +1,219 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { readFileSync } from 'node:fs'; -import { resolve, dirname } from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { parse } from 'yaml'; -import { z } from 'zod'; - -/** - * DESIGN.md Spec Configuration - * - * THE single source of truth for the DESIGN.md format specification. - * Both the linter and the spec generator read from this file. - * - * To change what the spec says: - * 1. Edit spec-config.yaml - * 2. Run `bun run spec:gen` to regenerate docs/spec.md - * 3. Run `bun test` to verify linter alignment - */ - -// ── Schema ──────────────────────────────────────────────────────────── - -const PropertyDefSchema = z.object({ - name: z.string(), - type: z.string(), - description: z.string().optional(), -}); - -const ConfigSchema = z.object({ - version: z.string(), - limits: z.object({ - max_token_nesting_depth: z.number().default(20), - max_reference_depth: z.number().default(10), - }).default({}), - units: z.array(z.string()).min(1), - sections: z.array(z.object({ - canonical: z.string(), - aliases: z.array(z.string()).optional(), - })).min(1), - typography_properties: z.array(PropertyDefSchema).min(1), - component_sub_tokens: z.array(PropertyDefSchema).min(1), - color_roles: z.array(z.string()).min(1), - recommended_tokens: z.record(z.string(), z.array(z.string())), - examples: z.object({ - colors: z.record(z.string(), z.string()), - typography: z.record(z.string(), z.record(z.string(), z.union([z.string(), z.number()]))), - components: z.record(z.string(), z.record(z.string(), z.string())), - }), -}); - -// ── Load & Validate ────────────────────────────────────────────────── - -export function loadSpecConfig(filePath?: string) { - const currentDir = dirname(fileURLToPath(import.meta.url)); - const yamlPath = filePath ? resolve(filePath) : resolve(currentDir, './spec-config.yaml'); - const raw = parse(readFileSync(yamlPath, 'utf-8')); - return ConfigSchema.parse(raw); -} - -// ── Lazy singleton ─────────────────────────────────────────────────── -// Config is loaded on first access, not at module evaluation time. -// This prevents redundant file reads and provides a clean entry point -// for programmatic consumers who want to defer loading. - -type ParsedConfig = ReturnType; -let _cachedConfig: ParsedConfig | undefined; - -/** Return the parsed spec config, loading and caching it on first call. */ -export function getSpecConfig(): ParsedConfig { - if (!_cachedConfig) { - _cachedConfig = loadSpecConfig(); - } - return _cachedConfig; -} - -// ── Interfaces ─────────────────────────────────────────────────────── - -export interface SectionDef { - /** The canonical section heading. */ - canonical: string; - /** Acceptable alternative headings that resolve to this section. */ - aliases?: readonly string[] | undefined; -} - -export interface TypographyPropertyDef { - /** Property name as it appears in YAML. */ - name: string; - /** Human-readable type for the spec document. */ - type: string; - /** Extended description for the spec (appears after the type). */ - description?: string | undefined; -} - -export interface ComponentSubTokenDef { - /** Sub-token property name. */ - name: string; - /** The type displayed in the spec (e.g., 'Color', 'Dimension'). */ - type: string; - /** Extended description for the spec (appears after the type). */ - description?: string | undefined; -} - -// ── Constant exports ───────────────────────────────────────────────── -// These are eagerly initialized from the lazy singleton on first import. -// The singleton cache ensures the YAML file is read exactly once. - -const config = getSpecConfig(); - -/** Current spec version. Appears in the schema and the front matter example. */ -export const SPEC_VERSION = config.version; - -/** Performance and safety limits for the model handler. */ -export const MAX_TOKEN_NESTING_DEPTH = config.limits.max_token_nesting_depth; -export const MAX_REFERENCE_DEPTH = config.limits.max_reference_depth; - -/** Units the spec formally supports for Dimension values. */ -export const STANDARD_UNITS = config.units; -export type StandardUnit = (typeof STANDARD_UNITS)[number]; - -export const SECTIONS = config.sections; - -export const TYPOGRAPHY_PROPERTIES: readonly TypographyPropertyDef[] = config.typography_properties; - -export const COMPONENT_SUB_TOKENS: readonly ComponentSubTokenDef[] = config.component_sub_tokens; - -/** Core color roles that every design system should define. */ -export const CORE_COLOR_ROLES = config.color_roles; - -/** Non-normative recommended token names, organized by category. */ -export const RECOMMENDED_TOKENS = config.recommended_tokens; - -/** Canonical examples that appear in the generated spec document. */ -export const EXAMPLES = config.examples; - -// ── Derived constants ───────────────────────────────────────────────── - -/** Ordered list of canonical section names. */ -export const CANONICAL_ORDER = SECTIONS.map(s => s.canonical); - -/** Map of alias → canonical name. */ -export const SECTION_ALIASES: Record = Object.fromEntries( - SECTIONS.flatMap(s => - (s.aliases ?? []).map(alias => [alias, s.canonical]) - ) -); - -/** Resolve a section heading to its canonical name. */ -export function resolveAlias(heading: string): string { - return SECTION_ALIASES[heading] ?? heading; -} - -/** Valid typography property names (for linter validation). */ -export const VALID_TYPOGRAPHY_PROPS = TYPOGRAPHY_PROPERTIES.map(p => p.name); - -/** Valid component sub-token names (for linter validation). */ -export const VALID_COMPONENT_SUB_TOKENS = COMPONENT_SUB_TOKENS.map(p => p.name); - -// ── Aggregate type ──────────────────────────────────────────────────── - -/** All config values bundled as a single object for renderer injection. */ -export interface SpecConfig { - SPEC_VERSION: typeof SPEC_VERSION; - MAX_TOKEN_NESTING_DEPTH: typeof MAX_TOKEN_NESTING_DEPTH; - MAX_REFERENCE_DEPTH: typeof MAX_REFERENCE_DEPTH; - STANDARD_UNITS: typeof STANDARD_UNITS; - SECTIONS: typeof SECTIONS; - TYPOGRAPHY_PROPERTIES: typeof TYPOGRAPHY_PROPERTIES; - COMPONENT_SUB_TOKENS: typeof COMPONENT_SUB_TOKENS; - CORE_COLOR_ROLES: typeof CORE_COLOR_ROLES; - RECOMMENDED_TOKENS: typeof RECOMMENDED_TOKENS; - EXAMPLES: typeof EXAMPLES; -} - -/** Build a SpecConfig from the module's exports. */ -export const SPEC_CONFIG: SpecConfig = { - SPEC_VERSION, - MAX_TOKEN_NESTING_DEPTH, - MAX_REFERENCE_DEPTH, - STANDARD_UNITS, - SECTIONS, - TYPOGRAPHY_PROPERTIES, - COMPONENT_SUB_TOKENS, - CORE_COLOR_ROLES, - RECOMMENDED_TOKENS, - EXAMPLES, -}; +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { readFileSync } from 'node:fs'; +import { resolve, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { parse } from 'yaml'; +import { z } from 'zod'; + +/** + * DESIGN.md Spec Configuration + * + * THE single source of truth for the DESIGN.md format specification. + * Both the linter and the spec generator read from this file. + * + * To change what the spec says: + * 1. Edit spec-config.yaml + * 2. Run `bun run spec:gen` to regenerate docs/spec.md + * 3. Run `bun test` to verify linter alignment + */ + +// ── Schema ──────────────────────────────────────────────────────────── + +const PropertyDefSchema = z.object({ + name: z.string(), + type: z.string(), + description: z.string().optional(), +}); + +const TypeDefSchema = z.object({ + description: z.string(), + formats: z.array(z.string()).optional(), + note: z.string().optional(), +}); + +const ConfigSchema = z.object({ + version: z.string(), + limits: z.object({ + max_token_nesting_depth: z.number().default(20), + max_reference_depth: z.number().default(10), + }).default({}), + units: z.array(z.string()).min(1), + sections: z.array(z.object({ + canonical: z.string(), + aliases: z.array(z.string()).optional(), + })).min(1), + typography_properties: z.array(PropertyDefSchema).min(1), + component_sub_tokens: z.array(PropertyDefSchema).min(1), + color_roles: z.array(z.string()).min(1), + types: z.record(z.string(), TypeDefSchema).optional().default({}), + recommended_tokens: z.record(z.string(), z.array(z.string())), + examples: z.object({ + colors: z.record(z.string(), z.string()), + typography: z.record(z.string(), z.record(z.string(), z.union([z.string(), z.number()]))), + components: z.record(z.string(), z.record(z.string(), z.string())), + }), +}); + +// ── Load & Validate ────────────────────────────────────────────────── + +export function loadSpecConfig(filePath?: string) { + const currentDir = dirname(fileURLToPath(import.meta.url)); + const yamlPath = filePath ? resolve(filePath) : resolve(currentDir, './spec-config.yaml'); + const raw = parse(readFileSync(yamlPath, 'utf-8')); + return ConfigSchema.parse(raw); +} + +// ── Lazy singleton ─────────────────────────────────────────────────── +// Config is loaded on first access, not at module evaluation time. +// This prevents redundant file reads and provides a clean entry point +// for programmatic consumers who want to defer loading. + +type ParsedConfig = ReturnType; +let _cachedConfig: ParsedConfig | undefined; + +/** Return the parsed spec config, loading and caching it on first call. */ +export function getSpecConfig(): ParsedConfig { + if (!_cachedConfig) { + _cachedConfig = loadSpecConfig(); + } + return _cachedConfig; +} + +// ── Interfaces ─────────────────────────────────────────────────────── + +export interface SectionDef { + /** The canonical section heading. */ + canonical: string; + /** Acceptable alternative headings that resolve to this section. */ + aliases?: readonly string[] | undefined; +} + +export interface TypographyPropertyDef { + /** Property name as it appears in YAML. */ + name: string; + /** Human-readable type for the spec document. */ + type: string; + /** Extended description for the spec (appears after the type). */ + description?: string | undefined; +} + +export interface ComponentSubTokenDef { + /** Sub-token property name. */ + name: string; + /** The type displayed in the spec (e.g., 'Color', 'Dimension'). */ + type: string; + /** Extended description for the spec (appears after the type). */ + description?: string | undefined; +} + +export interface TypeDef { + /** Human-readable description of the type. */ + description: string; + /** List of accepted format strings, rendered as bullets. */ + formats?: readonly string[]; + /** Follow-up note rendered as a separate paragraph. May contain \n\n for multiple paragraphs. */ + note?: string; +} + +// ── Constant exports ───────────────────────────────────────────────── +// These are eagerly initialized from the lazy singleton on first import. +// The singleton cache ensures the YAML file is read exactly once. + +const config = getSpecConfig(); + +/** Current spec version. Appears in the schema and the front matter example. */ +export const SPEC_VERSION = config.version; + +/** Performance and safety limits for the model handler. */ +export const MAX_TOKEN_NESTING_DEPTH = config.limits.max_token_nesting_depth; +export const MAX_REFERENCE_DEPTH = config.limits.max_reference_depth; + +/** Units the spec formally supports for Dimension values. */ +export const STANDARD_UNITS = config.units; +export type StandardUnit = (typeof STANDARD_UNITS)[number]; + +export const SECTIONS = config.sections; + +export const TYPOGRAPHY_PROPERTIES: readonly TypographyPropertyDef[] = config.typography_properties; + +export const COMPONENT_SUB_TOKENS: readonly ComponentSubTokenDef[] = config.component_sub_tokens; + +/** Core color roles that every design system should define. */ +export const CORE_COLOR_ROLES = config.color_roles; + +/** Non-normative recommended token names, organized by category. */ +export const RECOMMENDED_TOKENS = config.recommended_tokens; + +/** Canonical examples that appear in the generated spec document. */ +export const EXAMPLES = config.examples; + +/** Primitive type definitions (Color, Dimension, etc.) for the spec document. */ +export const PRIMITIVE_TYPES: Record = config.types; + +// ── Derived constants ───────────────────────────────────────────────── + +/** Ordered list of canonical section names. */ +export const CANONICAL_ORDER = SECTIONS.map(s => s.canonical); + +/** Map of alias → canonical name. */ +export const SECTION_ALIASES: Record = Object.fromEntries( + SECTIONS.flatMap(s => + (s.aliases ?? []).map(alias => [alias, s.canonical]) + ) +); + +/** Resolve a section heading to its canonical name. */ +export function resolveAlias(heading: string): string { + return SECTION_ALIASES[heading] ?? heading; +} + +/** Valid typography property names (for linter validation). */ +export const VALID_TYPOGRAPHY_PROPS = TYPOGRAPHY_PROPERTIES.map(p => p.name); + +/** Valid component sub-token names (for linter validation). */ +export const VALID_COMPONENT_SUB_TOKENS = COMPONENT_SUB_TOKENS.map(p => p.name); + +// ── Aggregate type ──────────────────────────────────────────────────── + +/** All config values bundled as a single object for renderer injection. */ +export interface SpecConfig { + SPEC_VERSION: typeof SPEC_VERSION; + MAX_TOKEN_NESTING_DEPTH: typeof MAX_TOKEN_NESTING_DEPTH; + MAX_REFERENCE_DEPTH: typeof MAX_REFERENCE_DEPTH; + STANDARD_UNITS: typeof STANDARD_UNITS; + SECTIONS: typeof SECTIONS; + TYPOGRAPHY_PROPERTIES: typeof TYPOGRAPHY_PROPERTIES; + COMPONENT_SUB_TOKENS: typeof COMPONENT_SUB_TOKENS; + CORE_COLOR_ROLES: typeof CORE_COLOR_ROLES; + RECOMMENDED_TOKENS: typeof RECOMMENDED_TOKENS; + EXAMPLES: typeof EXAMPLES; + PRIMITIVE_TYPES: typeof PRIMITIVE_TYPES; +} + +/** Build a SpecConfig from the module's exports. */ +export const SPEC_CONFIG: SpecConfig = { + SPEC_VERSION, + MAX_TOKEN_NESTING_DEPTH, + MAX_REFERENCE_DEPTH, + STANDARD_UNITS, + SECTIONS, + TYPOGRAPHY_PROPERTIES, + COMPONENT_SUB_TOKENS, + CORE_COLOR_ROLES, + RECOMMENDED_TOKENS, + EXAMPLES, + PRIMITIVE_TYPES, +}; diff --git a/packages/cli/src/linter/spec-config.yaml b/packages/cli/src/linter/spec-config.yaml index 67dfa143..92cbb801 100644 --- a/packages/cli/src/linter/spec-config.yaml +++ b/packages/cli/src/linter/spec-config.yaml @@ -1,149 +1,165 @@ -# Copyright 2026 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# DESIGN.md Spec Configuration -# This file is the single source of truth for the DESIGN.md format specification. -# Edit this file, then run `bun run spec:gen` to regenerate docs/spec.md. - -version: alpha - -# Performance and safety limits for the model handler. -limits: - max_token_nesting_depth: 20 - max_reference_depth: 10 - -units: - - px - - em - - rem - -sections: - - canonical: Overview - aliases: - - Brand & Style - - canonical: Colors - - canonical: Typography - - canonical: Layout - aliases: - - Layout & Spacing - - canonical: Elevation & Depth - aliases: - - Elevation - - canonical: Shapes - - canonical: Components - - canonical: "Do's and Don'ts" - -typography_properties: - - name: fontFamily - type: string - - name: fontSize - type: Dimension - - name: fontWeight - type: number - description: "A numeric font weight value (e.g., `400`, `700`). In YAML, this may be expressed as either a bare number or a quoted string; both are equivalent." - - name: lineHeight - type: "Dimension | number" - description: "Accepts either a Dimension (e.g., `24px`, `1.5rem`) or a unitless number (e.g., `1.6`). A unitless number represents a multiplier of the element's `fontSize`, which is the recommended CSS practice." - - name: letterSpacing - type: Dimension - - name: fontFeature - type: string - description: "configures\n [`font-feature-settings`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/font-feature-settings)." - - name: fontVariation - type: string - description: "configures\n [`font-variation-settings`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/font-variation-settings)." - -component_sub_tokens: - - name: backgroundColor - type: Color - - name: textColor - type: Color - - name: typography - type: Typography - - name: rounded - type: Dimension - - name: padding - type: Dimension - - name: size - type: Dimension - - name: height - type: Dimension - - name: width - type: Dimension - -color_roles: - - primary - - secondary - - tertiary - - neutral - -recommended_tokens: - colors: - - primary - - secondary - - tertiary - - neutral - - surface - - on-surface - - error - typography: - - headline-display - - headline-lg - - headline-md - - body-lg - - body-md - - body-sm - - label-lg - - label-md - - label-sm - rounded: - - none - - sm - - md - - lg - - xl - - full - -examples: - colors: - primary: "#1A1C1E" - secondary: "#6C7278" - tertiary: "#B8422E" - neutral: "#F7F5F2" - typography: - h1: - fontFamily: Public Sans - fontSize: 48px - fontWeight: 600 - lineHeight: 1.1 - letterSpacing: "-0.02em" - body-md: - fontFamily: Public Sans - fontSize: 16px - fontWeight: 400 - lineHeight: 1.6 - label-caps: - fontFamily: Space Grotesk - fontSize: 12px - fontWeight: 500 - lineHeight: 1.0 - letterSpacing: "0.1em" - components: - button-primary: - backgroundColor: "{colors.primary-60}" - textColor: "{colors.primary-20}" - rounded: "{rounded.md}" - padding: 12px - button-primary-hover: - backgroundColor: "{colors.primary-70}" +# Copyright 2026 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# DESIGN.md Spec Configuration +# This file is the single source of truth for the DESIGN.md format specification. +# Edit this file, then run `bun run spec:gen` to regenerate docs/spec.md. + +version: alpha + +# Performance and safety limits for the model handler. +limits: + max_token_nesting_depth: 20 + max_reference_depth: 10 + +units: + - px + - em + - rem + +types: + Color: + description: "A color value is any valid CSS color string. Supported formats include:" + formats: + - "Hex: `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`" + - "Named colors: `red`, `cornflowerblue`, `transparent`" + - "Functional: `rgb()`, `rgba()`, `hsl()`, `hsla()`, `hwb()`" + - "Wide-gamut: `oklch()`, `oklab()`, `lch()`, `lab()`" + - "Mixing: `color-mix(in srgb, ...)`" + note: | + All color values are internally converted to sRGB for WCAG contrast checking. The original format is preserved for display and export. + + Hex notation (`#RRGGBB`) remains the recommended default for simplicity and broad tooling support. + Dimension: + description: "A dimension value is a string with a unit suffix." + +sections: + - canonical: Overview + aliases: + - Brand & Style + - canonical: Colors + - canonical: Typography + - canonical: Layout + aliases: + - Layout & Spacing + - canonical: Elevation & Depth + aliases: + - Elevation + - canonical: Shapes + - canonical: Components + - canonical: "Do's and Don'ts" + +typography_properties: + - name: fontFamily + type: string + - name: fontSize + type: Dimension + - name: fontWeight + type: number + description: "A numeric font weight value (e.g., `400`, `700`). In YAML, this may be expressed as either a bare number or a quoted string; both are equivalent." + - name: lineHeight + type: "Dimension | number" + description: "Accepts either a Dimension (e.g., `24px`, `1.5rem`) or a unitless number (e.g., `1.6`). A unitless number represents a multiplier of the element's `fontSize`, which is the recommended CSS practice." + - name: letterSpacing + type: Dimension + - name: fontFeature + type: string + description: "configures\n [`font-feature-settings`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/font-feature-settings)." + - name: fontVariation + type: string + description: "configures\n [`font-variation-settings`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/font-variation-settings)." + +component_sub_tokens: + - name: backgroundColor + type: Color + - name: textColor + type: Color + - name: typography + type: Typography + - name: rounded + type: Dimension + - name: padding + type: Dimension + - name: size + type: Dimension + - name: height + type: Dimension + - name: width + type: Dimension + +color_roles: + - primary + - secondary + - tertiary + - neutral + +recommended_tokens: + colors: + - primary + - secondary + - tertiary + - neutral + - surface + - on-surface + - error + typography: + - headline-display + - headline-lg + - headline-md + - body-lg + - body-md + - body-sm + - label-lg + - label-md + - label-sm + rounded: + - none + - sm + - md + - lg + - xl + - full + +examples: + colors: + primary: "#1A1C1E" + secondary: "#6C7278" + tertiary: "#B8422E" + neutral: "#F7F5F2" + typography: + h1: + fontFamily: Public Sans + fontSize: 48px + fontWeight: 600 + lineHeight: 1.1 + letterSpacing: "-0.02em" + body-md: + fontFamily: Public Sans + fontSize: 16px + fontWeight: 400 + lineHeight: 1.6 + label-caps: + fontFamily: Space Grotesk + fontSize: 12px + fontWeight: 500 + lineHeight: 1.0 + letterSpacing: "0.1em" + components: + button-primary: + backgroundColor: "{colors.primary-60}" + textColor: "{colors.primary-20}" + rounded: "{rounded.md}" + padding: 12px + button-primary-hover: + backgroundColor: "{colors.primary-70}" From cebe05962d088a1a15d4b098aa20af77f01f1a5a Mon Sep 17 00:00:00 2001 From: Emp1500 Date: Thu, 25 Jun 2026 11:29:46 +0000 Subject: [PATCH 4/7] feat: add typeDefinitions renderer and wire into spec generator scope --- .../cli/src/linter/spec-gen/compiler.test.ts | 179 ++++++------ packages/cli/src/linter/spec-gen/generate.ts | 185 ++++++------ .../cli/src/linter/spec-gen/renderers.test.ts | 60 ++++ packages/cli/src/linter/spec-gen/renderers.ts | 263 ++++++++++-------- 4 files changed, 384 insertions(+), 303 deletions(-) create mode 100644 packages/cli/src/linter/spec-gen/renderers.test.ts diff --git a/packages/cli/src/linter/spec-gen/compiler.test.ts b/packages/cli/src/linter/spec-gen/compiler.test.ts index 656c796d..60e90ab5 100644 --- a/packages/cli/src/linter/spec-gen/compiler.test.ts +++ b/packages/cli/src/linter/spec-gen/compiler.test.ts @@ -1,88 +1,91 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { describe, it, expect } from 'bun:test'; -import { readFile } from 'node:fs/promises'; -import { resolve } from 'node:path'; -import { compileMdx } from './compiler.js'; -import { SPEC_CONFIG } from '../spec-config.js'; -import * as renderers from './renderers.js'; - -describe('compileMdx', () => { - it('passes plain markdown through unchanged', async () => { - const input = '# Hello\n\nThis is a paragraph.\n'; - const result = await compileMdx(input, {}); - expect(result).toBe('# Hello\n\nThis is a paragraph.\n'); - }); - - it('evaluates inline expressions', async () => { - const input = 'The answer is {1 + 1}.\n'; - const result = await compileMdx(input, {}); - expect(result).toContain('The answer is 2.'); - }); - - it('evaluates expressions with scope variables', async () => { - const input = 'Valid units: {UNITS.join(", ")}.\n'; - const result = await compileMdx(input, { UNITS: ['px', 'rem'] }); - expect(result).toContain('Valid units: px, rem.'); - }); - - it('evaluates block expressions that produce multi-line content', async () => { - const input = '# Items\n\n{ITEMS.map((item, i) => `${i + 1}. ${item}`).join("\\n")}\n'; - const result = await compileMdx(input, { ITEMS: ['Alpha', 'Beta'] }); - expect(result).toContain('1. Alpha'); - expect(result).toContain('2. Beta'); - }); - - it('strips import statements from output', async () => { - const input = 'import { X } from "./foo"\n\n# Title\n'; - const result = await compileMdx(input, {}); - expect(result).not.toContain('import'); - expect(result).toContain('# Title'); - }); - - it('preserves expressions inside fenced code blocks', async () => { - const input = '```yaml\ncolors:\n primary: "{colors.primary}"\n```\n'; - const result = await compileMdx(input, {}); - expect(result).toContain('{colors.primary}'); - }); - - it('compiles the full spec.mdx with spec-config scope', async () => { - const mdxPath = resolve(import.meta.dir, 'spec.mdx'); - const source = await readFile(mdxPath, 'utf-8'); - - const cfg = SPEC_CONFIG; - const scope = { - ...cfg, - frontmatterExample: () => renderers.frontmatterExample(cfg), - colorsExample: () => renderers.colorsExample(cfg), - typographyExample: () => renderers.typographyExample(cfg), - componentsExample: () => renderers.componentsExample(cfg), - typographyPropertyList: () => renderers.typographyPropertyList(cfg), - sectionOrderList: () => renderers.sectionOrderList(cfg), - componentSubTokenList: () => renderers.componentSubTokenList(cfg), - recommendedTokens: () => renderers.recommendedTokens(cfg), - }; - - const result = await compileMdx(source, scope); - - // Verify key config-driven content appears - expect(result).toContain('px, em, rem'); - expect(result).toContain('**Overview**'); - expect(result).toContain('**Components**'); - expect(result).toContain('backgroundColor'); - expect(result).toContain('`headline-display`'); - expect(result).toContain('#1A1C1E'); - }); -}); +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { describe, it, expect } from 'bun:test'; +import { readFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { compileMdx } from './compiler.js'; +import { SPEC_CONFIG } from '../spec-config.js'; +import * as renderers from './renderers.js'; + +describe('compileMdx', () => { + it('passes plain markdown through unchanged', async () => { + const input = '# Hello\n\nThis is a paragraph.\n'; + const result = await compileMdx(input, {}); + expect(result).toBe('# Hello\n\nThis is a paragraph.\n'); + }); + + it('evaluates inline expressions', async () => { + const input = 'The answer is {1 + 1}.\n'; + const result = await compileMdx(input, {}); + expect(result).toContain('The answer is 2.'); + }); + + it('evaluates expressions with scope variables', async () => { + const input = 'Valid units: {UNITS.join(", ")}.\n'; + const result = await compileMdx(input, { UNITS: ['px', 'rem'] }); + expect(result).toContain('Valid units: px, rem.'); + }); + + it('evaluates block expressions that produce multi-line content', async () => { + const input = '# Items\n\n{ITEMS.map((item, i) => `${i + 1}. ${item}`).join("\\n")}\n'; + const result = await compileMdx(input, { ITEMS: ['Alpha', 'Beta'] }); + expect(result).toContain('1. Alpha'); + expect(result).toContain('2. Beta'); + }); + + it('strips import statements from output', async () => { + const input = 'import { X } from "./foo"\n\n# Title\n'; + const result = await compileMdx(input, {}); + expect(result).not.toContain('import'); + expect(result).toContain('# Title'); + }); + + it('preserves expressions inside fenced code blocks', async () => { + const input = '```yaml\ncolors:\n primary: "{colors.primary}"\n```\n'; + const result = await compileMdx(input, {}); + expect(result).toContain('{colors.primary}'); + }); + + it('compiles the full spec.mdx with spec-config scope', async () => { + const mdxPath = resolve(import.meta.dir, 'spec.mdx'); + const source = await readFile(mdxPath, 'utf-8'); + + const cfg = SPEC_CONFIG; + const scope = { + ...cfg, + frontmatterExample: () => renderers.frontmatterExample(cfg), + colorsExample: () => renderers.colorsExample(cfg), + typographyExample: () => renderers.typographyExample(cfg), + componentsExample: () => renderers.componentsExample(cfg), + typographyPropertyList: () => renderers.typographyPropertyList(cfg), + sectionOrderList: () => renderers.sectionOrderList(cfg), + componentSubTokenList: () => renderers.componentSubTokenList(cfg), + recommendedTokens: () => renderers.recommendedTokens(cfg), + typeDefinitions: () => renderers.typeDefinitions(cfg), + }; + + const result = await compileMdx(source, scope); + + // Verify key config-driven content appears + expect(result).toContain('px, em, rem'); + expect(result).toContain('**Overview**'); + expect(result).toContain('**Components**'); + expect(result).toContain('backgroundColor'); + expect(result).toContain('`headline-display`'); + expect(result).toContain('#1A1C1E'); + expect(result).toContain('**Color**'); + expect(result).toContain('oklch()'); + }); +}); diff --git a/packages/cli/src/linter/spec-gen/generate.ts b/packages/cli/src/linter/spec-gen/generate.ts index b622ec37..a5f49634 100644 --- a/packages/cli/src/linter/spec-gen/generate.ts +++ b/packages/cli/src/linter/spec-gen/generate.ts @@ -1,92 +1,93 @@ -#!/usr/bin/env bun -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/** - * Generate docs/spec.md from docs/spec.mdx + spec-config.ts. - * - * Usage: - * bun run packages/linter/src/spec-gen/generate.ts - * bun run packages/linter/src/spec-gen/generate.ts --check - */ - -import { readFile, writeFile } from 'node:fs/promises'; -import { resolve } from 'node:path'; -import { compileMdx } from './compiler.js'; -import { SPEC_CONFIG } from '../spec-config.js'; -import * as renderers from './renderers.js'; - -const ROOT = resolve(import.meta.dir, '../../../../../'); -const MDX_PATH = resolve(import.meta.dir, 'spec.mdx'); -const OUTPUT_PATH = resolve(ROOT, 'docs/spec.md'); - -const isCheck = process.argv.includes('--check'); - -async function main() { - const source = await readFile(MDX_PATH, 'utf-8'); - - // Scope: raw config values + renderer functions - const cfg = SPEC_CONFIG; - const scope = { - ...cfg, - // Renderer functions — pre-bound to config so MDX calls are clean - frontmatterExample: () => renderers.frontmatterExample(cfg), - colorsExample: () => renderers.colorsExample(cfg), - typographyExample: () => renderers.typographyExample(cfg), - componentsExample: () => renderers.componentsExample(cfg), - typographyPropertyList: () => renderers.typographyPropertyList(cfg), - sectionOrderList: () => renderers.sectionOrderList(cfg), - componentSubTokenList: () => renderers.componentSubTokenList(cfg), - recommendedTokens: () => renderers.recommendedTokens(cfg), - }; - - const generated = await compileMdx(source, scope); - - // Prepend header comment - const header = `\n\n\n`; - const content = header + generated; - - if (isCheck) { - const existing = await readFile(OUTPUT_PATH, 'utf-8'); - - // Strip header for comparison (contains no timestamp, but future-proof) - const stripHeader = (s: string) => s.replace(/^\n/gm, ''); - const existingBody = stripHeader(existing); - const generatedBody = stripHeader(content); - - if (existingBody === generatedBody) { - console.log('✅ docs/spec.md is up to date.'); - process.exit(0); - } else { - console.error('❌ docs/spec.md is out of date. Run `bun run spec:gen` to regenerate.'); - - const existingLines = existingBody.split('\n'); - const generatedLines = generatedBody.split('\n'); - for (let i = 0; i < Math.max(existingLines.length, generatedLines.length); i++) { - if (existingLines[i] !== generatedLines[i]) { - console.error(` First difference at line ${i + 1}:`); - console.error(` - existing: ${existingLines[i]?.slice(0, 100)}`); - console.error(` + generated: ${generatedLines[i]?.slice(0, 100)}`); - break; - } - } - process.exit(1); - } - } - - await writeFile(OUTPUT_PATH, content); - console.log(`✅ Generated docs/spec.md (${content.split('\n').length} lines)`); -} - -main(); +#!/usr/bin/env bun +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * Generate docs/spec.md from docs/spec.mdx + spec-config.ts. + * + * Usage: + * bun run packages/linter/src/spec-gen/generate.ts + * bun run packages/linter/src/spec-gen/generate.ts --check + */ + +import { readFile, writeFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { compileMdx } from './compiler.js'; +import { SPEC_CONFIG } from '../spec-config.js'; +import * as renderers from './renderers.js'; + +const ROOT = resolve(import.meta.dir, '../../../../../'); +const MDX_PATH = resolve(import.meta.dir, 'spec.mdx'); +const OUTPUT_PATH = resolve(ROOT, 'docs/spec.md'); + +const isCheck = process.argv.includes('--check'); + +async function main() { + const source = await readFile(MDX_PATH, 'utf-8'); + + // Scope: raw config values + renderer functions + const cfg = SPEC_CONFIG; + const scope = { + ...cfg, + // Renderer functions — pre-bound to config so MDX calls are clean + frontmatterExample: () => renderers.frontmatterExample(cfg), + colorsExample: () => renderers.colorsExample(cfg), + typographyExample: () => renderers.typographyExample(cfg), + componentsExample: () => renderers.componentsExample(cfg), + typographyPropertyList: () => renderers.typographyPropertyList(cfg), + sectionOrderList: () => renderers.sectionOrderList(cfg), + componentSubTokenList: () => renderers.componentSubTokenList(cfg), + recommendedTokens: () => renderers.recommendedTokens(cfg), + typeDefinitions: () => renderers.typeDefinitions(cfg), + }; + + const generated = await compileMdx(source, scope); + + // Prepend header comment + const header = `\n\n\n`; + const content = header + generated; + + if (isCheck) { + const existing = await readFile(OUTPUT_PATH, 'utf-8'); + + // Strip header for comparison (contains no timestamp, but future-proof) + const stripHeader = (s: string) => s.replace(/^\n/gm, ''); + const existingBody = stripHeader(existing); + const generatedBody = stripHeader(content); + + if (existingBody === generatedBody) { + console.log('✅ docs/spec.md is up to date.'); + process.exit(0); + } else { + console.error('❌ docs/spec.md is out of date. Run `bun run spec:gen` to regenerate.'); + + const existingLines = existingBody.split('\n'); + const generatedLines = generatedBody.split('\n'); + for (let i = 0; i < Math.max(existingLines.length, generatedLines.length); i++) { + if (existingLines[i] !== generatedLines[i]) { + console.error(` First difference at line ${i + 1}:`); + console.error(` - existing: ${existingLines[i]?.slice(0, 100)}`); + console.error(` + generated: ${generatedLines[i]?.slice(0, 100)}`); + break; + } + } + process.exit(1); + } + } + + await writeFile(OUTPUT_PATH, content); + console.log(`✅ Generated docs/spec.md (${content.split('\n').length} lines)`); +} + +main(); diff --git a/packages/cli/src/linter/spec-gen/renderers.test.ts b/packages/cli/src/linter/spec-gen/renderers.test.ts new file mode 100644 index 00000000..8fc26b96 --- /dev/null +++ b/packages/cli/src/linter/spec-gen/renderers.test.ts @@ -0,0 +1,60 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { describe, it, expect } from 'bun:test'; +import { typeDefinitions } from './renderers.js'; +import { SPEC_CONFIG } from '../spec-config.js'; + +describe('typeDefinitions', () => { + it('includes **Color** with its description', () => { + const result = typeDefinitions(SPEC_CONFIG); + expect(result).toContain('**Color**'); + expect(result).toContain('A color value is any valid CSS color string'); + }); + + it('renders Color formats as bullet list', () => { + const result = typeDefinitions(SPEC_CONFIG); + expect(result).toContain('- Hex:'); + expect(result).toContain('- Wide-gamut:'); + expect(result).toContain('- Mixing:'); + }); + + it('includes **Dimension** with its description', () => { + const result = typeDefinitions(SPEC_CONFIG); + expect(result).toContain('**Dimension**'); + expect(result).toContain('A dimension value is a string with a unit suffix'); + }); + + it('appends STANDARD_UNITS inline to Dimension', () => { + const result = typeDefinitions(SPEC_CONFIG); + expect(result).toContain('Valid units are:'); + for (const unit of SPEC_CONFIG.STANDARD_UNITS) { + expect(result).toContain(unit); + } + }); + + it('does not render units as a bullet list for Dimension', () => { + const result = typeDefinitions(SPEC_CONFIG); + const dimensionStart = result.indexOf('**Dimension**'); + const afterDimension = result.slice(dimensionStart); + expect(afterDimension).not.toMatch(/\n- px/); + expect(afterDimension).not.toMatch(/\n- em/); + }); + + it('renders Color note paragraph', () => { + const result = typeDefinitions(SPEC_CONFIG); + expect(result).toContain('internally converted to sRGB'); + expect(result).toContain('recommended default'); + }); +}); diff --git a/packages/cli/src/linter/spec-gen/renderers.ts b/packages/cli/src/linter/spec-gen/renderers.ts index c7cfb985..fd5ccfd2 100644 --- a/packages/cli/src/linter/spec-gen/renderers.ts +++ b/packages/cli/src/linter/spec-gen/renderers.ts @@ -1,123 +1,140 @@ -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -/** - * Spec renderers: pure functions that turn spec-config data into - * markdown fragments. Used by the MDX compiler via scope injection. - * - * Each function returns a ready-to-embed markdown string. - */ - -import type { SpecConfig, TypographyPropertyDef, SectionDef, ComponentSubTokenDef } from '../spec-config.js'; - -// ── YAML code block helpers ───────────────────────────────────── - -/** Render a fenced yaml code block from key-value entries. */ -function yamlBlock(lines: string[]): string { - return ['```yaml', ...lines, '```'].join('\n'); -} - -function yamlEntries(entries: Record, indent = 2): string[] { - return Object.entries(entries).map( - ([k, v]) => `${' '.repeat(indent)}${k}: "${v}"` - ); -} - -function yamlObject(entries: Record, indent = 4): string[] { - return Object.entries(entries).map(([k, v]) => { - const val = typeof v === 'string' && v.startsWith('{') ? `"${v}"` : v; - return `${' '.repeat(indent)}${k}: ${val}`; - }); -} - -// ── Public renderers ──────────────────────────────────────────── - -/** Front matter example (overview section). */ -export function frontmatterExample(config: SpecConfig): string { - const [typoName, typoProps] = Object.entries(config.EXAMPLES.typography)[0]!; - return yamlBlock([ - '---', - `version: ${config.SPEC_VERSION}`, - 'name: Daylight Prestige', - 'colors:', - ...yamlEntries( - Object.fromEntries(Object.entries(config.EXAMPLES.colors).slice(0, 3)) as Record - ), - 'typography:', - ` ${typoName}:`, - ...yamlObject(typoProps as Record), - '---', - ]); -} - -/** Colors YAML example. */ -export function colorsExample(config: SpecConfig): string { - return yamlBlock(['colors:', ...yamlEntries(config.EXAMPLES.colors)]); -} - -/** Typography YAML example. */ -export function typographyExample(config: SpecConfig): string { - const lines = ['typography:']; - for (const [name, props] of Object.entries(config.EXAMPLES.typography)) { - lines.push(` ${name}:`); - lines.push(...yamlObject(props as Record)); - } - return yamlBlock(lines); -} - -/** Components YAML example. */ -export function componentsExample(config: SpecConfig): string { - const lines = ['components:']; - for (const [name, props] of Object.entries(config.EXAMPLES.components)) { - lines.push(` ${name}:`); - lines.push(...yamlObject(props as Record)); - } - return yamlBlock(lines); -} - -/** Typography property list (for the schema section). */ -export function typographyPropertyList(config: SpecConfig): string { - return config.TYPOGRAPHY_PROPERTIES.map((p: TypographyPropertyDef) => - p.description - ? `- \`${p.name}\` (${p.type}) - ${p.description}` - : `- \`${p.name}\` (${p.type})` - ).join('\n'); -} - -/** Numbered section order list with aliases. */ -export function sectionOrderList(config: SpecConfig): string { - return config.SECTIONS.map((s: SectionDef, i: number) => { - const aliases = s.aliases?.length - ? ` (also: ${s.aliases.map((a: string) => `"${a}"`).join(', ')})` - : ''; - return `${i + 1}. **${s.canonical}**${aliases}`; - }).join('\n'); -} - -/** Component sub-token property list. */ -export function componentSubTokenList(config: SpecConfig): string { - return config.COMPONENT_SUB_TOKENS - .map((t: ComponentSubTokenDef) => `- ${t.name}: \\<${t.type}\\>`) - .join('\n'); -} - -/** Recommended token names grouped by category. */ -export function recommendedTokens(config: SpecConfig): string { - return Object.entries(config.RECOMMENDED_TOKENS) - .map(([cat, tokens]) => { - const label = cat.charAt(0).toUpperCase() + cat.slice(1); - return `**${label}:** ${(tokens as readonly string[]).map((t: string) => `\`${t}\``).join(', ')}`; - }) - .join('\n\n'); -} +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * Spec renderers: pure functions that turn spec-config data into + * markdown fragments. Used by the MDX compiler via scope injection. + * + * Each function returns a ready-to-embed markdown string. + */ + +import type { SpecConfig, TypographyPropertyDef, SectionDef, ComponentSubTokenDef, TypeDef } from '../spec-config.js'; + +// ── YAML code block helpers ───────────────────────────────────── + +/** Render a fenced yaml code block from key-value entries. */ +function yamlBlock(lines: string[]): string { + return ['```yaml', ...lines, '```'].join('\n'); +} + +function yamlEntries(entries: Record, indent = 2): string[] { + return Object.entries(entries).map( + ([k, v]) => `${' '.repeat(indent)}${k}: "${v}"` + ); +} + +function yamlObject(entries: Record, indent = 4): string[] { + return Object.entries(entries).map(([k, v]) => { + const val = typeof v === 'string' && v.startsWith('{') ? `"${v}"` : v; + return `${' '.repeat(indent)}${k}: ${val}`; + }); +} + +// ── Public renderers ──────────────────────────────────────────── + +/** Front matter example (overview section). */ +export function frontmatterExample(config: SpecConfig): string { + const [typoName, typoProps] = Object.entries(config.EXAMPLES.typography)[0]!; + return yamlBlock([ + '---', + `version: ${config.SPEC_VERSION}`, + 'name: Daylight Prestige', + 'colors:', + ...yamlEntries( + Object.fromEntries(Object.entries(config.EXAMPLES.colors).slice(0, 3)) as Record + ), + 'typography:', + ` ${typoName}:`, + ...yamlObject(typoProps as Record), + '---', + ]); +} + +/** Colors YAML example. */ +export function colorsExample(config: SpecConfig): string { + return yamlBlock(['colors:', ...yamlEntries(config.EXAMPLES.colors)]); +} + +/** Typography YAML example. */ +export function typographyExample(config: SpecConfig): string { + const lines = ['typography:']; + for (const [name, props] of Object.entries(config.EXAMPLES.typography)) { + lines.push(` ${name}:`); + lines.push(...yamlObject(props as Record)); + } + return yamlBlock(lines); +} + +/** Components YAML example. */ +export function componentsExample(config: SpecConfig): string { + const lines = ['components:']; + for (const [name, props] of Object.entries(config.EXAMPLES.components)) { + lines.push(` ${name}:`); + lines.push(...yamlObject(props as Record)); + } + return yamlBlock(lines); +} + +/** Typography property list (for the schema section). */ +export function typographyPropertyList(config: SpecConfig): string { + return config.TYPOGRAPHY_PROPERTIES.map((p: TypographyPropertyDef) => + p.description + ? `- \`${p.name}\` (${p.type}) - ${p.description}` + : `- \`${p.name}\` (${p.type})` + ).join('\n'); +} + +/** Numbered section order list with aliases. */ +export function sectionOrderList(config: SpecConfig): string { + return config.SECTIONS.map((s: SectionDef, i: number) => { + const aliases = s.aliases?.length + ? ` (also: ${s.aliases.map((a: string) => `"${a}"`).join(', ')})` + : ''; + return `${i + 1}. **${s.canonical}**${aliases}`; + }).join('\n'); +} + +/** Component sub-token property list. */ +export function componentSubTokenList(config: SpecConfig): string { + return config.COMPONENT_SUB_TOKENS + .map((t: ComponentSubTokenDef) => `- ${t.name}: \\<${t.type}\\>`) + .join('\n'); +} + +/** Recommended token names grouped by category. */ +export function recommendedTokens(config: SpecConfig): string { + return Object.entries(config.RECOMMENDED_TOKENS) + .map(([cat, tokens]) => { + const label = cat.charAt(0).toUpperCase() + cat.slice(1); + return `**${label}:** ${(tokens as readonly string[]).map((t: string) => `\`${t}\``).join(', ')}`; + }) + .join('\n\n'); +} + +/** Primitive type definitions (Color, Dimension, etc.) for the schema section. */ +export function typeDefinitions(config: SpecConfig): string { + return Object.entries(config.PRIMITIVE_TYPES).map(([name, def]: [string, TypeDef]) => { + let block = `**${name}**: ${def.description}`; + if (def.formats?.length) { + block += '\n\n' + def.formats.map((f: string) => `- ${f}`).join('\n'); + } + if (name === 'Dimension') { + block += ` Valid units are: ${config.STANDARD_UNITS.join(', ')}.`; + } + if (def.note) { + block += '\n\n' + def.note.trim(); + } + return block; + }).join('\n\n'); +} From d186670ecf43a10e6e9613ec7ca3fdde70a327dd Mon Sep 17 00:00:00 2001 From: Emp1500 Date: Thu, 25 Jun 2026 11:33:13 +0000 Subject: [PATCH 5/7] feat: replace hardcoded Color/Dimension prose in spec.mdx with typeDefinitions() renderer --- docs/spec.md | 168 +++---- packages/cli/src/linter/spec-gen/spec.mdx | 566 +++++++++++----------- 2 files changed, 361 insertions(+), 373 deletions(-) diff --git a/docs/spec.md b/docs/spec.md index 41b6d085..5c94640a 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -9,7 +9,7 @@ A DESIGN.md file contains two parts: An optional YAML frontmatter, and a markdow # Design Tokens -DESIGN.md may embed design tokens in a structured format. The system that we use to describe design tokens is inspired by the +DESIGN.md may embed design tokens in a structured format. The system that we use to describe design tokens is inspired by the [Design Token JSON spec](https://www.designtokens.org/tr/2025.10/format/#abstract). Specifically, we adopt the concept of typed token groups (colors, typography, spacing) and the `{path.to.token}` reference syntax for cross-referencing values. These tokens are easily converted from or to `tokens.json`, Figma variables, and Tailwind theme configs. @@ -41,19 +41,19 @@ typography: Below is the schema for the design tokens defined in the front matter: ```yaml -version: # optional, current version: "alpha" -name: -description: # optional -colors: - : -typography: - : -rounded: - : -spacing: - : -components: - : +version: # optional, current version: "alpha" +name: +description: # optional +colors: + : +typography: + : +rounded: + : +spacing: + : +components: + : : ``` @@ -61,16 +61,18 @@ The `` placeholder represents a named level in a sizing or spacing **Color**: A color value is any valid CSS color string. Supported formats include: -* Hex: `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA` -* Named colors: `red`, `cornflowerblue`, `transparent` -* Functional: `rgb()`, `rgba()`, `hsl()`, `hsla()`, `hwb()` -* Wide-gamut: `oklch()`, `oklab()`, `lch()`, `lab()` -* Mixing: `color-mix(in srgb, ...)` +- Hex: `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA` +- Named colors: `red`, `cornflowerblue`, `transparent` +- Functional: `rgb()`, `rgba()`, `hsl()`, `hsla()`, `hwb()` +- Wide-gamut: `oklch()`, `oklab()`, `lch()`, `lab()` +- Mixing: `color-mix(in srgb, ...)` All color values are internally converted to sRGB for WCAG contrast checking. The original format is preserved for display and export. Hex notation (`#RRGGBB`) remains the recommended default for simplicity and broad tooling support. +**Dimension**: A dimension value is a string with a unit suffix. Valid units are: px, em, rem. + - `fontFamily` (string) - `fontSize` (Dimension) - `fontWeight` (number) - A numeric font weight value (e.g., `400`, `700`). In YAML, this may be expressed as either a bare number or a quoted string; both are equivalent. @@ -81,8 +83,6 @@ Hex notation (`#RRGGBB`) remains the recommended default for simplicity and broa - `fontVariation` (string) - configures [`font-variation-settings`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/font-variation-settings). -**Dimension**: A dimension value is a string with a unit suffix. Valid units are: px, em, rem. - **Token References**: A token reference must be wrapped in curly braces, and contain an object path to another value in the YAML tree. For most token groups, the reference must point to a primitive value (e.g., `colors.primary-60`), not a group (e.g., `colors`). Within the `components` section, references to composite values (e.g., `{typography.label-md}`) are permitted. # Sections @@ -119,17 +119,17 @@ When there are multiple color palettes, the design system may assign a semantic Example: ```markdown -## Colors - -The palette is rooted in high-contrast neutrals and a single, evocative accent color. - -- **Primary (#1A1C1E):** A deep ink used for headlines and core text to provide - maximum readability and a sense of permanence. -- **Secondary (#6C7278):** A sophisticated slate used primarily for utilitarian - elements like borders, captions, and metadata. -- **Tertiary (#B8422E):** A vibrant earthy red as the sole driver for - interaction, used exclusively for primary actions and critical highlights. -- **Neutral (#F7F5F2):** A warm limestone that serves as the foundation for all +## Colors + +The palette is rooted in high-contrast neutrals and a single, evocative accent color. + +- **Primary (#1A1C1E):** A deep ink used for headlines and core text to provide + maximum readability and a sense of permanence. +- **Secondary (#6C7278):** A sophisticated slate used primarily for utilitarian + elements like borders, captions, and metadata. +- **Tertiary (#B8422E):** A vibrant earthy red as the sole driver for + interaction, used exclusively for primary actions and critical highlights. +- **Neutral (#F7F5F2):** A warm limestone that serves as the foundation for all pages, providing a softer, more organic feel than pure white. ``` @@ -137,7 +137,7 @@ The palette is rooted in high-contrast neutrals and a single, evocative accent c The `colors` section defines all color design tokens. The color tokens should be derived from the key color palettes defined in the markdown prose. The exact mapping from color palettes to color tokens may follow any consistent naming convention. -It is a +It is a map\, that maps the name of the color token to its value. ```yaml @@ -159,17 +159,17 @@ A common naming convention for typography levels is to use semantic categories s Example: ```markdown -## Typography - -The typography strategy leverages two distinct weights of **Public Sans** for -the narrative and **Space Grotesk** for technical data. - -- **Headlines:** Set in Public Sans Semi-Bold to establish an institutional - and trustworthy voice. -- **Body:** Public Sans Regular at 16px ensures contemporary professionalism - and long-form readability. -- **Labels:** Space Grotesk is used for all technical data, timestamps, and - metadata. Its geometric construction evokes the precision of a digital +## Typography + +The typography strategy leverages two distinct weights of **Public Sans** for +the narrative and **Space Grotesk** for technical data. + +- **Headlines:** Set in Public Sans Semi-Bold to establish an institutional + and trustworthy voice. +- **Body:** Public Sans Regular at 16px ensures contemporary professionalism + and long-form readability. +- **Labels:** Space Grotesk is used for all technical data, timestamps, and + metadata. Its geometric construction evokes the precision of a digital stopwatch. Labels are strictly uppercase with generous letter spacing. ``` @@ -177,7 +177,7 @@ the narrative and **Space Grotesk** for technical data. The `typography` section defines the precise font properties for the typography design tokens. -It is a +It is a map\ ```yaml @@ -212,11 +212,11 @@ Many design systems follow a grid-based layout. Others, like Liquid Glass, use m Example: ```markdown -## Layout - -The layout follows a **Fluid Grid** model for mobile devices and a -**Fixed-Max-Width Grid** for desktop (max 1200px). - +## Layout + +The layout follows a **Fluid Grid** model for mobile devices and a +**Fixed-Max-Width Grid** for desktop (max 1200px). + A strict 8px spacing scale (with a 4px half-step for micro-adjustments) is used to maintain a consistent rhythm. Components are grouped using "containment" principles, where related items are housed in cards with generous internal padding (24px) to emphasize the soft, approachable nature of the brand. ``` @@ -224,18 +224,18 @@ A strict 8px spacing scale (with a 4px half-step for micro-adjustments) is used The spacing section defines the spacing design tokens. These may include spacing units that are useful for implementing the layout model. For example, a fixed grid layout may have spacing units for column spans, gutters, and margins. -It is a +It is a map\ that maps the spacing scale identifier to a dimension value or a unitless number (e.g., column counts or ratios). ```yaml -spacing: - base: 16px - xs: 4px - sm: 8px - md: 16px - lg: 32px - xl: 64px - gutter: 24px +spacing: + base: 16px + xs: 4px + sm: 8px + md: 16px + lg: 32px + xl: 64px + gutter: 24px margin: 32px ``` @@ -248,9 +248,9 @@ This section describes how visual hierarchy is conveyed based on the design styl Example: ```markdown -## Elevation & Depth - -Depth is achieved through **Tonal Layers** rather than heavy shadows. The +## Elevation & Depth + +Depth is achieved through **Tonal Layers** rather than heavy shadows. The background uses a soft off-white or very light green, while primary content sits on pure white cards. ``` @@ -261,26 +261,26 @@ This section describes how visual elements are shaped. Example: ```markdown -## Shapes - -The shape language is defined by **Architectural Sharpness**. All interactive -elements, containers, and inputs utilize a minimal **4px corner radius**. This -provides just enough softness to feel modern while maintaining a rigid, +## Shapes + +The shape language is defined by **Architectural Sharpness**. All interactive +elements, containers, and inputs utilize a minimal **4px corner radius**. This +provides just enough softness to feel modern while maintaining a rigid, engineered aesthetic. ``` ### Design Tokens -The `rounded` section defines the design tokens for rounded corners used in +The `rounded` section defines the design tokens for rounded corners used in buttons, cards, and other rectangular shapes. It is a map\. ```yaml -rounded: - sm: 4px - md: 8px - lg: 12px +rounded: + sm: 4px + md: 8px + lg: 12px full: 9999px ``` @@ -333,11 +333,11 @@ Each component has a set of properties that are themselves design tokens: This section provides practical guidelines and common pitfalls. These act as guardrails when creating designs. ```markdown -## Do's and Don'ts - -- Do use the primary color only for the single most important action per screen -- Don't mix rounded and sharp corners in the same view -- Do maintain WCAG AA contrast ratios (4.5:1 for normal text) +## Do's and Don'ts + +- Do use the primary color only for the single most important action per screen +- Don't mix rounded and sharp corners in the same view +- Do maintain WCAG AA contrast ratios (4.5:1 for normal text) - Don't use more than two font weights on a single screen ``` @@ -355,11 +355,11 @@ The following names are commonly used across design systems. They are not requir When a DESIGN.md consumer encounters content not defined by this spec: -| Scenario | Behavior | Example | -|---|---|---| -| Unknown section heading | Preserve; do not error | `## Iconography` | -| Unknown color token name | Accept if value is valid | `surface-container-high: '#ede7dd'` | -| Unknown typography token name | Accept as valid typography | `telemetry-data` | -| Unknown spacing value | Accept; store as string if not a valid dimension | `grid-columns: '5'` | -| Unknown component property | Accept with warning | `borderColor` | +| Scenario | Behavior | Example | +|---|---|---| +| Unknown section heading | Preserve; do not error | `## Iconography` | +| Unknown color token name | Accept if value is valid | `surface-container-high: '#ede7dd'` | +| Unknown typography token name | Accept as valid typography | `telemetry-data` | +| Unknown spacing value | Accept; store as string if not a valid dimension | `grid-columns: '5'` | +| Unknown component property | Accept with warning | `borderColor` | | Duplicate section heading | Error; reject the file | Two `## Colors` headings | diff --git a/packages/cli/src/linter/spec-gen/spec.mdx b/packages/cli/src/linter/spec-gen/spec.mdx index 44064d5c..3807a5ef 100644 --- a/packages/cli/src/linter/spec-gen/spec.mdx +++ b/packages/cli/src/linter/spec-gen/spec.mdx @@ -1,289 +1,277 @@ -import { SPEC_VERSION, STANDARD_UNITS, SECTIONS, TYPOGRAPHY_PROPERTIES, COMPONENT_SUB_TOKENS, CORE_COLOR_ROLES, RECOMMENDED_TOKENS, EXAMPLES } from '../spec-config.js' -import { frontmatterExample, colorsExample, typographyExample, componentsExample, typographyPropertyList, sectionOrderList, componentSubTokenList, recommendedTokens } from './renderers.js' - -# DESIGN.md Format - -DESIGN.md is a self-contained, plain-text representation of a design system. It defines the visual identity of a brand and product, thereby ensuring that these stylistic choices can be followed across design sessions and between different AI agents and tools. As a human-readable, open-format document, it serves as a living source of truth that both humans and AI can understand and refine. - -A DESIGN.md file contains two parts: An optional YAML frontmatter, and a markdown body. The YAML front matter contains machine-readable design tokens. The markdown body sections provide human-readable design rationale and guidance. Prose may use descriptive color names (e.g., "Midnight Forest Green") that correspond to systematic token names (e.g., `primary`). The tokens are the normative values; the prose provides context for how to apply them. - -# Design Tokens - -DESIGN.md may embed design tokens in a structured format. The system that we use to describe design tokens is inspired by the -[Design Token JSON spec](https://www.designtokens.org/tr/2025.10/format/#abstract). Specifically, we adopt the concept of typed token groups (colors, typography, spacing) and the `{path.to.token}` reference syntax for cross-referencing values. - -These tokens are easily converted from or to `tokens.json`, Figma variables, and Tailwind theme configs. - -Design tokens are embedded as YAML front matter at the beginning of the file. The front matter block must begin with a line containing exactly `---` and end with a line containing exactly `---`. The YAML content between these delimiters is parsed according to the schema defined below. - -Example: - -{frontmatterExample()} - -## Schema - -Below is the schema for the design tokens defined in the front matter: - -```yaml -version: # optional, current version: "alpha" -name: -description: # optional -colors: - : -typography: - : -rounded: - : -spacing: - : -components: - : - : -``` - -The `` placeholder represents a named level in a sizing or spacing scale. Common level names include `xs`, `sm`, `md`, `lg`, `xl`, and `full`. Any descriptive string key is valid. - -**Color**: A color value is any valid CSS color string. Supported formats include: - -- Hex: `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA` -- Named colors: `red`, `cornflowerblue`, `transparent` -- Functional: `rgb()`, `rgba()`, `hsl()`, `hsla()`, `hwb()` -- Wide-gamut: `oklch()`, `oklab()`, `lch()`, `lab()` -- Mixing: `color-mix(in srgb, ...)` - -All color values are internally converted to sRGB for WCAG contrast checking. The original format is preserved for display and export. - -Hex notation (`#RRGGBB`) remains the recommended default for simplicity and broad tooling support. - -{typographyPropertyList()} - -**Dimension**: A dimension value is a string with a unit suffix. Valid units are: {STANDARD_UNITS.join(', ')}. - -**Token References**: A token reference must be wrapped in curly braces, and contain an object path to another value in the YAML tree. For most token groups, the reference must point to a primitive value (e.g., `colors.primary-60`), not a group (e.g., `colors`). Within the `components` section, references to composite values (e.g., `{typography.label-md}`) are permitted. - -# Sections - -Every `DESIGN.md` follows the same structure. Sections can be omitted if they're not relevant to your project, but those present should appear in the sequence listed below. All sections use `

` (`##`) headings. An optional `

` heading may appear for document titling purposes but is not parsed as a section. - -### Section Order - -{sectionOrderList()} - -### Prose and Tokens - -## Overview - -Also known as "Brand & Style". - -This section is a holistic description of a product's look and feel. It defines the brand personality, target audience, and the emotional response the UI should evoke, such as whether it should feel playful or professional, dense or spacious. It serves as foundational context for guiding the agent's high-level stylistic decisions when a specific rule or token isn't explicitly defined. - -## Colors - -This section defines the color palettes for the design system. - -At least the `primary` color palette must be defined, and additional color palettes may be defined as needed. - -When there are multiple color palettes, the design system may assign a semantic role for each palette. A common convention is to name the palettes in this order: `primary`, `secondary`, `tertiary`, and `neutral`. - -Example: - -```markdown -## Colors - -The palette is rooted in high-contrast neutrals and a single, evocative accent color. - -- **Primary (#1A1C1E):** A deep ink used for headlines and core text to provide - maximum readability and a sense of permanence. -- **Secondary (#6C7278):** A sophisticated slate used primarily for utilitarian - elements like borders, captions, and metadata. -- **Tertiary (#B8422E):** A vibrant earthy red as the sole driver for - interaction, used exclusively for primary actions and critical highlights. -- **Neutral (#F7F5F2):** A warm limestone that serves as the foundation for all - pages, providing a softer, more organic feel than pure white. -``` - -### Design Tokens - -The `colors` section defines all color design tokens. The color tokens should be derived from the key color palettes defined in the markdown prose. The exact mapping from color palettes to color tokens may follow any consistent naming convention. - -It is a -map\, that maps the name of the color token to its value. - -{colorsExample()} - -## Typography - -This section defines typography levels. - -Most design systems have 9 - 15 typography levels. The design system may prescribe a role for each typography level. - -A common naming convention for typography levels is to use semantic categories such as `headline`, `display`, `body`, `label`, `caption`. Each category may further be divided into different sizes, such as `small`, `medium`, and `large`. - -Example: - -```markdown -## Typography - -The typography strategy leverages two distinct weights of **Public Sans** for -the narrative and **Space Grotesk** for technical data. - -- **Headlines:** Set in Public Sans Semi-Bold to establish an institutional - and trustworthy voice. -- **Body:** Public Sans Regular at 16px ensures contemporary professionalism - and long-form readability. -- **Labels:** Space Grotesk is used for all technical data, timestamps, and - metadata. Its geometric construction evokes the precision of a digital - stopwatch. Labels are strictly uppercase with generous letter spacing. -``` - -### Design Tokens - -The `typography` section defines the precise font properties for the typography design tokens. - -It is a -map\ - -{typographyExample()} - -## Layout - -Also known as "Layout & Spacing". - -This section describes the layout and spacing strategy. - -Many design systems follow a grid-based layout. Others, like Liquid Glass, use margins, safe areas, and dynamic padding. - -Example: - -```markdown -## Layout - -The layout follows a **Fluid Grid** model for mobile devices and a -**Fixed-Max-Width Grid** for desktop (max 1200px). - -A strict 8px spacing scale (with a 4px half-step for micro-adjustments) is used to maintain a consistent rhythm. Components are grouped using "containment" principles, where related items are housed in cards with generous internal padding (24px) to emphasize the soft, approachable nature of the brand. -``` - -### Design Tokens - -The spacing section defines the spacing design tokens. These may include spacing units that are useful for implementing the layout model. For example, a fixed grid layout may have spacing units for column spans, gutters, and margins. - -It is a -map\ that maps the spacing scale identifier to a dimension value or a unitless number (e.g., column counts or ratios). - -```yaml -spacing: - base: 16px - xs: 4px - sm: 8px - md: 16px - lg: 32px - xl: 64px - gutter: 24px - margin: 32px -``` - -## Elevation & Depth - -Also known as "Elevation". - -This section describes how visual hierarchy is conveyed based on the design style. If elevation is used, it defines the required styling (spread, blur, color). For flat designs, this section explains the alternative methods used to convey visual hierarchy (e.g., borders, color contrast). - -Example: - -```markdown -## Elevation & Depth - -Depth is achieved through **Tonal Layers** rather than heavy shadows. The -background uses a soft off-white or very light green, while primary content sits on pure white cards. -``` - -## Shapes - -This section describes how visual elements are shaped. - -Example: - -```markdown -## Shapes - -The shape language is defined by **Architectural Sharpness**. All interactive -elements, containers, and inputs utilize a minimal **4px corner radius**. This -provides just enough softness to feel modern while maintaining a rigid, -engineered aesthetic. -``` - -### Design Tokens - -The `rounded` section defines the design tokens for rounded corners used in -buttons, cards, and other rectangular shapes. - -It is a map\. - -```yaml -rounded: - sm: 4px - md: 8px - lg: 12px - full: 9999px -``` - -## Components - -This section provides style guidance for component atoms within the design system. The following are common component types. Design systems are encouraged to define additional components relevant to their domain. - -- **Buttons**: Covers primary, secondary, and tertiary variants, including sizing, padding, and states. -- **Chips**: Covers selection chips, filter chips, and action chips. -- **Lists**: Covers styling for list items, dividers, and leading/trailing elements. -- **Tooltips**: Covers positioning, colors, and timing. -- **Checkboxes**: Covers checked, unchecked, and indeterminate states. -- **Radio buttons**: Covers selected and unselected states. -- **Input fields**: Covers text inputs, text areas, labels, helper text, and error states. - -> **Note:** The components specification is actively evolving. The current structure provides intentional flexibility for domain-specific component definitions while the spec matures. - -### Design Tokens - -The components section defines a collection of design tokens used to ensure consistent styling of common components. It's a map\\> that maps a component identifier to a group of sub token names and values. The design token values may be literal values, or references to previously defined design tokens. - -**Variants**. A component may have a variant for different UI states such as active, hover, pressed, etc. Those variant components may be defined under a different but related key, for example, "button-primary", "button-primary-hover", "button-primary-active". The agent will consider all variants and make the appropriate styling decisions. - -{componentsExample()} - -### Component Property Tokens - -Each component has a set of properties that are themselves design tokens: - -{componentSubTokenList()} - -## Do's and Don'ts - -This section provides practical guidelines and common pitfalls. These act as guardrails when creating designs. - -```markdown -## Do's and Don'ts - -- Do use the primary color only for the single most important action per screen -- Don't mix rounded and sharp corners in the same view -- Do maintain WCAG AA contrast ratios (4.5:1 for normal text) -- Don't use more than two font weights on a single screen -``` - -# Recommended Token Names (Non-Normative) - -The following names are commonly used across design systems. They are not required but are provided as guidance for consistency. - -{recommendedTokens()} - -# Consumer Behavior for Unknown Content - -When a DESIGN.md consumer encounters content not defined by this spec: - -| Scenario | Behavior | Example | -|---|---|---| -| Unknown section heading | Preserve; do not error | `## Iconography` | -| Unknown color token name | Accept if value is valid | `surface-container-high: '#ede7dd'` | -| Unknown typography token name | Accept as valid typography | `telemetry-data` | -| Unknown spacing value | Accept; store as string if not a valid dimension | `grid-columns: '5'` | -| Unknown component property | Accept with warning | `borderColor` | -| Duplicate section heading | Error; reject the file | Two `## Colors` headings | +import { SPEC_VERSION, SECTIONS, TYPOGRAPHY_PROPERTIES, COMPONENT_SUB_TOKENS, CORE_COLOR_ROLES, RECOMMENDED_TOKENS, EXAMPLES } from '../spec-config.js' +import { frontmatterExample, colorsExample, typographyExample, componentsExample, typographyPropertyList, sectionOrderList, componentSubTokenList, recommendedTokens, typeDefinitions } from './renderers.js' + +# DESIGN.md Format + +DESIGN.md is a self-contained, plain-text representation of a design system. It defines the visual identity of a brand and product, thereby ensuring that these stylistic choices can be followed across design sessions and between different AI agents and tools. As a human-readable, open-format document, it serves as a living source of truth that both humans and AI can understand and refine. + +A DESIGN.md file contains two parts: An optional YAML frontmatter, and a markdown body. The YAML front matter contains machine-readable design tokens. The markdown body sections provide human-readable design rationale and guidance. Prose may use descriptive color names (e.g., "Midnight Forest Green") that correspond to systematic token names (e.g., `primary`). The tokens are the normative values; the prose provides context for how to apply them. + +# Design Tokens + +DESIGN.md may embed design tokens in a structured format. The system that we use to describe design tokens is inspired by the +[Design Token JSON spec](https://www.designtokens.org/tr/2025.10/format/#abstract). Specifically, we adopt the concept of typed token groups (colors, typography, spacing) and the `{path.to.token}` reference syntax for cross-referencing values. + +These tokens are easily converted from or to `tokens.json`, Figma variables, and Tailwind theme configs. + +Design tokens are embedded as YAML front matter at the beginning of the file. The front matter block must begin with a line containing exactly `---` and end with a line containing exactly `---`. The YAML content between these delimiters is parsed according to the schema defined below. + +Example: + +{frontmatterExample()} + +## Schema + +Below is the schema for the design tokens defined in the front matter: + +```yaml +version: # optional, current version: "alpha" +name: +description: # optional +colors: + : +typography: + : +rounded: + : +spacing: + : +components: + : + : +``` + +The `` placeholder represents a named level in a sizing or spacing scale. Common level names include `xs`, `sm`, `md`, `lg`, `xl`, and `full`. Any descriptive string key is valid. + +{typeDefinitions()} + +{typographyPropertyList()} + +**Token References**: A token reference must be wrapped in curly braces, and contain an object path to another value in the YAML tree. For most token groups, the reference must point to a primitive value (e.g., `colors.primary-60`), not a group (e.g., `colors`). Within the `components` section, references to composite values (e.g., `{typography.label-md}`) are permitted. + +# Sections + +Every `DESIGN.md` follows the same structure. Sections can be omitted if they're not relevant to your project, but those present should appear in the sequence listed below. All sections use `

` (`##`) headings. An optional `

` heading may appear for document titling purposes but is not parsed as a section. + +### Section Order + +{sectionOrderList()} + +### Prose and Tokens + +## Overview + +Also known as "Brand & Style". + +This section is a holistic description of a product's look and feel. It defines the brand personality, target audience, and the emotional response the UI should evoke, such as whether it should feel playful or professional, dense or spacious. It serves as foundational context for guiding the agent's high-level stylistic decisions when a specific rule or token isn't explicitly defined. + +## Colors + +This section defines the color palettes for the design system. + +At least the `primary` color palette must be defined, and additional color palettes may be defined as needed. + +When there are multiple color palettes, the design system may assign a semantic role for each palette. A common convention is to name the palettes in this order: `primary`, `secondary`, `tertiary`, and `neutral`. + +Example: + +```markdown +## Colors + +The palette is rooted in high-contrast neutrals and a single, evocative accent color. + +- **Primary (#1A1C1E):** A deep ink used for headlines and core text to provide + maximum readability and a sense of permanence. +- **Secondary (#6C7278):** A sophisticated slate used primarily for utilitarian + elements like borders, captions, and metadata. +- **Tertiary (#B8422E):** A vibrant earthy red as the sole driver for + interaction, used exclusively for primary actions and critical highlights. +- **Neutral (#F7F5F2):** A warm limestone that serves as the foundation for all + pages, providing a softer, more organic feel than pure white. +``` + +### Design Tokens + +The `colors` section defines all color design tokens. The color tokens should be derived from the key color palettes defined in the markdown prose. The exact mapping from color palettes to color tokens may follow any consistent naming convention. + +It is a +map\, that maps the name of the color token to its value. + +{colorsExample()} + +## Typography + +This section defines typography levels. + +Most design systems have 9 - 15 typography levels. The design system may prescribe a role for each typography level. + +A common naming convention for typography levels is to use semantic categories such as `headline`, `display`, `body`, `label`, `caption`. Each category may further be divided into different sizes, such as `small`, `medium`, and `large`. + +Example: + +```markdown +## Typography + +The typography strategy leverages two distinct weights of **Public Sans** for +the narrative and **Space Grotesk** for technical data. + +- **Headlines:** Set in Public Sans Semi-Bold to establish an institutional + and trustworthy voice. +- **Body:** Public Sans Regular at 16px ensures contemporary professionalism + and long-form readability. +- **Labels:** Space Grotesk is used for all technical data, timestamps, and + metadata. Its geometric construction evokes the precision of a digital + stopwatch. Labels are strictly uppercase with generous letter spacing. +``` + +### Design Tokens + +The `typography` section defines the precise font properties for the typography design tokens. + +It is a +map\ + +{typographyExample()} + +## Layout + +Also known as "Layout & Spacing". + +This section describes the layout and spacing strategy. + +Many design systems follow a grid-based layout. Others, like Liquid Glass, use margins, safe areas, and dynamic padding. + +Example: + +```markdown +## Layout + +The layout follows a **Fluid Grid** model for mobile devices and a +**Fixed-Max-Width Grid** for desktop (max 1200px). + +A strict 8px spacing scale (with a 4px half-step for micro-adjustments) is used to maintain a consistent rhythm. Components are grouped using "containment" principles, where related items are housed in cards with generous internal padding (24px) to emphasize the soft, approachable nature of the brand. +``` + +### Design Tokens + +The spacing section defines the spacing design tokens. These may include spacing units that are useful for implementing the layout model. For example, a fixed grid layout may have spacing units for column spans, gutters, and margins. + +It is a +map\ that maps the spacing scale identifier to a dimension value or a unitless number (e.g., column counts or ratios). + +```yaml +spacing: + base: 16px + xs: 4px + sm: 8px + md: 16px + lg: 32px + xl: 64px + gutter: 24px + margin: 32px +``` + +## Elevation & Depth + +Also known as "Elevation". + +This section describes how visual hierarchy is conveyed based on the design style. If elevation is used, it defines the required styling (spread, blur, color). For flat designs, this section explains the alternative methods used to convey visual hierarchy (e.g., borders, color contrast). + +Example: + +```markdown +## Elevation & Depth + +Depth is achieved through **Tonal Layers** rather than heavy shadows. The +background uses a soft off-white or very light green, while primary content sits on pure white cards. +``` + +## Shapes + +This section describes how visual elements are shaped. + +Example: + +```markdown +## Shapes + +The shape language is defined by **Architectural Sharpness**. All interactive +elements, containers, and inputs utilize a minimal **4px corner radius**. This +provides just enough softness to feel modern while maintaining a rigid, +engineered aesthetic. +``` + +### Design Tokens + +The `rounded` section defines the design tokens for rounded corners used in +buttons, cards, and other rectangular shapes. + +It is a map\. + +```yaml +rounded: + sm: 4px + md: 8px + lg: 12px + full: 9999px +``` + +## Components + +This section provides style guidance for component atoms within the design system. The following are common component types. Design systems are encouraged to define additional components relevant to their domain. + +- **Buttons**: Covers primary, secondary, and tertiary variants, including sizing, padding, and states. +- **Chips**: Covers selection chips, filter chips, and action chips. +- **Lists**: Covers styling for list items, dividers, and leading/trailing elements. +- **Tooltips**: Covers positioning, colors, and timing. +- **Checkboxes**: Covers checked, unchecked, and indeterminate states. +- **Radio buttons**: Covers selected and unselected states. +- **Input fields**: Covers text inputs, text areas, labels, helper text, and error states. + +> **Note:** The components specification is actively evolving. The current structure provides intentional flexibility for domain-specific component definitions while the spec matures. + +### Design Tokens + +The components section defines a collection of design tokens used to ensure consistent styling of common components. It's a map\\> that maps a component identifier to a group of sub token names and values. The design token values may be literal values, or references to previously defined design tokens. + +**Variants**. A component may have a variant for different UI states such as active, hover, pressed, etc. Those variant components may be defined under a different but related key, for example, "button-primary", "button-primary-hover", "button-primary-active". The agent will consider all variants and make the appropriate styling decisions. + +{componentsExample()} + +### Component Property Tokens + +Each component has a set of properties that are themselves design tokens: + +{componentSubTokenList()} + +## Do's and Don'ts + +This section provides practical guidelines and common pitfalls. These act as guardrails when creating designs. + +```markdown +## Do's and Don'ts + +- Do use the primary color only for the single most important action per screen +- Don't mix rounded and sharp corners in the same view +- Do maintain WCAG AA contrast ratios (4.5:1 for normal text) +- Don't use more than two font weights on a single screen +``` + +# Recommended Token Names (Non-Normative) + +The following names are commonly used across design systems. They are not required but are provided as guidance for consistency. + +{recommendedTokens()} + +# Consumer Behavior for Unknown Content + +When a DESIGN.md consumer encounters content not defined by this spec: + +| Scenario | Behavior | Example | +|---|---|---| +| Unknown section heading | Preserve; do not error | `## Iconography` | +| Unknown color token name | Accept if value is valid | `surface-container-high: '#ede7dd'` | +| Unknown typography token name | Accept as valid typography | `telemetry-data` | +| Unknown spacing value | Accept; store as string if not a valid dimension | `grid-columns: '5'` | +| Unknown component property | Accept with warning | `borderColor` | +| Duplicate section heading | Error; reject the file | Two `## Colors` headings | From e94a39356f025b546886cc8a82626dcd3c8d351f Mon Sep 17 00:00:00 2001 From: Emp1500 Date: Thu, 25 Jun 2026 11:49:02 +0000 Subject: [PATCH 6/7] fix: require types in schema, restore Dimension position after typography properties --- docs/spec.md | 4 ++-- packages/cli/src/linter/spec-config.test.ts | 1 + packages/cli/src/linter/spec-config.ts | 2 +- packages/cli/src/linter/spec-gen/compiler.test.ts | 2 +- packages/cli/src/linter/spec-gen/generate.ts | 2 +- .../cli/src/linter/spec-gen/renderers.test.ts | 15 +++++++++++++++ packages/cli/src/linter/spec-gen/renderers.ts | 7 +++++-- packages/cli/src/linter/spec-gen/spec.mdx | 4 +++- 8 files changed, 29 insertions(+), 8 deletions(-) diff --git a/docs/spec.md b/docs/spec.md index 5c94640a..719d97b2 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -71,8 +71,6 @@ All color values are internally converted to sRGB for WCAG contrast checking. Th Hex notation (`#RRGGBB`) remains the recommended default for simplicity and broad tooling support. -**Dimension**: A dimension value is a string with a unit suffix. Valid units are: px, em, rem. - - `fontFamily` (string) - `fontSize` (Dimension) - `fontWeight` (number) - A numeric font weight value (e.g., `400`, `700`). In YAML, this may be expressed as either a bare number or a quoted string; both are equivalent. @@ -83,6 +81,8 @@ Hex notation (`#RRGGBB`) remains the recommended default for simplicity and broa - `fontVariation` (string) - configures [`font-variation-settings`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/font-variation-settings). +**Dimension**: A dimension value is a string with a unit suffix. Valid units are: px, em, rem. + **Token References**: A token reference must be wrapped in curly braces, and contain an object path to another value in the YAML tree. For most token groups, the reference must point to a primitive value (e.g., `colors.primary-60`), not a group (e.g., `colors`). Within the `components` section, references to composite values (e.g., `{typography.label-md}`) are permitted. # Sections diff --git a/packages/cli/src/linter/spec-config.test.ts b/packages/cli/src/linter/spec-config.test.ts index eb33682b..a54d6ff8 100644 --- a/packages/cli/src/linter/spec-config.test.ts +++ b/packages/cli/src/linter/spec-config.test.ts @@ -68,6 +68,7 @@ describe('spec-config loader', () => { 'typography_properties: [{name: x, type: y}]', 'component_sub_tokens: [{name: x, type: y}]', 'color_roles: [primary]', + 'types: {Color: {description: x}}', 'recommended_tokens: {a: [b]}', 'examples:', ' colors: {a: "#000"}', diff --git a/packages/cli/src/linter/spec-config.ts b/packages/cli/src/linter/spec-config.ts index 6f00c1aa..76b7e2b7 100644 --- a/packages/cli/src/linter/spec-config.ts +++ b/packages/cli/src/linter/spec-config.ts @@ -58,7 +58,7 @@ const ConfigSchema = z.object({ typography_properties: z.array(PropertyDefSchema).min(1), component_sub_tokens: z.array(PropertyDefSchema).min(1), color_roles: z.array(z.string()).min(1), - types: z.record(z.string(), TypeDefSchema).optional().default({}), + types: z.record(z.string(), TypeDefSchema), recommended_tokens: z.record(z.string(), z.array(z.string())), examples: z.object({ colors: z.record(z.string(), z.string()), diff --git a/packages/cli/src/linter/spec-gen/compiler.test.ts b/packages/cli/src/linter/spec-gen/compiler.test.ts index 60e90ab5..e55a2a17 100644 --- a/packages/cli/src/linter/spec-gen/compiler.test.ts +++ b/packages/cli/src/linter/spec-gen/compiler.test.ts @@ -73,7 +73,7 @@ describe('compileMdx', () => { sectionOrderList: () => renderers.sectionOrderList(cfg), componentSubTokenList: () => renderers.componentSubTokenList(cfg), recommendedTokens: () => renderers.recommendedTokens(cfg), - typeDefinitions: () => renderers.typeDefinitions(cfg), + typeDefinitions: (typeName?: string) => renderers.typeDefinitions(cfg, typeName), }; const result = await compileMdx(source, scope); diff --git a/packages/cli/src/linter/spec-gen/generate.ts b/packages/cli/src/linter/spec-gen/generate.ts index a5f49634..0bb9a8c6 100644 --- a/packages/cli/src/linter/spec-gen/generate.ts +++ b/packages/cli/src/linter/spec-gen/generate.ts @@ -49,7 +49,7 @@ async function main() { sectionOrderList: () => renderers.sectionOrderList(cfg), componentSubTokenList: () => renderers.componentSubTokenList(cfg), recommendedTokens: () => renderers.recommendedTokens(cfg), - typeDefinitions: () => renderers.typeDefinitions(cfg), + typeDefinitions: (typeName?: string) => renderers.typeDefinitions(cfg, typeName), }; const generated = await compileMdx(source, scope); diff --git a/packages/cli/src/linter/spec-gen/renderers.test.ts b/packages/cli/src/linter/spec-gen/renderers.test.ts index 8fc26b96..d103c7ca 100644 --- a/packages/cli/src/linter/spec-gen/renderers.test.ts +++ b/packages/cli/src/linter/spec-gen/renderers.test.ts @@ -57,4 +57,19 @@ describe('typeDefinitions', () => { expect(result).toContain('internally converted to sRGB'); expect(result).toContain('recommended default'); }); + + it('filters to a single type when typeName is provided', () => { + const colorOnly = typeDefinitions(SPEC_CONFIG, 'Color'); + expect(colorOnly).toContain('**Color**'); + expect(colorOnly).not.toContain('**Dimension**'); + + const dimensionOnly = typeDefinitions(SPEC_CONFIG, 'Dimension'); + expect(dimensionOnly).toContain('**Dimension**'); + expect(dimensionOnly).not.toContain('**Color**'); + }); + + it('returns empty string for unknown typeName', () => { + const result = typeDefinitions(SPEC_CONFIG, 'NonExistent'); + expect(result).toBe(''); + }); }); diff --git a/packages/cli/src/linter/spec-gen/renderers.ts b/packages/cli/src/linter/spec-gen/renderers.ts index fd5ccfd2..c12c4bc0 100644 --- a/packages/cli/src/linter/spec-gen/renderers.ts +++ b/packages/cli/src/linter/spec-gen/renderers.ts @@ -123,8 +123,11 @@ export function recommendedTokens(config: SpecConfig): string { } /** Primitive type definitions (Color, Dimension, etc.) for the schema section. */ -export function typeDefinitions(config: SpecConfig): string { - return Object.entries(config.PRIMITIVE_TYPES).map(([name, def]: [string, TypeDef]) => { +export function typeDefinitions(config: SpecConfig, typeName?: string): string { + const entries = typeName + ? Object.entries(config.PRIMITIVE_TYPES).filter(([n]) => n === typeName) + : Object.entries(config.PRIMITIVE_TYPES); + return entries.map(([name, def]: [string, TypeDef]) => { let block = `**${name}**: ${def.description}`; if (def.formats?.length) { block += '\n\n' + def.formats.map((f: string) => `- ${f}`).join('\n'); diff --git a/packages/cli/src/linter/spec-gen/spec.mdx b/packages/cli/src/linter/spec-gen/spec.mdx index 3807a5ef..78a828db 100644 --- a/packages/cli/src/linter/spec-gen/spec.mdx +++ b/packages/cli/src/linter/spec-gen/spec.mdx @@ -43,10 +43,12 @@ components: The `` placeholder represents a named level in a sizing or spacing scale. Common level names include `xs`, `sm`, `md`, `lg`, `xl`, and `full`. Any descriptive string key is valid. -{typeDefinitions()} +{typeDefinitions('Color')} {typographyPropertyList()} +{typeDefinitions('Dimension')} + **Token References**: A token reference must be wrapped in curly braces, and contain an object path to another value in the YAML tree. For most token groups, the reference must point to a primitive value (e.g., `colors.primary-60`), not a group (e.g., `colors`). Within the `components` section, references to composite values (e.g., `{typography.label-md}`) are permitted. # Sections From 128f75849c44e450360437b3ef2f4cee4fbca6d2 Mon Sep 17 00:00:00 2001 From: Emp1500 Date: Thu, 25 Jun 2026 11:58:05 +0000 Subject: [PATCH 7/7] chore: remove planning artifacts not intended for upstream --- .../2026-06-25-spec-config-color-type.md | 552 ------------------ ...026-06-25-spec-config-color-type-design.md | 173 ------ 2 files changed, 725 deletions(-) delete mode 100644 docs/superpowers/plans/2026-06-25-spec-config-color-type.md delete mode 100644 docs/superpowers/specs/2026-06-25-spec-config-color-type-design.md diff --git a/docs/superpowers/plans/2026-06-25-spec-config-color-type.md b/docs/superpowers/plans/2026-06-25-spec-config-color-type.md deleted file mode 100644 index 592332f9..00000000 --- a/docs/superpowers/plans/2026-06-25-spec-config-color-type.md +++ /dev/null @@ -1,552 +0,0 @@ -# Spec-Config Primitive Type Definitions Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Move the Color and Dimension type definitions from hardcoded prose in `spec.mdx` into a data-driven `types:` section in `spec-config.yaml`, rendered dynamically the same way typography properties already are. - -**Architecture:** Add a `types` map to the YAML config; extend the Zod schema and TypeScript exports in `spec-config.ts` to parse and expose it; add a `typeDefinitions()` pure renderer in `renderers.ts`; replace two hardcoded blocks in `spec.mdx` with a single `{typeDefinitions()}` call; wire the renderer into `generate.ts`; regenerate `docs/spec.md`. - -**Tech Stack:** TypeScript, Bun (test runner + script runner), Zod (schema validation), MDX (spec template), remark (markdown processing) - -## Global Constraints - -- All test files use `bun:test` (`import { describe, it, expect } from 'bun:test'`) -- Run tests from `packages/cli/` with `bun test` -- Run spec generation from `packages/cli/` with `bun run spec:gen` -- Check spec is up to date with `bun run spec:gen --check` -- No new runtime dependencies -- Follow existing Apache 2.0 license header in every new file -- `Dimension` units are NOT duplicated in `types.Dimension` — the renderer reads them from `STANDARD_UNITS` - ---- - -## File Map - -| File | Action | What changes | -|---|---|---| -| `packages/cli/src/linter/spec-config.yaml` | Modify | Add `types:` section with Color and Dimension | -| `packages/cli/src/linter/spec-config.ts` | Modify | Add `TypeDefSchema`, `TypeDef` interface, `PRIMITIVE_TYPES` export, extend `SpecConfig` | -| `packages/cli/src/linter/spec-config.test.ts` | Modify | Add structural invariant tests for `PRIMITIVE_TYPES` | -| `packages/cli/src/linter/spec-gen/renderers.ts` | Modify | Add `typeDefinitions()` function | -| `packages/cli/src/linter/spec-gen/renderers.test.ts` | Create | Unit tests for `typeDefinitions()` output | -| `packages/cli/src/linter/spec-gen/generate.ts` | Modify | Bind `typeDefinitions` in MDX scope | -| `packages/cli/src/linter/spec-gen/compiler.test.ts` | Modify | Add `typeDefinitions` to the full spec.mdx compile test | -| `packages/cli/src/linter/spec-gen/spec.mdx` | Modify | Remove hardcoded Color+Dimension prose; add `{typeDefinitions()}` call | -| `docs/spec.md` | Regenerate | Output of `bun run spec:gen` | - ---- - -## Task 1: Schema — add `types` to spec-config.yaml and spec-config.ts - -**Files:** -- Modify: `packages/cli/src/linter/spec-config.yaml` -- Modify: `packages/cli/src/linter/spec-config.ts` -- Modify: `packages/cli/src/linter/spec-config.test.ts` - -**Interfaces:** -- Produces: - - `TypeDef` interface: `{ description: string; formats?: readonly string[]; note?: string }` - - `PRIMITIVE_TYPES: Record` — exported constant - - `SpecConfig.PRIMITIVE_TYPES` — added to the aggregate type - - `SPEC_CONFIG.PRIMITIVE_TYPES` — added to the bundle object - ---- - -- [ ] **Step 1: Write the failing tests in spec-config.test.ts** - -Open `packages/cli/src/linter/spec-config.test.ts`. Add `PRIMITIVE_TYPES` to the import at the top of the file: - -```ts -import { - loadSpecConfig, - getSpecConfig, - STANDARD_UNITS, - SECTIONS, - TYPOGRAPHY_PROPERTIES, - COMPONENT_SUB_TOKENS, - CORE_COLOR_ROLES, - RECOMMENDED_TOKENS, - EXAMPLES, - CANONICAL_ORDER, - SECTION_ALIASES, - resolveAlias, - VALID_TYPOGRAPHY_PROPS, - VALID_COMPONENT_SUB_TOKENS, - PRIMITIVE_TYPES, -} from './spec-config.js'; -``` - -Then append this new `describe` block at the end of the file (before the final blank line): - -```ts -describe('spec-config PRIMITIVE_TYPES', () => { - it('contains Color and Dimension entries', () => { - expect(PRIMITIVE_TYPES).toHaveProperty('Color'); - expect(PRIMITIVE_TYPES).toHaveProperty('Dimension'); - }); - - it('every type has a non-empty description', () => { - for (const [, def] of Object.entries(PRIMITIVE_TYPES)) { - expect(def.description.length).toBeGreaterThan(0); - } - }); - - it('Color has at least one format entry', () => { - expect(PRIMITIVE_TYPES['Color']!.formats?.length).toBeGreaterThan(0); - }); - - it('Dimension has no formats list (units come from STANDARD_UNITS)', () => { - expect(PRIMITIVE_TYPES['Dimension']!.formats).toBeUndefined(); - }); -}); -``` - -- [ ] **Step 2: Run tests to confirm they fail** - -```bash -cd packages/cli && bun test src/linter/spec-config.test.ts -``` - -Expected: 4 new tests FAIL with something like `PRIMITIVE_TYPES is not exported` or `Cannot read properties of undefined`. - -- [ ] **Step 3: Add `types:` section to spec-config.yaml** - -Open `packages/cli/src/linter/spec-config.yaml`. Insert the following block **after the `units:` list and before the `sections:` key** (after line 30, before line 32): - -```yaml -types: - Color: - description: "A color value is any valid CSS color string. Supported formats include:" - formats: - - "Hex: `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`" - - "Named colors: `red`, `cornflowerblue`, `transparent`" - - "Functional: `rgb()`, `rgba()`, `hsl()`, `hsla()`, `hwb()`" - - "Wide-gamut: `oklch()`, `oklab()`, `lch()`, `lab()`" - - "Mixing: `color-mix(in srgb, ...)`" - note: | - All color values are internally converted to sRGB for WCAG contrast checking. The original format is preserved for display and export. - - Hex notation (`#RRGGBB`) remains the recommended default for simplicity and broad tooling support. - Dimension: - description: "A dimension value is a string with a unit suffix." -``` - -- [ ] **Step 4: Update spec-config.ts — add TypeDefSchema** - -Open `packages/cli/src/linter/spec-config.ts`. After the `PropertyDefSchema` definition (around line 38), add: - -```ts -const TypeDefSchema = z.object({ - description: z.string(), - formats: z.array(z.string()).optional(), - note: z.string().optional(), -}); -``` - -- [ ] **Step 5: Add `types` to ConfigSchema** - -In the same file, inside `ConfigSchema`, add a `types` field after the `color_roles` line: - -```ts - color_roles: z.array(z.string()).min(1), - types: z.record(z.string(), TypeDefSchema).optional().default({}), - recommended_tokens: z.record(z.string(), z.array(z.string())), -``` - -- [ ] **Step 6: Add TypeDef interface** - -In the `// ── Interfaces ───` section (around line 89), add after `ComponentSubTokenDef`: - -```ts -export interface TypeDef { - /** Human-readable description of the type. */ - description: string; - /** List of accepted format strings, rendered as bullets. */ - formats?: readonly string[]; - /** Follow-up note rendered as a separate paragraph. May contain \n\n for multiple paragraphs. */ - note?: string; -} -``` - -- [ ] **Step 7: Export PRIMITIVE_TYPES constant** - -In the `// ── Constant exports ─────` section, after the `EXAMPLES` export (around line 145), add: - -```ts -/** Primitive type definitions (Color, Dimension, etc.) for the spec document. */ -export const PRIMITIVE_TYPES: Record = config.types; -``` - -- [ ] **Step 8: Extend SpecConfig interface and SPEC_CONFIG bundle** - -In the `SpecConfig` interface (around line 173), add: - -```ts - PRIMITIVE_TYPES: typeof PRIMITIVE_TYPES; -``` - -In the `SPEC_CONFIG` object (around line 187), add: - -```ts - PRIMITIVE_TYPES, -``` - -- [ ] **Step 9: Run tests to confirm they pass** - -```bash -cd packages/cli && bun test src/linter/spec-config.test.ts -``` - -Expected: All tests PASS, including the 4 new `spec-config PRIMITIVE_TYPES` tests. - -- [ ] **Step 10: Commit** - -```bash -git add packages/cli/src/linter/spec-config.yaml packages/cli/src/linter/spec-config.ts packages/cli/src/linter/spec-config.test.ts -git commit -m "feat: add types section to spec-config for Color and Dimension definitions" -``` - ---- - -## Task 2: Renderer — add `typeDefinitions()` and wire into generate.ts - -**Files:** -- Modify: `packages/cli/src/linter/spec-gen/renderers.ts` -- Create: `packages/cli/src/linter/spec-gen/renderers.test.ts` -- Modify: `packages/cli/src/linter/spec-gen/generate.ts` -- Modify: `packages/cli/src/linter/spec-gen/compiler.test.ts` - -**Interfaces:** -- Consumes: `SpecConfig.PRIMITIVE_TYPES` (from Task 1), `SpecConfig.STANDARD_UNITS` -- Produces: `typeDefinitions(config: SpecConfig): string` exported from `renderers.ts` - ---- - -- [ ] **Step 1: Create renderers.test.ts with failing tests** - -Create `packages/cli/src/linter/spec-gen/renderers.test.ts`: - -```ts -// Copyright 2026 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// https://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { describe, it, expect } from 'bun:test'; -import { typeDefinitions } from './renderers.js'; -import { SPEC_CONFIG } from '../spec-config.js'; - -describe('typeDefinitions', () => { - it('includes **Color** with its description', () => { - const result = typeDefinitions(SPEC_CONFIG); - expect(result).toContain('**Color**'); - expect(result).toContain('A color value is any valid CSS color string'); - }); - - it('renders Color formats as bullet list', () => { - const result = typeDefinitions(SPEC_CONFIG); - expect(result).toContain('- Hex:'); - expect(result).toContain('- Wide-gamut:'); - expect(result).toContain('- Mixing:'); - }); - - it('includes **Dimension** with its description', () => { - const result = typeDefinitions(SPEC_CONFIG); - expect(result).toContain('**Dimension**'); - expect(result).toContain('A dimension value is a string with a unit suffix'); - }); - - it('appends STANDARD_UNITS inline to Dimension', () => { - const result = typeDefinitions(SPEC_CONFIG); - expect(result).toContain('Valid units are:'); - for (const unit of SPEC_CONFIG.STANDARD_UNITS) { - expect(result).toContain(unit); - } - }); - - it('does not render units as a bullet list for Dimension', () => { - const result = typeDefinitions(SPEC_CONFIG); - const dimensionStart = result.indexOf('**Dimension**'); - const afterDimension = result.slice(dimensionStart); - expect(afterDimension).not.toMatch(/\n- px/); - expect(afterDimension).not.toMatch(/\n- em/); - }); - - it('renders Color note paragraph', () => { - const result = typeDefinitions(SPEC_CONFIG); - expect(result).toContain('internally converted to sRGB'); - expect(result).toContain('recommended default'); - }); -}); -``` - -- [ ] **Step 2: Run tests to confirm they fail** - -```bash -cd packages/cli && bun test src/linter/spec-gen/renderers.test.ts -``` - -Expected: 6 tests FAIL with `typeDefinitions is not a function` or similar. - -- [ ] **Step 3: Add typeDefinitions() to renderers.ts** - -Open `packages/cli/src/linter/spec-gen/renderers.ts`. Add `TypeDef` to the import at the top: - -```ts -import type { SpecConfig, TypographyPropertyDef, SectionDef, ComponentSubTokenDef, TypeDef } from '../spec-config.js'; -``` - -Then append the new function at the end of the file (after `recommendedTokens`): - -```ts -/** Primitive type definitions (Color, Dimension, etc.) for the schema section. */ -export function typeDefinitions(config: SpecConfig): string { - return Object.entries(config.PRIMITIVE_TYPES).map(([name, def]: [string, TypeDef]) => { - let block = `**${name}**: ${def.description}`; - if (def.formats?.length) { - block += '\n\n' + def.formats.map((f: string) => `- ${f}`).join('\n'); - } - if (name === 'Dimension') { - block += ` Valid units are: ${config.STANDARD_UNITS.join(', ')}.`; - } - if (def.note) { - block += '\n\n' + def.note.trim(); - } - return block; - }).join('\n\n'); -} -``` - -- [ ] **Step 4: Run renderer tests to confirm they pass** - -```bash -cd packages/cli && bun test src/linter/spec-gen/renderers.test.ts -``` - -Expected: All 6 tests PASS. - -- [ ] **Step 5: Update compiler.test.ts — add typeDefinitions to full spec compile test** - -Open `packages/cli/src/linter/spec-gen/compiler.test.ts`. In the `'compiles the full spec.mdx with spec-config scope'` test (around line 61), add `typeDefinitions` to the scope object: - -```ts - const scope = { - ...cfg, - frontmatterExample: () => renderers.frontmatterExample(cfg), - colorsExample: () => renderers.colorsExample(cfg), - typographyExample: () => renderers.typographyExample(cfg), - componentsExample: () => renderers.componentsExample(cfg), - typographyPropertyList: () => renderers.typographyPropertyList(cfg), - sectionOrderList: () => renderers.sectionOrderList(cfg), - componentSubTokenList: () => renderers.componentSubTokenList(cfg), - recommendedTokens: () => renderers.recommendedTokens(cfg), - typeDefinitions: () => renderers.typeDefinitions(cfg), - }; -``` - -Also add an assertion in that test to verify the Color definition appears in the output: - -```ts - expect(result).toContain('**Color**'); - expect(result).toContain('oklch()'); -``` - -- [ ] **Step 6: Wire typeDefinitions into generate.ts scope** - -Open `packages/cli/src/linter/spec-gen/generate.ts`. In the `scope` object (around line 42), add: - -```ts - typeDefinitions: () => renderers.typeDefinitions(cfg), -``` - -The full scope object should now be: - -```ts - const scope = { - ...cfg, - frontmatterExample: () => renderers.frontmatterExample(cfg), - colorsExample: () => renderers.colorsExample(cfg), - typographyExample: () => renderers.typographyExample(cfg), - componentsExample: () => renderers.componentsExample(cfg), - typographyPropertyList: () => renderers.typographyPropertyList(cfg), - sectionOrderList: () => renderers.sectionOrderList(cfg), - componentSubTokenList: () => renderers.componentSubTokenList(cfg), - recommendedTokens: () => renderers.recommendedTokens(cfg), - typeDefinitions: () => renderers.typeDefinitions(cfg), - }; -``` - -- [ ] **Step 7: Run all tests to confirm everything still passes** - -```bash -cd packages/cli && bun test -``` - -Expected: All tests PASS (the full spec.mdx compile test will still pass because spec.mdx hasn't been updated yet — `typeDefinitions` is now in scope but not called from the template). - -- [ ] **Step 8: Commit** - -```bash -git add packages/cli/src/linter/spec-gen/renderers.ts packages/cli/src/linter/spec-gen/renderers.test.ts packages/cli/src/linter/spec-gen/generate.ts packages/cli/src/linter/spec-gen/compiler.test.ts -git commit -m "feat: add typeDefinitions renderer and wire into spec generator scope" -``` - ---- - -## Task 3: Template — update spec.mdx and regenerate docs/spec.md - -**Files:** -- Modify: `packages/cli/src/linter/spec-gen/spec.mdx` -- Regenerate: `docs/spec.md` - -**Interfaces:** -- Consumes: `typeDefinitions()` from Task 2 (available in MDX scope) - ---- - -- [ ] **Step 1: Update the import lines in spec.mdx** - -Open `packages/cli/src/linter/spec-gen/spec.mdx`. Line 1 currently imports `STANDARD_UNITS` from spec-config — that direct usage is being removed. Update line 1 to remove `STANDARD_UNITS`: - -**Before (line 1):** -```mdx -import { SPEC_VERSION, STANDARD_UNITS, SECTIONS, TYPOGRAPHY_PROPERTIES, COMPONENT_SUB_TOKENS, CORE_COLOR_ROLES, RECOMMENDED_TOKENS, EXAMPLES } from '../spec-config.js' -``` - -**After (line 1):** -```mdx -import { SPEC_VERSION, SECTIONS, TYPOGRAPHY_PROPERTIES, COMPONENT_SUB_TOKENS, CORE_COLOR_ROLES, RECOMMENDED_TOKENS, EXAMPLES } from '../spec-config.js' -``` - -Update line 2 to add `typeDefinitions` to the renderers import: - -**Before (line 2):** -```mdx -import { frontmatterExample, colorsExample, typographyExample, componentsExample, typographyPropertyList, sectionOrderList, componentSubTokenList, recommendedTokens } from './renderers.js' -``` - -**After (line 2):** -```mdx -import { frontmatterExample, colorsExample, typographyExample, componentsExample, typographyPropertyList, sectionOrderList, componentSubTokenList, recommendedTokens, typeDefinitions } from './renderers.js' -``` - -- [ ] **Step 2: Replace hardcoded Color and Dimension prose in spec.mdx** - -In `spec.mdx`, find and replace the block from the hardcoded `**Color**` paragraph through the hardcoded `**Dimension**` line. The section to replace is lines 46–60: - -**Remove this (lines 46–60):** -```mdx -**Color**: A color value is any valid CSS color string. Supported formats include: - -- Hex: `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA` -- Named colors: `red`, `cornflowerblue`, `transparent` -- Functional: `rgb()`, `rgba()`, `hsl()`, `hsla()`, `hwb()` -- Wide-gamut: `oklch()`, `oklab()`, `lch()`, `lab()` -- Mixing: `color-mix(in srgb, ...)` - -All color values are internally converted to sRGB for WCAG contrast checking. The original format is preserved for display and export. - -Hex notation (`#RRGGBB`) remains the recommended default for simplicity and broad tooling support. - -{typographyPropertyList()} - -**Dimension**: A dimension value is a string with a unit suffix. Valid units are: {STANDARD_UNITS.join(', ')}. -``` - -**Replace with:** -```mdx -{typeDefinitions()} - -{typographyPropertyList()} -``` - -After this edit, the schema section should flow: `{typeDefinitions()}` (Color + Dimension) → `{typographyPropertyList()}` (Typography) → `**Token References**:`. - -- [ ] **Step 3: Run all tests to confirm nothing broke** - -```bash -cd packages/cli && bun test -``` - -Expected: All tests PASS. - -- [ ] **Step 4: Regenerate docs/spec.md** - -```bash -cd packages/cli && bun run spec:gen -``` - -Expected output: -``` -✅ Generated docs/spec.md (N lines) -``` - -- [ ] **Step 5: Verify the regenerated spec is accepted by the --check gate** - -```bash -cd packages/cli && bun run spec:gen --check -``` - -Expected output: -``` -✅ docs/spec.md is up to date. -``` - -- [ ] **Step 6: Inspect the generated output to confirm Color and Dimension appear correctly** - -```bash -grep -A3 "\*\*Color\*\*" docs/spec.md -grep -A1 "\*\*Dimension\*\*" docs/spec.md -``` - -Expected for Color: bold name, followed by format bullets, followed by sRGB note. -Expected for Dimension: bold name with inline "Valid units are: px, em, rem." on the same paragraph. - -- [ ] **Step 7: Commit** - -```bash -git add packages/cli/src/linter/spec-gen/spec.mdx docs/spec.md -git commit -m "feat: replace hardcoded Color/Dimension prose in spec.mdx with typeDefinitions() renderer" -``` - ---- - -## Self-Review - -**Spec coverage check:** - -| Spec requirement | Task that covers it | -|---|---| -| Add `types:` to spec-config.yaml with Color and Dimension | Task 1, Step 3 | -| Zod schema validates the new `types` field | Task 1, Steps 4–5 | -| `TypeDef` interface exported from spec-config.ts | Task 1, Step 6 | -| `PRIMITIVE_TYPES` constant exported | Task 1, Step 7 | -| `SpecConfig` interface and `SPEC_CONFIG` bundle include `PRIMITIVE_TYPES` | Task 1, Step 8 | -| Structural invariant tests for `PRIMITIVE_TYPES` | Task 1, Step 1 | -| `typeDefinitions()` renderer in renderers.ts | Task 2, Step 3 | -| Dimension units come from `STANDARD_UNITS`, not duplicated | Task 2, Step 3 (name === 'Dimension' branch) | -| Color note renders both paragraphs | Task 2, Step 3 (`def.note.trim()`) | -| `typeDefinitions` bound in generate.ts scope | Task 2, Step 6 | -| Full spec.mdx compile test updated | Task 2, Step 5 | -| Hardcoded Color prose removed from spec.mdx | Task 3, Step 2 | -| Hardcoded Dimension prose removed from spec.mdx | Task 3, Step 2 | -| `STANDARD_UNITS` removed from spec.mdx import | Task 3, Step 1 | -| `typeDefinitions` added to spec.mdx import | Task 3, Step 1 | -| docs/spec.md regenerated | Task 3, Steps 4–5 | - -**Placeholder scan:** No TBDs, no "similar to above", all code shown explicitly. ✓ - -**Type consistency:** -- `TypeDef` defined in Task 1 Step 6, imported in Task 2 Step 3 — matches. -- `PRIMITIVE_TYPES: Record` used in Task 2 renderer — matches Task 1 export. -- `typeDefinitions(config: SpecConfig): string` defined in Task 2, called in Task 3 — matches. -- `SpecConfig.PRIMITIVE_TYPES` added in Task 1, used in `renderers.ts` via `config.PRIMITIVE_TYPES` — matches. diff --git a/docs/superpowers/specs/2026-06-25-spec-config-color-type-design.md b/docs/superpowers/specs/2026-06-25-spec-config-color-type-design.md deleted file mode 100644 index 7372ea96..00000000 --- a/docs/superpowers/specs/2026-06-25-spec-config-color-type-design.md +++ /dev/null @@ -1,173 +0,0 @@ ---- -name: spec-config-color-type -description: Add a data-driven `types` section to spec-config.yaml so Color and Dimension definitions are generated from config rather than hardcoded in spec.mdx -metadata: - type: project - issue: 97 - branch: fix/spec-config-color-type ---- - -# Design: Data-driven primitive type definitions (issue #97) - -## Problem - -`spec-config.yaml` is the single source of truth for the DESIGN.md format specification, but the Color and Dimension type definitions are hardcoded as static prose in `spec.mdx`. Anyone reading only the YAML cannot learn what a Color value is. The goal is to make these definitions fully data-driven — the same pattern already used for typography properties and component sub-tokens. - -## Solution overview - -Add a `types:` map to `spec-config.yaml`. Each entry describes one primitive type (Color, Dimension) with its description, optional accepted-format list, and optional follow-up note. A new `typeDefinitions()` renderer in `renderers.ts` converts this map to markdown. The two hardcoded blocks in `spec.mdx` are replaced with a single `{typeDefinitions()}` call. - ---- - -## 1. `spec-config.yaml` — new `types:` section - -Inserted after `units:`, before `sections:`. - -```yaml -types: - Color: - description: "A color value is any valid CSS color string. Supported formats include:" - formats: - - "Hex: `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`" - - "Named colors: `red`, `cornflowerblue`, `transparent`" - - "Functional: `rgb()`, `rgba()`, `hsl()`, `hsla()`, `hwb()`" - - "Wide-gamut: `oklch()`, `oklab()`, `lch()`, `lab()`" - - "Mixing: `color-mix(in srgb, ...)`" - note: "All color values are internally converted to sRGB for WCAG contrast checking. The original format is preserved for display and export.\n\nHex notation (`#RRGGBB`) remains the recommended default for simplicity and broad tooling support." - Dimension: - description: "A dimension value is a string with a unit suffix." - # no formats — units are appended automatically from the top-level `units` array -``` - -**Key decision:** `Dimension` has no `units` field in the type definition; the renderer appends "Valid units are: px, em, rem." from the already-existing top-level `STANDARD_UNITS`. This avoids duplication. - ---- - -## 2. `spec-config.ts` — schema, interface, exports - -### Zod schema additions - -```ts -const TypeDefSchema = z.object({ - description: z.string(), - formats: z.array(z.string()).optional(), - note: z.string().optional(), -}); - -// inside ConfigSchema: -types: z.record(z.string(), TypeDefSchema).optional().default({}), -``` - -### New TypeScript interface - -```ts -export interface TypeDef { - description: string; - formats?: readonly string[]; - note?: string; -} -``` - -### New constant and SpecConfig extension - -```ts -export const PRIMITIVE_TYPES: Record = config.types; - -// Add to SpecConfig interface: -PRIMITIVE_TYPES: typeof PRIMITIVE_TYPES; - -// Add to SPEC_CONFIG bundle: -PRIMITIVE_TYPES, -``` - ---- - -## 3. `renderers.ts` — `typeDefinitions()` function - -New pure renderer. `Dimension` is the only type that gets units appended (by name match), keeping all other types generic. - -```ts -export function typeDefinitions(config: SpecConfig): string { - return Object.entries(config.PRIMITIVE_TYPES).map(([name, def]) => { - let block = `**${name}**: ${def.description}`; - if (def.formats?.length) { - block += '\n\n' + def.formats.map(f => `- ${f}`).join('\n'); - } - if (name === 'Dimension') { - block += ` Valid units are: ${config.STANDARD_UNITS.join(', ')}.`; - } - if (def.note) { - block += '\n\n' + def.note; - } - return block; - }).join('\n\n'); -} -``` - ---- - -## 4. `spec.mdx` — replace hardcoded blocks - -**Remove** lines 46–60 (the Color and Dimension static paragraphs). - -**Replace** with: - -```mdx -{typeDefinitions()} -``` - -Update the import at the top of `spec.mdx`: - -```mdx -import { ..., typeDefinitions } from './renderers.js' -``` - ---- - -## 5. `generate.ts` — bind renderer into scope - -```ts -typeDefinitions: () => renderers.typeDefinitions(cfg), -``` - ---- - -## 6. Tests - -### `spec-config.test.ts` — structural invariants - -- `PRIMITIVE_TYPES` contains at least `Color` and `Dimension`. -- Each entry has a non-empty `description`. -- Type names are unique (already guaranteed by `z.record`, but worth asserting). - -### `renderers.test.ts` (new file, or added to `compiler.test.ts`) - -- `typeDefinitions()` output includes `**Color**` and `**Dimension**`. -- `typeDefinitions()` output includes at least one format bullet for Color. -- `typeDefinitions()` output includes `STANDARD_UNITS` values for Dimension. -- `typeDefinitions()` output does NOT include a `units` bullet list for Dimension (units are appended inline, not as a list). - -### Regenerate spec - -Run `bun run spec:gen` and verify `bun run spec:gen --check` passes (CI gate). - ---- - -## Files changed - -| File | Change | -|---|---| -| `packages/cli/src/linter/spec-config.yaml` | Add `types:` section | -| `packages/cli/src/linter/spec-config.ts` | Add `TypeDefSchema`, `TypeDef`, `PRIMITIVE_TYPES`, extend `SpecConfig` | -| `packages/cli/src/linter/spec-gen/renderers.ts` | Add `typeDefinitions()` | -| `packages/cli/src/linter/spec-gen/spec.mdx` | Replace hardcoded Color+Dimension prose with `{typeDefinitions()}` | -| `packages/cli/src/linter/spec-gen/generate.ts` | Bind `typeDefinitions` in scope | -| `packages/cli/src/linter/spec-config.test.ts` | Add invariant tests for `PRIMITIVE_TYPES` | -| `packages/cli/src/linter/spec-gen/compiler.test.ts` | Add renderer output tests | -| `docs/spec.md` | Regenerated output | - -## Out of scope - -- Composite types (Typography) — not a primitive, not referenced in the issue. -- Runtime linter validation using the types map — no change to linter behaviour. -- Adding more types beyond Color and Dimension — YAGNI.