diff --git a/packages/pen-core/src/__tests__/semantic-palette.test.ts b/packages/pen-core/src/__tests__/semantic-palette.test.ts index 4edcf5631..42ad57df4 100644 --- a/packages/pen-core/src/__tests__/semantic-palette.test.ts +++ b/packages/pen-core/src/__tests__/semantic-palette.test.ts @@ -20,9 +20,9 @@ import { */ describe('getSemanticPalette', () => { - it('returns 14 original + 8 alert + 4 extended + 6 chart = 32 color variables', () => { + it('returns 32 color + 18 type tokens = 50 total variables', () => { const p = getSemanticPalette(); - expect(Object.keys(p.variables).length).toBe(32); + expect(Object.keys(p.variables).length).toBe(50); }); it('defines the Mode theme axis with Light + Dark', () => { @@ -131,9 +131,14 @@ describe('getSemanticPaletteHex', () => { it('returns all color token names (omits numeric tokens)', () => { const hex = getSemanticPaletteHex(); - // SEMANTIC_PALETTE_NAMES includes all 28 color tokens (no numerics yet at P1.1.5) - for (const name of SEMANTIC_PALETTE_NAMES) { - expect(hex[name], `${name} should be in hex map`).toBeDefined(); + const p = getSemanticPalette(); + // Only color variables should appear in the hex map + for (const [name, def] of Object.entries(p.variables)) { + if ((def as VariableDefinition).type === 'color') { + expect(hex[name], `${name} should be in hex map`).toBeDefined(); + } else { + expect(hex[name], `${name} (numeric) should NOT be in hex map`).toBeUndefined(); + } } }); }); @@ -156,7 +161,7 @@ describe('applySemanticPalette', () => { it('seeds an empty document with palette + theme axis', () => { const doc: PenDocument = createEmptyDocument(); const out = applySemanticPalette(doc); - expect(Object.keys(out.variables ?? {}).length).toBe(32); + expect(Object.keys(out.variables ?? {}).length).toBe(50); expect(out.themes?.Mode).toEqual(['Light', 'Dark']); }); @@ -179,7 +184,7 @@ describe('applySemanticPalette', () => { expect(out.variables!['color-accent'].value).toBe('#FF0000'); // All other palette vars still added expect(out.variables!['color-surface']).toBeDefined(); - expect(Object.keys(out.variables!).length).toBe(32); + expect(Object.keys(out.variables!).length).toBe(50); }); it('user-defined theme axis WINS (does not overwrite existing Mode)', () => { @@ -206,7 +211,7 @@ describe('applySemanticPalette', () => { it('applies cleanly to a doc without variables or themes', () => { const doc: PenDocument = { version: '1.0.0', children: [] }; const out = applySemanticPalette(doc); - expect(Object.keys(out.variables!).length).toBe(32); + expect(Object.keys(out.variables!).length).toBe(50); expect(out.themes!.Mode).toEqual(['Light', 'Dark']); }); }); @@ -399,19 +404,85 @@ describe('chart color tokens (6 single-value)', () => { }); }); -describe('palette count after P1.1.5 + P1.1.6 color additions', () => { - it('getSemanticPalette() now has 32 color variables', () => { +describe('palette count snapshot (color additions complete)', () => { + it('palette has exactly 32 color-type variables', () => { const p = getSemanticPalette(); - expect(Object.keys(p.variables).length).toBe(32); + const colorCount = Object.values(p.variables).filter( + (d) => (d as VariableDefinition).type === 'color', + ).length; + expect(colorCount).toBe(32); }); - it('SEMANTIC_PALETTE_NAMES has 32 entries', () => { - expect(SEMANTIC_PALETTE_NAMES.length).toBe(32); + it('SEMANTIC_PALETTE_NAMES matches total variable count', () => { + const p = getSemanticPalette(); + expect(SEMANTIC_PALETTE_NAMES.length).toBe(Object.keys(p.variables).length); }); - it('applySemanticPalette seeds 32 variables on an empty document', () => { + it('applySemanticPalette seeds all palette variables on an empty document', () => { const doc: PenDocument = createEmptyDocument(); const out = applySemanticPalette(doc); - expect(Object.keys(out.variables ?? {}).length).toBe(32); + expect(Object.keys(out.variables ?? {}).length).toBe(Object.keys(getSemanticPalette().variables).length); + }); +}); + +// ───────────────────────────────────────────────────────────────────────────── +// Task 1.2 — Typography tokens: size + weight + line-height × 6 roles (18) +// ───────────────────────────────────────────────────────────────────────────── + +describe('typography tokens (18 numeric single-value)', () => { + const TYPE_TOKENS: Record = { + 'type-display-size': 64, + 'type-display-weight': 700, + 'type-display-line-height': 1.0, + 'type-h1-size': 24, + 'type-h1-weight': 600, + 'type-h1-line-height': 1.2, + 'type-h2-size': 20, + 'type-h2-weight': 600, + 'type-h2-line-height': 1.25, + 'type-h3-size': 16, + 'type-h3-weight': 600, + 'type-h3-line-height': 1.3, + 'type-body-size': 14, + 'type-body-weight': 400, + 'type-body-line-height': 1.5, + 'type-caption-size': 12, + 'type-caption-weight': 400, + 'type-caption-line-height': 1.4, + }; + + it('all 18 typography tokens present in getSemanticPalette()', () => { + const p = getSemanticPalette(); + for (const name of Object.keys(TYPE_TOKENS)) { + expect(p.variables[name], `missing: ${name}`).toBeDefined(); + } + }); + + it('typography tokens have type="number"', () => { + const p = getSemanticPalette(); + for (const name of Object.keys(TYPE_TOKENS)) { + const def = p.variables[name] as VariableDefinition; + expect(def.type, `${name} type`).toBe('number'); + } + }); + + it('typography token values match spec', () => { + const p = getSemanticPalette(); + for (const [name, expected] of Object.entries(TYPE_TOKENS)) { + const def = p.variables[name] as VariableDefinition; + expect(def.value, `${name}`).toBe(expected); + } + }); + + it('typography tokens are NOT included in getSemanticPaletteHex()', () => { + const hex = getSemanticPaletteHex('Light'); + for (const name of Object.keys(TYPE_TOKENS)) { + expect(hex[name], `${name} should not appear in hex map`).toBeUndefined(); + } + }); + + it('palette total grows to 50 (32 color + 18 type)', () => { + const p = getSemanticPalette(); + expect(Object.keys(p.variables).length).toBe(50); }); }); diff --git a/packages/pen-core/src/variables/semantic-palette.ts b/packages/pen-core/src/variables/semantic-palette.ts index 74c0e64b2..cc79444e7 100644 --- a/packages/pen-core/src/variables/semantic-palette.ts +++ b/packages/pen-core/src/variables/semantic-palette.ts @@ -207,6 +207,26 @@ const PALETTE: Record = { description: 'Stronger danger text — tag "error" label on light bg', }, + // ── Typography: size + weight + line-height × 6 roles (18 numeric) ─────── + 'type-display-size': { single: 64, description: 'Display text — font size (px)' }, + 'type-display-weight': { single: 700, description: 'Display text — font weight' }, + 'type-display-line-height': { single: 1.0, description: 'Display text — line height multiplier' }, + 'type-h1-size': { single: 24, description: 'Heading 1 — font size (px)' }, + 'type-h1-weight': { single: 600, description: 'Heading 1 — font weight' }, + 'type-h1-line-height': { single: 1.2, description: 'Heading 1 — line height multiplier' }, + 'type-h2-size': { single: 20, description: 'Heading 2 — font size (px)' }, + 'type-h2-weight': { single: 600, description: 'Heading 2 — font weight' }, + 'type-h2-line-height': { single: 1.25, description: 'Heading 2 — line height multiplier' }, + 'type-h3-size': { single: 16, description: 'Heading 3 — font size (px)' }, + 'type-h3-weight': { single: 600, description: 'Heading 3 — font weight' }, + 'type-h3-line-height': { single: 1.3, description: 'Heading 3 — line height multiplier' }, + 'type-body-size': { single: 14, description: 'Body text — font size (px)' }, + 'type-body-weight': { single: 400, description: 'Body text — font weight' }, + 'type-body-line-height': { single: 1.5, description: 'Body text — line height multiplier' }, + 'type-caption-size': { single: 12, description: 'Caption / helper text — font size (px)' }, + 'type-caption-weight': { single: 400, description: 'Caption / helper text — font weight' }, + 'type-caption-line-height': { single: 1.4, description: 'Caption / helper text — line height multiplier' }, + // ── Chart palette (6 single-value colors, no theme axis) ───────────────── 'color-chart-1': { single: '#3B82F6', description: 'Chart series 1 — blue' }, 'color-chart-2': { single: '#8B5CF6', description: 'Chart series 2 — purple' },