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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 12 additions & 8 deletions docs/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,24 @@ components:

The `<scale-level>` 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.
Expand All @@ -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
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/src/linter/spec-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
loadSpecConfig,
getSpecConfig,
STANDARD_UNITS,
SPEC_TYPES,
SECTIONS,
TYPOGRAPHY_PROPERTIES,
COMPONENT_SUB_TOKENS,
Expand Down Expand Up @@ -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);
Expand Down
27 changes: 27 additions & 0 deletions packages/cli/src/linter/spec-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,22 @@ 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({
max_token_nesting_depth: z.number().default(20),
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(),
Expand Down Expand Up @@ -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.
Expand All @@ -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<string, TypeDef> = config.types;

export const SECTIONS = config.sections;

export const TYPOGRAPHY_PROPERTIES: readonly TypographyPropertyDef[] = config.typography_properties;
Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand Down
18 changes: 18 additions & 0 deletions packages/cli/src/linter/spec-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/linter/spec-gen/compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/linter/spec-gen/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
32 changes: 31 additions & 1 deletion packages/cli/src/linter/spec-gen/renderers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ─────────────────────────────────────

Expand Down Expand Up @@ -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) => {
Expand Down
6 changes: 6 additions & 0 deletions packages/cli/src/linter/spec-gen/spec-helpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
18 changes: 3 additions & 15 deletions packages/cli/src/linter/spec-gen/spec.mdx
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -43,22 +43,10 @@ components:

The `<scale-level>` 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
Expand Down