diff --git a/docs/spec.md b/docs/spec.md index 41b6d085..5ff34173 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -59,18 +59,24 @@ 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: +**Color**: A color value is any valid CSS color string. -* 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, ...)` +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. +**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 +87,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 diff --git a/packages/cli/src/linter/spec-config.test.ts b/packages/cli/src/linter/spec-config.test.ts index 0115b371..b1ab7cbc 100644 --- a/packages/cli/src/linter/spec-config.test.ts +++ b/packages/cli/src/linter/spec-config.test.ts @@ -18,6 +18,7 @@ import { loadSpecConfig, getSpecConfig, STANDARD_UNITS, + SPEC_TYPES, SECTIONS, TYPOGRAPHY_PROPERTIES, COMPONENT_SUB_TOKENS, @@ -159,6 +160,14 @@ describe('spec-config structural invariants', () => { expect(new Set(STANDARD_UNITS).size).toBe(STANDARD_UNITS.length); }); + it('primitive type definitions are non-empty', () => { + expect(Object.keys(SPEC_TYPES).length).toBeGreaterThan(0); + for (const [name, typeDef] of Object.entries(SPEC_TYPES)) { + expect(name.length).toBeGreaterThan(0); + expect(typeDef.description.length).toBeGreaterThan(0); + } + }); + it('recommended token categories are non-empty', () => { for (const [category, tokens] of Object.entries(RECOMMENDED_TOKENS)) { expect(tokens.length).toBeGreaterThan(0); diff --git a/packages/cli/src/linter/spec-config.ts b/packages/cli/src/linter/spec-config.ts index b146a47b..30339a19 100644 --- a/packages/cli/src/linter/spec-config.ts +++ b/packages/cli/src/linter/spec-config.ts @@ -38,6 +38,14 @@ const PropertyDefSchema = z.object({ description: z.string().optional(), }); +const TypeDefSchema = z.object({ + description: z.string(), + formats: z.array(z.string()).optional(), + units: z.array(z.string()).optional(), + note: z.string().optional(), + recommendation: z.string().optional(), +}); + const ConfigSchema = z.object({ version: z.string(), limits: z.object({ @@ -45,6 +53,7 @@ const ConfigSchema = z.object({ max_reference_depth: z.number().default(10), }).default({}), units: z.array(z.string()).min(1), + types: z.record(z.string(), TypeDefSchema), sections: z.array(z.object({ canonical: z.string(), aliases: z.array(z.string()).optional(), @@ -112,6 +121,19 @@ export interface ComponentSubTokenDef { description?: string | undefined; } +export interface TypeDef { + /** One-sentence definition for the type. */ + description: string; + /** Accepted formats rendered as a bullet list in the generated spec. */ + formats?: readonly string[] | undefined; + /** Accepted units for dimensional types. */ + units?: readonly string[] | undefined; + /** Additional normative or implementation note. */ + note?: string | undefined; + /** Non-normative authoring recommendation. */ + recommendation?: 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. @@ -129,6 +151,9 @@ export const MAX_REFERENCE_DEPTH = config.limits.max_reference_depth; export const STANDARD_UNITS = config.units; export type StandardUnit = (typeof STANDARD_UNITS)[number]; +/** Primitive type definitions rendered into the generated spec. */ +export const SPEC_TYPES: Record = config.types; + export const SECTIONS = config.sections; export const TYPOGRAPHY_PROPERTIES: readonly TypographyPropertyDef[] = config.typography_properties; @@ -175,6 +200,7 @@ export interface SpecConfig { MAX_TOKEN_NESTING_DEPTH: typeof MAX_TOKEN_NESTING_DEPTH; MAX_REFERENCE_DEPTH: typeof MAX_REFERENCE_DEPTH; STANDARD_UNITS: typeof STANDARD_UNITS; + SPEC_TYPES: typeof SPEC_TYPES; SECTIONS: typeof SECTIONS; TYPOGRAPHY_PROPERTIES: typeof TYPOGRAPHY_PROPERTIES; COMPONENT_SUB_TOKENS: typeof COMPONENT_SUB_TOKENS; @@ -189,6 +215,7 @@ export const SPEC_CONFIG: SpecConfig = { MAX_TOKEN_NESTING_DEPTH, MAX_REFERENCE_DEPTH, STANDARD_UNITS, + SPEC_TYPES, SECTIONS, TYPOGRAPHY_PROPERTIES, COMPONENT_SUB_TOKENS, diff --git a/packages/cli/src/linter/spec-config.yaml b/packages/cli/src/linter/spec-config.yaml index 67dfa143..478be8f9 100644 --- a/packages/cli/src/linter/spec-config.yaml +++ b/packages/cli/src/linter/spec-config.yaml @@ -28,6 +28,24 @@ units: - em - rem +types: + Color: + description: A color value is any valid CSS color string. + 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. + recommendation: 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. + units: + - px + - em + - rem + sections: - canonical: Overview aliases: diff --git a/packages/cli/src/linter/spec-gen/compiler.test.ts b/packages/cli/src/linter/spec-gen/compiler.test.ts index 656c796d..ce260aa4 100644 --- a/packages/cli/src/linter/spec-gen/compiler.test.ts +++ b/packages/cli/src/linter/spec-gen/compiler.test.ts @@ -70,6 +70,7 @@ describe('compileMdx', () => { typographyExample: () => renderers.typographyExample(cfg), componentsExample: () => renderers.componentsExample(cfg), typographyPropertyList: () => renderers.typographyPropertyList(cfg), + typeDefinitions: () => renderers.typeDefinitions(cfg), sectionOrderList: () => renderers.sectionOrderList(cfg), componentSubTokenList: () => renderers.componentSubTokenList(cfg), recommendedTokens: () => renderers.recommendedTokens(cfg), diff --git a/packages/cli/src/linter/spec-gen/generate.ts b/packages/cli/src/linter/spec-gen/generate.ts index b622ec37..b397ddf4 100644 --- a/packages/cli/src/linter/spec-gen/generate.ts +++ b/packages/cli/src/linter/spec-gen/generate.ts @@ -46,6 +46,7 @@ async function main() { typographyExample: () => renderers.typographyExample(cfg), componentsExample: () => renderers.componentsExample(cfg), typographyPropertyList: () => renderers.typographyPropertyList(cfg), + typeDefinitions: () => renderers.typeDefinitions(cfg), sectionOrderList: () => renderers.sectionOrderList(cfg), componentSubTokenList: () => renderers.componentSubTokenList(cfg), recommendedTokens: () => renderers.recommendedTokens(cfg), diff --git a/packages/cli/src/linter/spec-gen/renderers.ts b/packages/cli/src/linter/spec-gen/renderers.ts index c7cfb985..9e0354d6 100644 --- a/packages/cli/src/linter/spec-gen/renderers.ts +++ b/packages/cli/src/linter/spec-gen/renderers.ts @@ -19,7 +19,7 @@ * Each function returns a ready-to-embed markdown string. */ -import type { SpecConfig, TypographyPropertyDef, SectionDef, ComponentSubTokenDef } from '../spec-config.js'; +import type { SpecConfig, TypographyPropertyDef, SectionDef, ComponentSubTokenDef, TypeDef } from '../spec-config.js'; // ── YAML code block helpers ───────────────────────────────────── @@ -95,6 +95,36 @@ export function typographyPropertyList(config: SpecConfig): string { ).join('\n'); } +/** Primitive type definitions for the schema section. */ +export function typeDefinitions(config: SpecConfig): string { + return Object.entries(config.SPEC_TYPES) + .map(([name, typeDef]) => typeDefinition(name, typeDef)) + .join('\n\n'); +} + +function typeDefinition(name: string, typeDef: TypeDef): string { + const lines = [`**${name}**: ${typeDef.description}`]; + + if (typeDef.formats?.length) { + lines.push('', 'Supported formats include:', ''); + lines.push(...typeDef.formats.map(format => `- ${format}`)); + } + + if (typeDef.units?.length) { + lines.push('', `Valid units are: ${typeDef.units.join(', ')}.`); + } + + if (typeDef.note) { + lines.push('', typeDef.note); + } + + if (typeDef.recommendation) { + lines.push('', typeDef.recommendation); + } + + return lines.join('\n'); +} + /** Numbered section order list with aliases. */ export function sectionOrderList(config: SpecConfig): string { return config.SECTIONS.map((s: SectionDef, i: number) => { diff --git a/packages/cli/src/linter/spec-gen/spec-helpers.test.ts b/packages/cli/src/linter/spec-gen/spec-helpers.test.ts index 926bac71..4c3ae081 100644 --- a/packages/cli/src/linter/spec-gen/spec-helpers.test.ts +++ b/packages/cli/src/linter/spec-gen/spec-helpers.test.ts @@ -58,6 +58,12 @@ describe('getSpecContent', () => { expect(content.length).toBeGreaterThan(1000); }); + it('renders primitive type definitions from spec config', () => { + const content = getSpecContent(); + expect(content).toContain('**Color**: A color value is any valid CSS color string.'); + expect(content).toContain('**Dimension**: A dimension value is a string with a unit suffix.'); + }); + it('accepts an explicit specPath override', () => { // This tests the explicit-path contract. If someone passes a path, // it should use that path exactly — no guessing. diff --git a/packages/cli/src/linter/spec-gen/spec.mdx b/packages/cli/src/linter/spec-gen/spec.mdx index 44064d5c..b82bbc51 100644 --- a/packages/cli/src/linter/spec-gen/spec.mdx +++ b/packages/cli/src/linter/spec-gen/spec.mdx @@ -1,5 +1,5 @@ -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' +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, typeDefinitions, sectionOrderList, componentSubTokenList, recommendedTokens } from './renderers.js' # DESIGN.md Format @@ -43,22 +43,10 @@ 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. +{typeDefinitions()} {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