feat(design-system): define three-level tokens
This commit is contained in:
parent
129282d634
commit
fdffe204e4
88
packages/design-system/src/tokens/components.ts
Normal file
88
packages/design-system/src/tokens/components.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import type { TokenCollection } from './types';
|
||||
|
||||
export const componentTokens = {
|
||||
// Button
|
||||
'button.font.weight': { $type: 'fontWeight' as const, $value: '{font.weight.semibold}' },
|
||||
'button.radius': { $type: 'dimension' as const, $value: '{radius.md}' },
|
||||
'button.padding.x': { $type: 'dimension' as const, $value: '{space.inset.lg}' },
|
||||
'button.padding.y': { $type: 'dimension' as const, $value: '{space.inset.sm}' },
|
||||
'button.primary.bg': { $type: 'color' as const, $value: '{color.action.primary}' },
|
||||
'button.primary.text': { $type: 'color' as const, $value: '{color.text.inverse}' },
|
||||
'button.primary.hover.bg': { $type: 'color' as const, $value: '{color.action.primaryHover}' },
|
||||
'button.secondary.bg': { $type: 'color' as const, $value: '{color.surface.default}' },
|
||||
'button.secondary.text': { $type: 'color' as const, $value: '{color.text.primary}' },
|
||||
'button.secondary.border': { $type: 'color' as const, $value: '{color.border.default}' },
|
||||
'button.danger.bg': { $type: 'color' as const, $value: '{color.action.danger}' },
|
||||
'button.danger.text': { $type: 'color' as const, $value: '{color.text.inverse}' },
|
||||
|
||||
// IconButton
|
||||
'iconButton.size.sm': { $type: 'dimension' as const, $value: '{size.control.sm}' },
|
||||
'iconButton.size.md': { $type: 'dimension' as const, $value: '{size.control.md}' },
|
||||
|
||||
// Field
|
||||
'field.radius': { $type: 'dimension' as const, $value: '{radius.md}' },
|
||||
'field.border': { $type: 'color' as const, $value: '{color.border.default}' },
|
||||
'field.border.focus': { $type: 'color' as const, $value: '{color.border.focus}' },
|
||||
'field.border.error': { $type: 'color' as const, $value: '{color.feedback.error}' },
|
||||
'field.label.weight': { $type: 'fontWeight' as const, $value: '{font.weight.medium}' },
|
||||
|
||||
// Checkbox
|
||||
'checkbox.radius': { $type: 'dimension' as const, $value: '{radius.sm}' },
|
||||
|
||||
// Surface
|
||||
'surface.radius': { $type: 'dimension' as const, $value: '{radius.lg}' },
|
||||
'surface.padding': { $type: 'dimension' as const, $value: '{space.inset.lg}' },
|
||||
|
||||
// Card
|
||||
'card.radius': { $type: 'dimension' as const, $value: '{radius.lg}' },
|
||||
'card.shadow': { $type: 'shadow' as const, $value: '{shadow.sm}' },
|
||||
'card.padding': { $type: 'dimension' as const, $value: '{space.inset.lg}' },
|
||||
|
||||
// Chip
|
||||
'chip.radius': { $type: 'dimension' as const, $value: '{radius.pill}' },
|
||||
'chip.height': { $type: 'dimension' as const, $value: '{size.control.sm}' },
|
||||
|
||||
// Badge
|
||||
'badge.radius': { $type: 'dimension' as const, $value: '{radius.pill}' },
|
||||
'badge.font.size': { $type: 'dimension' as const, $value: '{font.size.200}' },
|
||||
|
||||
// Alert
|
||||
'alert.radius': { $type: 'dimension' as const, $value: '{radius.md}' },
|
||||
'alert.info.bg': { $type: 'color' as const, $value: '{color.blue.50}' },
|
||||
'alert.info.border': { $type: 'color' as const, $value: '{color.feedback.info}' },
|
||||
'alert.success.bg': { $type: 'color' as const, $value: '{color.green.50}' },
|
||||
'alert.success.border': { $type: 'color' as const, $value: '{color.feedback.success}' },
|
||||
'alert.warning.bg': { $type: 'color' as const, $value: '{color.amber.50}' },
|
||||
'alert.warning.border': { $type: 'color' as const, $value: '{color.feedback.warning}' },
|
||||
'alert.error.bg': { $type: 'color' as const, $value: '{color.red.50}' },
|
||||
'alert.error.border': { $type: 'color' as const, $value: '{color.feedback.error}' },
|
||||
|
||||
// Dialog
|
||||
'dialog.radius': { $type: 'dimension' as const, $value: '{radius.lg}' },
|
||||
'dialog.shadow': { $type: 'shadow' as const, $value: '{shadow.md}' },
|
||||
'dialog.padding': { $type: 'dimension' as const, $value: '{space.6}' },
|
||||
|
||||
// Skeleton
|
||||
'skeleton.radius': { $type: 'dimension' as const, $value: '{radius.sm}' },
|
||||
|
||||
// Progress
|
||||
'progress.height': { $type: 'dimension' as const, $value: '{space.1}' },
|
||||
'progress.radius': { $type: 'dimension' as const, $value: '{radius.pill}' },
|
||||
'progress.bg': { $type: 'color' as const, $value: '{color.neutral.100}' },
|
||||
'progress.fill': { $type: 'color' as const, $value: '{color.action.primary}' },
|
||||
|
||||
// Table
|
||||
'table.header.weight': { $type: 'fontWeight' as const, $value: '{font.weight.semibold}' },
|
||||
'table.cell.padding': { $type: 'dimension' as const, $value: '{space.inset.sm}' },
|
||||
'table.row.hover.bg': { $type: 'color' as const, $value: '{color.surface.subtle}' },
|
||||
|
||||
// Metric
|
||||
'metric.label.size': { $type: 'dimension' as const, $value: '{font.size.200}' },
|
||||
'metric.label.weight': { $type: 'fontWeight' as const, $value: '{font.weight.medium}' },
|
||||
'metric.value.weight': { $type: 'fontWeight' as const, $value: '{font.weight.bold}' },
|
||||
'metric.gap': { $type: 'dimension' as const, $value: '{space.1}' },
|
||||
|
||||
// PageState
|
||||
'pageState.icon.size': { $type: 'dimension' as const, $value: '{space.12}' },
|
||||
'pageState.title.gap': { $type: 'dimension' as const, $value: '{space.4}' },
|
||||
} as const satisfies TokenCollection;
|
||||
11
packages/design-system/src/tokens/index.ts
Normal file
11
packages/design-system/src/tokens/index.ts
Normal file
@ -0,0 +1,11 @@
|
||||
export { primitiveTokens } from './primitives';
|
||||
export { semanticLightTokens } from './semantic.light';
|
||||
export { componentTokens } from './components';
|
||||
export { resolveToken } from './resolveToken';
|
||||
export type {
|
||||
TokenType,
|
||||
TokenValue,
|
||||
TokenDefinition,
|
||||
TokenCollection,
|
||||
ResolvedToken,
|
||||
} from './types';
|
||||
103
packages/design-system/src/tokens/primitives.ts
Normal file
103
packages/design-system/src/tokens/primitives.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import type { TokenCollection } from './types';
|
||||
|
||||
export const primitiveTokens = {
|
||||
// Colors — neutral
|
||||
'color.neutral.0': { $type: 'color' as const, $value: '#ffffff' },
|
||||
'color.neutral.50': { $type: 'color' as const, $value: '#f5f5f5' },
|
||||
'color.neutral.100': { $type: 'color' as const, $value: '#e8e8e8' },
|
||||
'color.neutral.200': { $type: 'color' as const, $value: '#d1d1d1' },
|
||||
'color.neutral.400': { $type: 'color' as const, $value: '#a3a3a3' },
|
||||
'color.neutral.600': { $type: 'color' as const, $value: '#666666' },
|
||||
'color.neutral.800': { $type: 'color' as const, $value: '#333333' },
|
||||
'color.neutral.900': { $type: 'color' as const, $value: '#1a1a1a' },
|
||||
|
||||
// Colors — green (positive)
|
||||
'color.green.50': { $type: 'color' as const, $value: '#e8f5e9' },
|
||||
'color.green.100': { $type: 'color' as const, $value: '#c8e6c9' },
|
||||
'color.green.600': { $type: 'color' as const, $value: '#2e7d32' },
|
||||
'color.green.700': { $type: 'color' as const, $value: '#176747' },
|
||||
'color.green.800': { $type: 'color' as const, $value: '#1b5e20' },
|
||||
|
||||
// Colors — red (negative)
|
||||
'color.red.50': { $type: 'color' as const, $value: '#ffebee' },
|
||||
'color.red.600': { $type: 'color' as const, $value: '#e53935' },
|
||||
'color.red.700': { $type: 'color' as const, $value: '#c62828' },
|
||||
|
||||
// Colors — amber (warning)
|
||||
'color.amber.50': { $type: 'color' as const, $value: '#fff8e1' },
|
||||
'color.amber.600': { $type: 'color' as const, $value: '#ffb300' },
|
||||
|
||||
// Colors — blue (info)
|
||||
'color.blue.50': { $type: 'color' as const, $value: '#e3f2fd' },
|
||||
'color.blue.600': { $type: 'color' as const, $value: '#1e88e5' },
|
||||
|
||||
// Spacing (4px grid)
|
||||
'space.0': { $type: 'dimension' as const, $value: '0px' },
|
||||
'space.1': { $type: 'dimension' as const, $value: '4px' },
|
||||
'space.2': { $type: 'dimension' as const, $value: '8px' },
|
||||
'space.3': { $type: 'dimension' as const, $value: '12px' },
|
||||
'space.4': { $type: 'dimension' as const, $value: '16px' },
|
||||
'space.5': { $type: 'dimension' as const, $value: '20px' },
|
||||
'space.6': { $type: 'dimension' as const, $value: '24px' },
|
||||
'space.8': { $type: 'dimension' as const, $value: '32px' },
|
||||
'space.10': { $type: 'dimension' as const, $value: '40px' },
|
||||
'space.12': { $type: 'dimension' as const, $value: '48px' },
|
||||
|
||||
// Border radius
|
||||
'radius.none': { $type: 'dimension' as const, $value: '0px' },
|
||||
'radius.sm': { $type: 'dimension' as const, $value: '4px' },
|
||||
'radius.md': { $type: 'dimension' as const, $value: '8px' },
|
||||
'radius.lg': { $type: 'dimension' as const, $value: '12px' },
|
||||
'radius.pill': { $type: 'dimension' as const, $value: '9999px' },
|
||||
|
||||
// Font family
|
||||
'font.family.sans': {
|
||||
$type: 'fontFamily' as const,
|
||||
$value: ['Inter', 'system-ui', '-apple-system', 'sans-serif'],
|
||||
},
|
||||
'font.family.mono': {
|
||||
$type: 'fontFamily' as const,
|
||||
$value: ['SFMono-Regular', 'Menlo', 'Monaco', 'Consolas', 'monospace'],
|
||||
},
|
||||
|
||||
// Font size (100 = 10px, 700 = 48px scale)
|
||||
'font.size.100': { $type: 'dimension' as const, $value: '10px' },
|
||||
'font.size.200': { $type: 'dimension' as const, $value: '12px' },
|
||||
'font.size.300': { $type: 'dimension' as const, $value: '14px' },
|
||||
'font.size.400': { $type: 'dimension' as const, $value: '16px' },
|
||||
'font.size.500': { $type: 'dimension' as const, $value: '20px' },
|
||||
'font.size.600': { $type: 'dimension' as const, $value: '24px' },
|
||||
'font.size.700': { $type: 'dimension' as const, $value: '32px' },
|
||||
|
||||
// Font weight
|
||||
'font.weight.regular': { $type: 'fontWeight' as const, $value: 400 },
|
||||
'font.weight.medium': { $type: 'fontWeight' as const, $value: 500 },
|
||||
'font.weight.semibold': { $type: 'fontWeight' as const, $value: 600 },
|
||||
'font.weight.bold': { $type: 'fontWeight' as const, $value: 700 },
|
||||
|
||||
// Line height
|
||||
'lineHeight.tight': { $type: 'dimension' as const, $value: '1.25' },
|
||||
'lineHeight.normal': { $type: 'dimension' as const, $value: '1.5' },
|
||||
'lineHeight.relaxed': { $type: 'dimension' as const, $value: '1.75' },
|
||||
|
||||
// Shadows
|
||||
'shadow.none': { $type: 'shadow' as const, $value: 'none' },
|
||||
'shadow.sm': { $type: 'shadow' as const, $value: '0px 1px 2px 0px rgba(0,0,0,0.08)' },
|
||||
'shadow.md': { $type: 'shadow' as const, $value: '0px 4px 8px 0px rgba(0,0,0,0.12)' },
|
||||
|
||||
// Duration
|
||||
'duration.instant': { $type: 'duration' as const, $value: 0 },
|
||||
'duration.fast': { $type: 'duration' as const, $value: 150 },
|
||||
'duration.normal': { $type: 'duration' as const, $value: 250 },
|
||||
|
||||
// Easing
|
||||
'easing.standard': { $type: 'cubicBezier' as const, $value: [0.4, 0, 0.2, 1] as const },
|
||||
|
||||
// Control sizes
|
||||
'size.control.sm': { $type: 'dimension' as const, $value: '32px' },
|
||||
'size.control.md': { $type: 'dimension' as const, $value: '40px' },
|
||||
|
||||
// Focus ring
|
||||
'size.focus.ring': { $type: 'dimension' as const, $value: '2px' },
|
||||
'size.focus.offset': { $type: 'dimension' as const, $value: '2px' },
|
||||
} as const satisfies TokenCollection;
|
||||
61
packages/design-system/src/tokens/semantic.light.ts
Normal file
61
packages/design-system/src/tokens/semantic.light.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import type { TokenCollection } from './types';
|
||||
|
||||
export const semanticLightTokens = {
|
||||
// Canvas
|
||||
'color.canvas': { $type: 'color' as const, $value: '{color.neutral.50}' },
|
||||
|
||||
// Surface
|
||||
'color.surface.default': { $type: 'color' as const, $value: '{color.neutral.0}' },
|
||||
'color.surface.subtle': { $type: 'color' as const, $value: '{color.neutral.50}' },
|
||||
'color.surface.raised': { $type: 'color' as const, $value: '{color.neutral.0}' },
|
||||
|
||||
// Text
|
||||
'color.text.primary': { $type: 'color' as const, $value: '{color.neutral.900}' },
|
||||
'color.text.secondary': { $type: 'color' as const, $value: '{color.neutral.600}' },
|
||||
'color.text.disabled': { $type: 'color' as const, $value: '{color.neutral.400}' },
|
||||
'color.text.inverse': { $type: 'color' as const, $value: '{color.neutral.0}' },
|
||||
|
||||
// Border
|
||||
'color.border.subtle': { $type: 'color' as const, $value: '{color.neutral.100}' },
|
||||
'color.border.default': { $type: 'color' as const, $value: '{color.neutral.200}' },
|
||||
'color.border.strong': { $type: 'color' as const, $value: '{color.neutral.400}' },
|
||||
'color.border.focus': { $type: 'color' as const, $value: '{color.blue.600}' },
|
||||
|
||||
// Action
|
||||
'color.action.primary': { $type: 'color' as const, $value: '{color.green.700}' },
|
||||
'color.action.primaryHover': { $type: 'color' as const, $value: '{color.green.800}' },
|
||||
'color.action.secondary': { $type: 'color' as const, $value: '{color.neutral.600}' },
|
||||
'color.action.danger': { $type: 'color' as const, $value: '{color.red.600}' },
|
||||
|
||||
// Feedback
|
||||
'color.feedback.info': { $type: 'color' as const, $value: '{color.blue.600}' },
|
||||
'color.feedback.success': { $type: 'color' as const, $value: '{color.green.600}' },
|
||||
'color.feedback.warning': { $type: 'color' as const, $value: '{color.amber.600}' },
|
||||
'color.feedback.error': { $type: 'color' as const, $value: '{color.red.600}' },
|
||||
|
||||
// Finance
|
||||
'color.finance.positive': { $type: 'color' as const, $value: '{color.green.700}' },
|
||||
'color.finance.negative': { $type: 'color' as const, $value: '{color.red.700}' },
|
||||
'color.finance.neutral': { $type: 'color' as const, $value: '{color.neutral.600}' },
|
||||
|
||||
// Typography
|
||||
'font.family.body': { $type: 'fontFamily' as const, $value: '{font.family.sans}' },
|
||||
'font.weight.body': { $type: 'fontWeight' as const, $value: '{font.weight.regular}' },
|
||||
'font.weight.heading': { $type: 'fontWeight' as const, $value: '{font.weight.semibold}' },
|
||||
'font.weight.strong': { $type: 'fontWeight' as const, $value: '{font.weight.bold}' },
|
||||
// Spacing
|
||||
'space.inset.sm': { $type: 'dimension' as const, $value: '{space.2}' },
|
||||
'space.inset.md': { $type: 'dimension' as const, $value: '{space.3}' },
|
||||
'space.inset.lg': { $type: 'dimension' as const, $value: '{space.4}' },
|
||||
'space.stack.sm': { $type: 'dimension' as const, $value: '{space.1}' },
|
||||
'space.stack.md': { $type: 'dimension' as const, $value: '{space.2}' },
|
||||
'space.stack.lg': { $type: 'dimension' as const, $value: '{space.4}' },
|
||||
'space.inline.sm': { $type: 'dimension' as const, $value: '{space.1}' },
|
||||
'space.inline.md': { $type: 'dimension' as const, $value: '{space.2}' },
|
||||
'space.inline.lg': { $type: 'dimension' as const, $value: '{space.3}' },
|
||||
|
||||
// Focus ring
|
||||
'focus.ring.width': { $type: 'dimension' as const, $value: '{size.focus.ring}' },
|
||||
'focus.ring.offset': { $type: 'dimension' as const, $value: '{size.focus.offset}' },
|
||||
'focus.ring.color': { $type: 'color' as const, $value: '{color.blue.600}' },
|
||||
} as const satisfies TokenCollection;
|
||||
72
packages/design-system/src/tokens/tokens.test.ts
Normal file
72
packages/design-system/src/tokens/tokens.test.ts
Normal file
@ -0,0 +1,72 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { primitiveTokens } from './primitives';
|
||||
import { semanticLightTokens } from './semantic.light';
|
||||
import { componentTokens } from './components';
|
||||
import { resolveToken } from './resolveToken';
|
||||
|
||||
const aliasPattern = /^\{([^}]+)\}$/;
|
||||
const isAlias = (value: string | number | readonly number[] | readonly string[]): boolean =>
|
||||
typeof value === 'string' && aliasPattern.test(value);
|
||||
|
||||
const allTokens = {
|
||||
...primitiveTokens,
|
||||
...semanticLightTokens,
|
||||
...componentTokens,
|
||||
} as const;
|
||||
|
||||
describe('token integrity', () => {
|
||||
it('primitives contain no aliases', () => {
|
||||
for (const [name, def] of Object.entries(primitiveTokens)) {
|
||||
expect(isAlias(def.$value), `${name} should not be an alias`).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it('semantic tokens only use aliases', () => {
|
||||
for (const [name, def] of Object.entries(semanticLightTokens)) {
|
||||
expect(isAlias(def.$value), `${name} should be an alias`).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('component tokens only use aliases', () => {
|
||||
for (const [name, def] of Object.entries(componentTokens)) {
|
||||
expect(isAlias(def.$value), `${name} should be an alias`).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('all tokens resolve without errors', () => {
|
||||
for (const name of Object.keys(allTokens)) {
|
||||
expect(() => resolveToken(allTokens, name), `Failed to resolve ${name}`).not.toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it('has unique names across all collections', () => {
|
||||
const allNames = [
|
||||
...Object.keys(primitiveTokens),
|
||||
...Object.keys(semanticLightTokens),
|
||||
...Object.keys(componentTokens),
|
||||
];
|
||||
const uniqueNames = new Set(allNames);
|
||||
expect(uniqueNames.size).toBe(allNames.length);
|
||||
});
|
||||
|
||||
it('semantic tokens reference only primitive tokens', () => {
|
||||
const primitiveNames = new Set(Object.keys(primitiveTokens));
|
||||
for (const [name, def] of Object.entries(semanticLightTokens)) {
|
||||
if (typeof def.$value === 'string' && aliasPattern.test(def.$value)) {
|
||||
const target = def.$value.match(aliasPattern)![1];
|
||||
expect(primitiveNames.has(target), `${name} references non-primitive "${target}"`).toBe(
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('component tokens avoid direct primitive value literals', () => {
|
||||
const literalPattern = /(px|#|rgba|rgb)/;
|
||||
for (const [name, def] of Object.entries(componentTokens)) {
|
||||
if (typeof def.$value === 'string') {
|
||||
expect(literalPattern.test(def.$value), `${name} contains a literal value`).toBe(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user