Skip to content
Merged
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
10 changes: 3 additions & 7 deletions docs/spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,7 @@ 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. Supported formats include:

- Hex: `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`
- Named colors: `red`, `cornflowerblue`, `transparent`
Expand All @@ -73,10 +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.
Expand All @@ -87,6 +81,8 @@ Valid units are: px, em, rem.
- `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
1 change: 1 addition & 0 deletions packages/cli/src/commands/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,4 @@ export default defineCommand({
process.exitCode = 0;
},
});

25 changes: 25 additions & 0 deletions packages/cli/src/linter/spec-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
resolveAlias,
VALID_TYPOGRAPHY_PROPS,
VALID_COMPONENT_SUB_TOKENS,
PRIMITIVE_TYPES,
} from './spec-config.js';

// ── Loader robustness ─────────────────────────────────────────────────
Expand Down Expand Up @@ -68,6 +69,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"}',
Expand Down Expand Up @@ -215,3 +217,26 @@ describe('spec-config derived constants', () => {
}
});
});

// ── 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();
});
});
5 changes: 5 additions & 0 deletions packages/cli/src/linter/spec-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ 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<string, TypeDef> = config.types;

// ── Derived constants ─────────────────────────────────────────────────

/** Ordered list of canonical section names. */
Expand Down Expand Up @@ -207,6 +210,7 @@ export interface SpecConfig {
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. */
Expand All @@ -222,4 +226,5 @@ export const SPEC_CONFIG: SpecConfig = {
CORE_COLOR_ROLES,
RECOMMENDED_TOKENS,
EXAMPLES,
PRIMITIVE_TYPES,
};
4 changes: 2 additions & 2 deletions packages/cli/src/linter/spec-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ units:

types:
Color:
description: A color value is any valid CSS color string.
description: "A color value is any valid CSS color string. Supported formats include:"
formats:
- "Hex: `#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`"
- "Named colors: `red`, `cornflowerblue`, `transparent`"
Expand All @@ -40,7 +40,7 @@ types:
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.
description: "A dimension value is a string with a unit suffix."
units:
- px
- em
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/linter/spec-gen/compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,10 @@ 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),
typeDefinitions: (typeName?: string) => renderers.typeDefinitions(cfg, typeName),
};

const result = await compileMdx(source, scope);
Expand All @@ -85,5 +85,7 @@ describe('compileMdx', () => {
expect(result).toContain('backgroundColor');
expect(result).toContain('`headline-display`');
expect(result).toContain('#1A1C1E');
expect(result).toContain('**Color**');
expect(result).toContain('oklch()');
});
});
2 changes: 1 addition & 1 deletion packages/cli/src/linter/spec-gen/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ 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),
typeDefinitions: (typeName?: string) => renderers.typeDefinitions(cfg, typeName),
};

const generated = await compileMdx(source, scope);
Expand Down
75 changes: 75 additions & 0 deletions packages/cli/src/linter/spec-gen/renderers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// 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');
});

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('');
});
});
51 changes: 23 additions & 28 deletions packages/cli/src/linter/spec-gen/renderers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,34 +95,29 @@ 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');
/** Primitive type definitions (Color, Dimension, etc.) for the schema section. */
export function typeDefinitions(config: SpecConfig, typeName?: string): string {
const types = config.PRIMITIVE_TYPES || config.SPEC_TYPES;
const entries = typeName
? Object.entries(types).filter(([n]) => n === typeName)
: Object.entries(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');
}
if (name === 'Dimension' || def.units?.length) {
const units = def.units?.length ? def.units : config.STANDARD_UNITS;
block += ` Valid units are: ${units.join(', ')}.`;
}
if (def.note) {
block += '\n\n' + def.note.trim();
}
if (def.recommendation) {
block += '\n\n' + def.recommendation.trim();
}
return block;
}).join('\n\n');
}

/** Numbered section order list with aliases. */
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/linter/spec-gen/spec.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@ 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.

{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
Expand Down
Loading