feat(types): add 18 typography tokens to semantic-palette (P1.2)

size + weight + line-height for 6 roles: display / h1 / h2 / h3 / body / caption.
All 18 tokens use type="number" with a plain scalar value (no theme axis).
getSemanticPaletteHex() omits numeric tokens from its return map.
Palette grows from 32 color → 50 total variables.
This commit is contained in:
Fini 2026-04-29 09:50:07 +08:00
parent 7eefa08acf
commit 67003d72f6
2 changed files with 106 additions and 15 deletions

View file

@ -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<string, number> = {
'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);
});
});

View file

@ -207,6 +207,26 @@ const PALETTE: Record<string, PaletteEntry> = {
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' },