Skip to content

Commit 4444b58

Browse files
authored
feat(tokens): add seed-driven token foundations (#128)
* fix: unify the token key * feat(tokens): add seed-driven token foundations Introduce an internal primitive seed layer (brand, surface, typography, size, effects) and rewrite semantic tokens to reference seeds via {seed.*}. Enforce the primitive -> semantic -> component flow in both build-time validation and theme-runtime checks. Share a single compileBrandTheme via the new @tiny-design/tokens/compile-brand-theme export, generated from a source file with staleness verification. Remove the duplicated compile logic from the docs theme studio and rework its sidebar around seed groups. Add resolvedValue to the registry so downstream tooling (extract/MCP) can surface concrete default values while defaultValue stays the unresolved source reference. * chore: add changeset for seed-driven token foundations
1 parent 06330e0 commit 4444b58

53 files changed

Lines changed: 3217 additions & 843 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/fair-rules-heal.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@tiny-design/tokens': minor
3+
'@tiny-design/mcp': patch
4+
---
5+
6+
Add seed-driven token foundations to `@tiny-design/tokens` with an internal `primitive -> semantic -> component` model, a shared `compile-brand-theme` runtime export, stricter build validation, and richer registry metadata including resolved token values for downstream tooling.
7+
8+
Fix `@tiny-design/mcp` token extraction so MCP clients receive concrete resolved token values instead of unresolved token references.

apps/docs/src/containers/theme-studio/defaults.ts

Lines changed: 2 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,7 @@
1+
import { DEFAULT_BRAND_SEED_FIELDS } from '@tiny-design/tokens/compile-brand-theme';
12
import type { ThemeEditorDraft, ThemeEditorFields, ThemeMode } from './types';
23

3-
const DEFAULT_FONT_SANS = '"Instrument Sans", "Inter", system-ui, sans-serif';
4-
const DEFAULT_FONT_MONO = '"JetBrains Mono", "SFMono-Regular", monospace';
5-
6-
export const DEFAULT_FIELDS: ThemeEditorFields = {
7-
primary: '#6e41bf',
8-
primaryForeground: '#ffffff',
9-
secondary: '#f5f5f7',
10-
secondaryForeground: '#16181d',
11-
accent: '#f3eefa',
12-
accentForeground: '#6e41bf',
13-
success: '#52c41a',
14-
successForeground: '#ffffff',
15-
info: '#1890ff',
16-
infoForeground: '#ffffff',
17-
warning: '#fa8c16',
18-
warningForeground: '#ffffff',
19-
danger: '#dc2626',
20-
dangerForeground: '#ffffff',
21-
base: '#ffffff',
22-
baseForeground: 'rgba(0, 0, 0, 0.85)',
23-
card: '#ffffff',
24-
cardForeground: '#111827',
25-
popover: '#ffffff',
26-
popoverForeground: '#111827',
27-
muted: '#f5f5f5',
28-
mutedForeground: 'rgba(0, 0, 0, 0.55)',
29-
border: '#d9d9d9',
30-
input: '#d9d9d9',
31-
ring: '#6e41bf',
32-
chart1: '#6e41bf',
33-
chart2: '#1890ff',
34-
chart3: '#52c41a',
35-
chart4: '#fa8c16',
36-
chart5: '#eb2f96',
37-
sidebar: '#12131a',
38-
sidebarForeground: '#f8fafc',
39-
sidebarPrimary: '#6e41bf',
40-
sidebarPrimaryForeground: '#ffffff',
41-
sidebarAccent: '#23173f',
42-
sidebarAccentForeground: '#efe8ff',
43-
sidebarBorder: '#2a2d36',
44-
sidebarRing: '#8b62d0',
45-
fontSans: DEFAULT_FONT_SANS,
46-
fontMono: DEFAULT_FONT_MONO,
47-
fontSizeBase: '14px',
48-
lineHeightBase: '1.5',
49-
h1Size: '40px',
50-
h2Size: '32px',
51-
letterSpacing: '-0.02em',
52-
radius: '0.3rem',
53-
shadowControl: 'none',
54-
shadowCard: '0 20px 55px rgba(17, 24, 39, 0.08)',
55-
shadowFocus: '0 0 0 3px rgba(110, 65, 191, 0.22)',
56-
buttonRadius: '0.3rem',
57-
inputRadius: '0.3rem',
58-
cardRadius: '0.3rem',
59-
fieldPaddingSm: '8px',
60-
fieldPaddingMd: '8px',
61-
fieldPaddingLg: '8px',
62-
buttonPaddingSm: '8px',
63-
buttonPaddingMd: '15px',
64-
buttonPaddingLg: '20px',
65-
fieldHeightSm: '24px',
66-
fieldHeightMd: '35px',
67-
fieldHeightLg: '44px',
68-
buttonHeightSm: '24px',
69-
buttonHeightMd: '35px',
70-
buttonHeightLg: '44px',
71-
cardPadding: '18px',
72-
};
4+
export const DEFAULT_FIELDS: ThemeEditorFields = { ...DEFAULT_BRAND_SEED_FIELDS };
735

746
export function createDraft(
757
id: string,

apps/docs/src/containers/theme-studio/editor-config.ts

Lines changed: 51 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,88 @@
1-
import type { FieldKey, ThemeEditorColorGroup } from './types';
1+
import type { ThemeEditorSeedGroup } from './types';
22

3-
export const COLOR_GROUPS: ThemeEditorColorGroup[] = [
3+
export const SEED_COLOR_GROUPS: ThemeEditorSeedGroup[] = [
44
{
5-
title: 'Primary',
5+
title: 'Brand Core',
6+
description: 'Primary, accent, and supporting brand surfaces.',
7+
tier: 'core',
68
fields: [
7-
{ key: 'primary', label: 'Background' },
8-
{ key: 'primaryForeground', label: 'Foreground' },
9+
{ key: 'primary', label: 'Primary' },
10+
{ key: 'primaryForeground', label: 'On Primary' },
11+
{ key: 'secondary', label: 'Secondary' },
12+
{ key: 'secondaryForeground', label: 'On Secondary' },
13+
{ key: 'accent', label: 'Accent' },
14+
{ key: 'accentForeground', label: 'On Accent' },
915
],
1016
},
1117
{
12-
title: 'Secondary',
13-
fields: [
14-
{ key: 'secondary', label: 'Background' },
15-
{ key: 'secondaryForeground', label: 'Foreground' },
16-
],
17-
},
18-
{
19-
title: 'Accent',
20-
fields: [
21-
{ key: 'accent', label: 'Background' },
22-
{ key: 'accentForeground', label: 'Foreground' },
23-
],
24-
},
25-
{
26-
title: 'Status',
18+
title: 'Feedback States',
19+
description: 'Semantic status hues that propagate through alerts, tags, and feedback UI.',
20+
tier: 'core',
2721
fields: [
2822
{ key: 'success', label: 'Success' },
29-
{ key: 'successForeground', label: 'Success FG' },
23+
{ key: 'successForeground', label: 'On Success' },
3024
{ key: 'info', label: 'Info' },
31-
{ key: 'infoForeground', label: 'Info FG' },
25+
{ key: 'infoForeground', label: 'On Info' },
3226
{ key: 'warning', label: 'Warning' },
33-
{ key: 'warningForeground', label: 'Warning FG' },
27+
{ key: 'warningForeground', label: 'On Warning' },
3428
{ key: 'danger', label: 'Danger' },
35-
{ key: 'dangerForeground', label: 'Danger FG' },
29+
{ key: 'dangerForeground', label: 'On Danger' },
3630
],
3731
},
3832
{
39-
title: 'Base',
33+
title: 'Surfaces & Text',
34+
description: 'Page, card, popover, and muted surfaces plus their text companions.',
35+
tier: 'core',
4036
fields: [
41-
{ key: 'base', label: 'Background' },
42-
{ key: 'baseForeground', label: 'Foreground' },
37+
{ key: 'base', label: 'Page' },
38+
{ key: 'baseForeground', label: 'On Page' },
39+
{ key: 'card', label: 'Card' },
40+
{ key: 'cardForeground', label: 'On Card' },
41+
{ key: 'popover', label: 'Popover' },
42+
{ key: 'popoverForeground', label: 'On Popover' },
43+
{ key: 'muted', label: 'Muted' },
44+
{ key: 'mutedForeground', label: 'On Muted' },
4345
],
4446
},
4547
{
46-
title: 'Card',
47-
fields: [
48-
{ key: 'card', label: 'Background' },
49-
{ key: 'cardForeground', label: 'Foreground' },
50-
],
51-
},
52-
{
53-
title: 'Popover',
54-
fields: [
55-
{ key: 'popover', label: 'Background' },
56-
{ key: 'popoverForeground', label: 'Foreground' },
57-
],
58-
},
59-
{
60-
title: 'Muted',
61-
fields: [
62-
{ key: 'muted', label: 'Background' },
63-
{ key: 'mutedForeground', label: 'Foreground' },
64-
],
65-
},
66-
{
67-
title: 'Border & Input',
48+
title: 'Lines & Focus',
49+
description: 'Borders, field chrome, and the interaction ring seed.',
50+
tier: 'core',
6851
fields: [
6952
{ key: 'border', label: 'Border' },
7053
{ key: 'input', label: 'Input' },
7154
{ key: 'ring', label: 'Ring' },
7255
],
7356
},
7457
{
75-
title: 'Chart',
58+
title: 'Data Visualization',
59+
description: 'Chart palette seeds for data-heavy views.',
60+
tier: 'advanced',
7661
fields: [
77-
{ key: 'chart1', label: 'Chart 1' },
78-
{ key: 'chart2', label: 'Chart 2' },
79-
{ key: 'chart3', label: 'Chart 3' },
80-
{ key: 'chart4', label: 'Chart 4' },
81-
{ key: 'chart5', label: 'Chart 5' },
62+
{ key: 'chart1', label: 'Series 1' },
63+
{ key: 'chart2', label: 'Series 2' },
64+
{ key: 'chart3', label: 'Series 3' },
65+
{ key: 'chart4', label: 'Series 4' },
66+
{ key: 'chart5', label: 'Series 5' },
8267
],
8368
},
8469
{
85-
title: 'Sidebar',
70+
title: 'Sidebar Shell',
71+
description: 'Dedicated app-shell seeds for navigation-heavy layouts.',
72+
tier: 'advanced',
8673
fields: [
87-
{ key: 'sidebar', label: 'Background' },
88-
{ key: 'sidebarForeground', label: 'Foreground' },
89-
{ key: 'sidebarPrimary', label: 'Primary' },
90-
{ key: 'sidebarPrimaryForeground', label: 'Primary FG' },
91-
{ key: 'sidebarAccent', label: 'Accent' },
92-
{ key: 'sidebarAccentForeground', label: 'Accent FG' },
93-
{ key: 'sidebarBorder', label: 'Border' },
94-
{ key: 'sidebarRing', label: 'Ring' },
74+
{ key: 'sidebar', label: 'Sidebar' },
75+
{ key: 'sidebarForeground', label: 'On Sidebar' },
76+
{ key: 'sidebarPrimary', label: 'Sidebar Primary' },
77+
{ key: 'sidebarPrimaryForeground', label: 'On Sidebar Primary' },
78+
{ key: 'sidebarAccent', label: 'Sidebar Accent' },
79+
{ key: 'sidebarAccentForeground', label: 'On Sidebar Accent' },
80+
{ key: 'sidebarBorder', label: 'Sidebar Border' },
81+
{ key: 'sidebarRing', label: 'Sidebar Ring' },
9582
],
9683
},
9784
];
9885

99-
export const CORE_COLOR_GROUP_TITLES = new Set([
100-
'Primary',
101-
'Secondary',
102-
'Accent',
103-
'Status',
104-
'Base',
105-
'Muted',
106-
'Border & Input',
107-
]);
108-
10986
export const FONT_OPTIONS = [
11087
'ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif',
11188
'"Instrument Sans", "Inter", system-ui, sans-serif',

apps/docs/src/containers/theme-studio/index.tsx

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,14 +46,28 @@ const ThemeStudioPage = (): React.ReactElement => {
4646
const [codeVisible, setCodeVisible] = useState(false);
4747
const [importText, setImportText] = useState('');
4848
const [importError, setImportError] = useState<string | null>(null);
49-
const [status, setStatus] = useState<string>('Editing local draft');
49+
const [status, setStatus] = useState<string>('Editing local seed draft');
5050
const globalMode: ThemeMode = resolvedTheme === 'dark' ? 'dark' : 'light';
5151

5252
useEffect(() => {
5353
localStorage.setItem(DRAFT_KEY, JSON.stringify(draft));
5454
}, [draft]);
5555

5656
const themeDocument = useMemo(() => buildThemeDocumentFromDraft(draft), [draft]);
57+
const seedJson = useMemo(
58+
() =>
59+
JSON.stringify(
60+
{
61+
meta: draft.meta,
62+
mode: draft.mode,
63+
presetId: draft.presetId,
64+
fields: draft.fields,
65+
},
66+
null,
67+
2
68+
),
69+
[draft]
70+
);
5771
const themeJson = useMemo(() => generateThemeDocumentJSON(themeDocument), [themeDocument]);
5872
const cssVars = useMemo(() => generateThemeCssVariables(themeDocument), [themeDocument]);
5973
const changedTokens = useMemo(() => compareThemeAgainstBase(themeDocument), [themeDocument]);
@@ -105,13 +119,13 @@ const ThemeStudioPage = (): React.ReactElement => {
105119

106120
const resetToPreset = () => {
107121
setDraft((current) => applyPresetToDraft(current.presetId, current));
108-
setStatus('Reset to preset defaults');
122+
setStatus('Reset seed draft to preset defaults');
109123
};
110124

111125
const handlePresetChange = (presetId: string) => {
112126
setDraft((current) => applyPresetToDraft(presetId, current));
113127
setStatus(
114-
`Applied ${THEME_EDITOR_PRESETS.find((preset) => preset.id === presetId)?.name ?? 'preset'}`
128+
`Applied ${THEME_EDITOR_PRESETS.find((preset) => preset.id === presetId)?.name ?? 'preset'} seed preset`
115129
);
116130
};
117131

@@ -129,8 +143,8 @@ const ThemeStudioPage = (): React.ReactElement => {
129143
setImportError(null);
130144
setStatus(
131145
validation.warnings.length > 0
132-
? 'Imported theme document with validation warnings'
133-
: 'Imported theme document'
146+
? 'Imported theme document and remapped it into seed groups with validation warnings'
147+
: 'Imported theme document and remapped it into seed groups'
134148
);
135149
} catch {
136150
setImportError('Invalid theme document JSON');
@@ -253,7 +267,8 @@ const ThemeStudioPage = (): React.ReactElement => {
253267
Paste a Tiny theme document JSON export to replace the current global theme.
254268
</Paragraph>
255269
<Text type="secondary">
256-
Preset selection and all editor controls will sync to the imported values.
270+
The studio will map semantic and component token overrides back into the seed groups
271+
shown in the editor.
257272
</Text>
258273
</div>
259274
<Textarea
@@ -275,7 +290,9 @@ const ThemeStudioPage = (): React.ReactElement => {
275290
bodyStyle={{ maxHeight: 'min(74vh, 760px)', overflow: 'auto' }}
276291
cancelText="Close"
277292
confirmText={
278-
draft.activeCodeView === 'json'
293+
draft.activeCodeView === 'seeds'
294+
? 'Copy Seed JSON'
295+
: draft.activeCodeView === 'json'
279296
? 'Copy JSON'
280297
: draft.activeCodeView === 'css'
281298
? 'Copy CSS'
@@ -285,7 +302,9 @@ const ThemeStudioPage = (): React.ReactElement => {
285302
draft.activeCodeView === 'tokens' ? { style: { display: 'none' } } : undefined
286303
}
287304
onConfirm={
288-
draft.activeCodeView === 'json'
305+
draft.activeCodeView === 'seeds'
306+
? () => handleCopy(seedJson, 'Seed JSON')
307+
: draft.activeCodeView === 'json'
289308
? () => handleCopy(themeJson, 'Theme JSON')
290309
: draft.activeCodeView === 'css'
291310
? () => handleCopy(cssVars, 'CSS variables')
@@ -296,7 +315,7 @@ const ThemeStudioPage = (): React.ReactElement => {
296315
<div>
297316
<Text strong>Output</Text>
298317
<Text type="secondary">
299-
{activePreset.name} · {status}
318+
{activePreset.name} · {status} · seed draft compiles to theme JSON and CSS vars
300319
</Text>
301320
</div>
302321
<Segmented
@@ -307,6 +326,9 @@ const ThemeStudioPage = (): React.ReactElement => {
307326
}
308327
/>
309328
</div>
329+
{draft.activeCodeView === 'seeds' ? (
330+
<pre className="theme-studio__code-block">{seedJson}</pre>
331+
) : null}
310332
{draft.activeCodeView === 'json' ? (
311333
<pre className="theme-studio__code-block">{themeJson}</pre>
312334
) : null}

0 commit comments

Comments
 (0)