codex/design-system-foundation #33
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