@discourser/design-system 0.25.3 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/README.md +76 -73
  2. package/dist/{chunk-ZPECW4N2.js → chunk-4XOWPACJ.js} +257 -105
  3. package/dist/chunk-4XOWPACJ.js.map +1 -0
  4. package/dist/{chunk-QNCZYFUJ.cjs → chunk-AZ6QU2L2.cjs} +257 -105
  5. package/dist/chunk-AZ6QU2L2.cjs.map +1 -0
  6. package/dist/{chunk-TBLDQATQ.cjs → chunk-EBDNCZF6.cjs} +94 -54
  7. package/dist/chunk-EBDNCZF6.cjs.map +1 -0
  8. package/dist/{chunk-UHSL4N44.js → chunk-MAVUSE4F.js} +94 -55
  9. package/dist/chunk-MAVUSE4F.js.map +1 -0
  10. package/dist/components/Checkbox.d.ts +1 -1
  11. package/dist/components/Icons/LeftArrowIcon.d.ts +6 -0
  12. package/dist/components/Icons/LeftArrowIcon.d.ts.map +1 -0
  13. package/dist/components/Icons/RightArrowIcon.d.ts.map +1 -1
  14. package/dist/components/Icons/index.d.ts +1 -0
  15. package/dist/components/Icons/index.d.ts.map +1 -1
  16. package/dist/components/index.cjs +79 -75
  17. package/dist/components/index.d.ts +1 -0
  18. package/dist/components/index.d.ts.map +1 -1
  19. package/dist/components/index.js +1 -1
  20. package/dist/contracts/design-language.contract.d.ts +52 -18
  21. package/dist/contracts/design-language.contract.d.ts.map +1 -1
  22. package/dist/figma-codex.json +2 -2
  23. package/dist/index.cjs +83 -79
  24. package/dist/index.js +2 -2
  25. package/dist/languages/material3.language.d.ts.map +1 -1
  26. package/dist/languages/transform.d.ts +5 -5
  27. package/dist/languages/transform.d.ts.map +1 -1
  28. package/dist/preset/index.cjs +2 -2
  29. package/dist/preset/index.js +1 -1
  30. package/docs/component-catalog.md +469 -0
  31. package/docs/superpowers/plans/2026-04-03-component-catalog-pipeline.md +667 -0
  32. package/docs/token-name-mapping.json +614 -42
  33. package/docs/token-name-mapping.md +117 -29
  34. package/package.json +3 -2
  35. package/src/components/Icons/LeftArrowIcon.tsx +28 -0
  36. package/src/components/Icons/RightArrowIcon.tsx +7 -2
  37. package/src/components/Icons/index.ts +1 -0
  38. package/src/components/__tests__/AbsoluteCenter.test.tsx +31 -0
  39. package/src/components/__tests__/Divider.test.tsx +38 -0
  40. package/src/components/__tests__/Group.test.tsx +34 -0
  41. package/src/components/__tests__/Icon.test.tsx +31 -0
  42. package/src/components/__tests__/SettingsPopover.test.tsx +39 -0
  43. package/src/components/__tests__/StudioControls.test.tsx +59 -0
  44. package/src/components/__tests__/Toaster.test.tsx +24 -0
  45. package/src/components/index.ts +1 -0
  46. package/src/contracts/design-language.contract.ts +69 -20
  47. package/src/languages/material3.language.ts +249 -80
  48. package/src/languages/transform.ts +45 -48
  49. package/src/preset/__tests__/translation-token-accuracy.test.ts +13 -0
  50. package/src/stories/foundations/Colors.mdx +9 -1
  51. package/src/stories/foundations/Elevation.mdx +23 -17
  52. package/src/stories/foundations/TokenReference.stories.tsx +970 -0
  53. package/src/stories/foundations/TonalPaletteDerivation.stories.tsx +782 -0
  54. package/src/stories/foundations/Typography.mdx +125 -25
  55. package/dist/chunk-QNCZYFUJ.cjs.map +0 -1
  56. package/dist/chunk-TBLDQATQ.cjs.map +0 -1
  57. package/dist/chunk-UHSL4N44.js.map +0 -1
  58. package/dist/chunk-ZPECW4N2.js.map +0 -1
  59. package/docs/context-share/ELEVATION_FIX_PLAN.md +0 -903
  60. package/docs/context-share/fix-checkbox-radio-tokens.md +0 -145
  61. package/docs/context-share/icon-component-prompt.md +0 -154
@@ -0,0 +1,782 @@
1
+ /**
2
+ * Tonal Palette Derivation Map
3
+ *
4
+ * Shows every M3 tonal primitive — its Figma name, hex value,
5
+ * the CSS variable it would generate IF exposed as a Panda token,
6
+ * and which semantic role(s) it derives into.
7
+ *
8
+ * Key insight: tonal steps have NO direct Panda CSS tokens.
9
+ * They exist only as Figma Primitives variables and as the raw
10
+ * values behind semantic tokens. Use semantic tokens in code.
11
+ *
12
+ * Data sourced from src/languages/material3.language.ts
13
+ */
14
+
15
+ import type { Meta, StoryObj } from '@storybook/react-vite';
16
+ import type { CSSProperties } from 'react';
17
+
18
+ // ── Raw data sourced directly from material3.language.ts ─────────────────────
19
+
20
+ const PALETTES = {
21
+ primary: {
22
+ label: 'Primary',
23
+ description: 'TastyMakers green — brand identity, CTAs, active states',
24
+ steps: {
25
+ 0: '#000000',
26
+ 10: '#102000',
27
+ 20: '#1F3700',
28
+ 30: '#2F4F00',
29
+ 40: '#3F6900',
30
+ 50: '#518500',
31
+ 60: '#64A104',
32
+ 70: '#7DBD2A',
33
+ 80: '#97D945',
34
+ 90: '#B2F65F',
35
+ 95: '#D2FF9B',
36
+ 99: '#F9FFE9',
37
+ 100: '#FFFFFF',
38
+ },
39
+ },
40
+ secondary: {
41
+ label: 'Secondary',
42
+ description:
43
+ 'Muted olive green — supporting actions, less prominent elements',
44
+ steps: {
45
+ 0: '#000000',
46
+ 10: '#121F04',
47
+ 20: '#263515',
48
+ 30: '#3C4C2A',
49
+ 40: '#54643F',
50
+ 50: '#6C7D56',
51
+ 60: '#85976E',
52
+ 70: '#A0B187',
53
+ 80: '#BBCDA1',
54
+ 90: '#D7E9BB',
55
+ 95: '#E5F7C9',
56
+ 99: '#F9FFE9',
57
+ 100: '#FFFFFF',
58
+ },
59
+ },
60
+ tertiary: {
61
+ label: 'Tertiary',
62
+ description:
63
+ 'Teal / cyan — accent color, visual contrast, complementary interest',
64
+ steps: {
65
+ 0: '#000000',
66
+ 10: '#00201E',
67
+ 20: '#003735',
68
+ 30: '#00504C',
69
+ 40: '#046A66',
70
+ 50: '#30837F',
71
+ 60: '#4D9D98',
72
+ 70: '#69B8B3',
73
+ 80: '#85D4CF',
74
+ 90: '#A1F1EB',
75
+ 95: '#B0FFF9',
76
+ 99: '#F2FFFD',
77
+ 100: '#FFFFFF',
78
+ },
79
+ },
80
+ neutral: {
81
+ label: 'Neutral',
82
+ description: 'Warm gray — all surface, background, and primary text tokens',
83
+ steps: {
84
+ 0: '#000000',
85
+ 10: '#1B1C18',
86
+ 20: '#30312C',
87
+ 30: '#464742',
88
+ 40: '#5E5F59',
89
+ 50: '#777771',
90
+ 60: '#91918B',
91
+ 70: '#ABACA5',
92
+ 80: '#C7C7C0',
93
+ 90: '#E3E3DB',
94
+ 95: '#F2F1E9',
95
+ 99: '#FDFCF5',
96
+ 100: '#FFFFFF',
97
+ },
98
+ },
99
+ neutralVariant: {
100
+ label: 'Neutral Variant',
101
+ description:
102
+ 'Slightly warmer gray — chip backgrounds, secondary borders, muted text',
103
+ steps: {
104
+ 0: '#000000',
105
+ 10: '#191D14',
106
+ 20: '#2E3228',
107
+ 30: '#44483D',
108
+ 40: '#5C6054',
109
+ 50: '#75796C',
110
+ 60: '#8F9285',
111
+ 70: '#A9AD9F',
112
+ 80: '#C5C8BA',
113
+ 90: '#E1E4D5',
114
+ 95: '#EFF2E3',
115
+ 99: '#FBFEEE',
116
+ 100: '#FFFFFF',
117
+ },
118
+ },
119
+ error: {
120
+ label: 'Error',
121
+ description:
122
+ 'Red — destructive actions, validation failures, danger states',
123
+ steps: {
124
+ 0: '#000000',
125
+ 10: '#410E0B',
126
+ 20: '#601410',
127
+ 30: '#8C1D18',
128
+ 40: '#B3261E',
129
+ 50: '#DC362E',
130
+ 60: '#E46962',
131
+ 70: '#EC928E',
132
+ 80: '#F2B8B5',
133
+ 90: '#F9DEDC',
134
+ 95: '#FCEEEE',
135
+ 99: '#FFFBF9',
136
+ 100: '#FFFFFF',
137
+ },
138
+ },
139
+ } as const;
140
+
141
+ // ── Derivation map: (palette, step) → semantic Panda CSS token(s) ─────────────
142
+ // Sourced from src/preset/semantic-tokens.ts + M3 spec derivation
143
+
144
+ const DERIVATION: Record<string, string[]> = {
145
+ // Primary
146
+ 'primary/10': ['onPrimary.container'],
147
+ 'primary/40': ['primary'],
148
+ 'primary/80': ['inversePrimary'],
149
+ 'primary/90': ['primary.container'],
150
+ 'primary/100': ['onPrimary'],
151
+ // Secondary
152
+ 'secondary/10': ['onSecondary.container'],
153
+ 'secondary/40': ['secondary'],
154
+ 'secondary/90': ['secondary.container'],
155
+ 'secondary/100': ['onSecondary'],
156
+ // Tertiary
157
+ 'tertiary/10': ['onTertiary.container'],
158
+ 'tertiary/40': ['tertiary'],
159
+ 'tertiary/90': ['tertiary.container'],
160
+ 'tertiary/100': ['onTertiary'],
161
+ // Error
162
+ 'error/10': ['onError.container'],
163
+ 'error/40': ['error'],
164
+ 'error/90': ['error.container'],
165
+ 'error/100': ['onError'],
166
+ // Neutral → surface system
167
+ 'neutral/0': ['scrim', 'shadow'],
168
+ 'neutral/10': ['onSurface', 'onBackground'],
169
+ 'neutral/20': ['inverseSurface'],
170
+ 'neutral/90': ['surface.container'],
171
+ 'neutral/95': ['surface.container.low', 'inverseOnSurface'],
172
+ 'neutral/99': ['surface', 'background'],
173
+ 'neutral/100': ['surface.container.lowest'],
174
+ // NeutralVariant → surface variant + outline
175
+ 'neutralVariant/30': ['onSurface.variant'],
176
+ 'neutralVariant/50': ['outline'],
177
+ 'neutralVariant/80': ['outline.variant'],
178
+ 'neutralVariant/90': ['surfaceVariant'],
179
+ };
180
+
181
+ // ── Helper: luminance for readable text color over swatch ───────────────────
182
+
183
+ function isLight(hex: string): boolean {
184
+ const r = parseInt(hex.slice(1, 3), 16);
185
+ const g = parseInt(hex.slice(3, 5), 16);
186
+ const b = parseInt(hex.slice(5, 7), 16);
187
+ return r * 0.299 + g * 0.587 + b * 0.114 > 128;
188
+ }
189
+
190
+ // ── Shared styles ─────────────────────────────────────────────────────────────
191
+
192
+ const S: Record<string, CSSProperties> = {
193
+ page: {
194
+ fontFamily: 'Inter, -apple-system, BlinkMacSystemFont, sans-serif',
195
+ fontSize: '13px',
196
+ color: '#1a1a1a',
197
+ maxWidth: '1200px',
198
+ },
199
+ header: {
200
+ marginBottom: '40px',
201
+ },
202
+ h1: {
203
+ fontSize: '22px',
204
+ fontWeight: '700',
205
+ margin: '0 0 8px',
206
+ color: '#111',
207
+ },
208
+ subtitle: {
209
+ fontSize: '13px',
210
+ color: '#666',
211
+ margin: '0 0 24px',
212
+ lineHeight: 1.6,
213
+ maxWidth: '700px',
214
+ },
215
+ sectionBlock: {
216
+ marginBottom: '48px',
217
+ },
218
+ sectionHeader: {
219
+ display: 'flex',
220
+ alignItems: 'baseline',
221
+ gap: '12px',
222
+ marginBottom: '4px',
223
+ },
224
+ h2: {
225
+ fontSize: '17px',
226
+ fontWeight: '700',
227
+ margin: '0',
228
+ color: '#111',
229
+ },
230
+ description: {
231
+ fontSize: '12px',
232
+ color: '#777',
233
+ margin: '0 0 12px',
234
+ lineHeight: 1.5,
235
+ },
236
+ table: {
237
+ width: '100%',
238
+ borderCollapse: 'collapse' as const,
239
+ fontSize: '12px',
240
+ tableLayout: 'fixed' as const,
241
+ },
242
+ th: {
243
+ textAlign: 'left' as const,
244
+ padding: '7px 10px',
245
+ background: '#f3f3f3',
246
+ borderBottom: '2px solid #ddd',
247
+ fontWeight: '600',
248
+ fontSize: '11px',
249
+ color: '#444',
250
+ whiteSpace: 'nowrap' as const,
251
+ },
252
+ td: {
253
+ padding: '0',
254
+ borderBottom: '1px solid #eee',
255
+ verticalAlign: 'middle' as const,
256
+ },
257
+ mono: {
258
+ fontFamily: 'monospace',
259
+ fontSize: '11px',
260
+ },
261
+ pill: {
262
+ display: 'inline-flex' as const,
263
+ alignItems: 'center',
264
+ fontSize: '10px',
265
+ fontWeight: '600',
266
+ padding: '2px 7px',
267
+ borderRadius: '10px',
268
+ whiteSpace: 'nowrap' as const,
269
+ },
270
+ };
271
+
272
+ function cellPad(extra?: CSSProperties): CSSProperties {
273
+ return { padding: '6px 10px', ...extra };
274
+ }
275
+
276
+ // Semantic token pill — green if it has a semantic role
277
+ function SemanticPill({ token }: { token: string }) {
278
+ const isContainer = token.includes('container');
279
+ const isInverse = token.includes('inverse') || token.includes('Inverse');
280
+ const isOutline = token.includes('outline') || token.includes('Outline');
281
+ const isSurface = token.includes('surface') || token.includes('Surface');
282
+
283
+ let bg = '#e8f4e8';
284
+ let color = '#2a6a2a';
285
+
286
+ if (isInverse) {
287
+ bg = '#f3e8ff';
288
+ color = '#6a2a8a';
289
+ } else if (isContainer) {
290
+ bg = '#e8f0ff';
291
+ color = '#2a4a8a';
292
+ } else if (isOutline) {
293
+ bg = '#fff3e0';
294
+ color = '#8a5a00';
295
+ } else if (isSurface) {
296
+ bg = '#f0f0f0';
297
+ color = '#444444';
298
+ }
299
+
300
+ return (
301
+ <span
302
+ style={{
303
+ ...S.pill,
304
+ background: bg,
305
+ color,
306
+ marginRight: '3px',
307
+ marginBottom: '2px',
308
+ }}
309
+ >
310
+ {token}
311
+ </span>
312
+ );
313
+ }
314
+
315
+ // Color swatch cell
316
+ function SwatchCell({ hex, step }: { hex: string; step: number }) {
317
+ const light = isLight(hex);
318
+ return (
319
+ <td style={{ ...S.td, width: '72px' }}>
320
+ <div
321
+ style={{
322
+ background: hex,
323
+ width: '100%',
324
+ height: '40px',
325
+ display: 'flex',
326
+ alignItems: 'center',
327
+ justifyContent: 'center',
328
+ }}
329
+ >
330
+ <span
331
+ style={{
332
+ ...S.mono,
333
+ fontSize: '10px',
334
+ color: light ? 'rgba(0,0,0,0.55)' : 'rgba(255,255,255,0.7)',
335
+ fontWeight: '600',
336
+ }}
337
+ >
338
+ {step}
339
+ </span>
340
+ </div>
341
+ </td>
342
+ );
343
+ }
344
+
345
+ // ── Main story component ──────────────────────────────────────────────────────
346
+
347
+ function DerivationTable({
348
+ paletteKey,
349
+ }: {
350
+ paletteKey: keyof typeof PALETTES;
351
+ }) {
352
+ const palette = PALETTES[paletteKey];
353
+ const steps = Object.entries(palette.steps) as [string, string][];
354
+
355
+ return (
356
+ <div style={S.sectionBlock}>
357
+ <div style={S.sectionHeader}>
358
+ <h2 style={S.h2}>{palette.label}</h2>
359
+ <span style={{ ...S.mono, fontSize: '11px', color: '#999' }}>
360
+ {paletteKey}
361
+ </span>
362
+ </div>
363
+ <p style={S.description}>{palette.description}</p>
364
+
365
+ <table style={S.table}>
366
+ <colgroup>
367
+ <col style={{ width: '72px' }} /> {/* swatch */}
368
+ <col style={{ width: '160px' }} /> {/* figma */}
369
+ <col style={{ width: '90px' }} /> {/* step */}
370
+ <col style={{ width: '90px' }} /> {/* hex */}
371
+ <col style={{ width: '240px' }} /> {/* css var */}
372
+ <col /> {/* semantic */}
373
+ </colgroup>
374
+ <thead>
375
+ <tr>
376
+ <th style={{ ...S.th, width: '72px' }}></th>
377
+ <th style={S.th}>Figma Variable</th>
378
+ <th style={S.th}>Step</th>
379
+ <th style={S.th}>Hex</th>
380
+ <th style={S.th}>Panda CSS Note</th>
381
+ <th style={S.th}>Semantic Role(s)</th>
382
+ </tr>
383
+ </thead>
384
+ <tbody>
385
+ {steps.map(([stepStr, hex]) => {
386
+ const step = parseInt(stepStr);
387
+ const figmaName = `${paletteKey}/${step}`;
388
+ const semanticRoles = DERIVATION[figmaName] ?? [];
389
+ const hasRole = semanticRoles.length > 0;
390
+
391
+ // Highlight rows that have semantic roles
392
+ const rowBg = hasRole ? 'rgba(76,102,43,0.04)' : undefined;
393
+
394
+ return (
395
+ <tr key={step} style={{ background: rowBg }}>
396
+ <SwatchCell hex={hex} step={step} />
397
+
398
+ {/* Figma variable name */}
399
+ <td style={S.td}>
400
+ <span style={{ ...cellPad(), ...S.mono, display: 'block' }}>
401
+ {figmaName}
402
+ </span>
403
+ </td>
404
+
405
+ {/* Step number */}
406
+ <td style={S.td}>
407
+ <span style={{ ...cellPad(), ...S.mono, display: 'block' }}>
408
+ {step}
409
+ </span>
410
+ </td>
411
+
412
+ {/* Hex */}
413
+ <td style={S.td}>
414
+ <div
415
+ style={{
416
+ ...cellPad(),
417
+ display: 'flex',
418
+ alignItems: 'center',
419
+ gap: '6px',
420
+ }}
421
+ >
422
+ <div
423
+ style={{
424
+ width: '12px',
425
+ height: '12px',
426
+ borderRadius: '2px',
427
+ background: hex,
428
+ border: '1px solid rgba(0,0,0,0.15)',
429
+ flexShrink: 0,
430
+ }}
431
+ />
432
+ <span style={S.mono}>{hex}</span>
433
+ </div>
434
+ </td>
435
+
436
+ {/* Panda CSS note */}
437
+ <td style={S.td}>
438
+ <span
439
+ style={{
440
+ ...cellPad(),
441
+ display: 'block',
442
+ fontSize: '11px',
443
+ color: '#999',
444
+ fontStyle: 'italic',
445
+ }}
446
+ >
447
+ No direct Panda token —{' '}
448
+ <span style={{ ...S.mono, fontStyle: 'normal' }}>
449
+ --colors-{paletteKey}-{step}
450
+ </span>{' '}
451
+ (Figma Primitives only)
452
+ </span>
453
+ </td>
454
+
455
+ {/* Semantic roles */}
456
+ <td style={S.td}>
457
+ <div
458
+ style={{
459
+ ...cellPad(),
460
+ display: 'flex',
461
+ flexWrap: 'wrap',
462
+ gap: '2px',
463
+ }}
464
+ >
465
+ {hasRole ? (
466
+ semanticRoles.map((role) => (
467
+ <SemanticPill key={role} token={role} />
468
+ ))
469
+ ) : (
470
+ <span style={{ color: '#ccc', fontSize: '11px' }}>
471
+ — no semantic mapping
472
+ </span>
473
+ )}
474
+ </div>
475
+ </td>
476
+ </tr>
477
+ );
478
+ })}
479
+ </tbody>
480
+ </table>
481
+ </div>
482
+ );
483
+ }
484
+
485
+ // ── Storybook meta ────────────────────────────────────────────────────────────
486
+
487
+ const meta = {
488
+ title: 'Foundations/Tonal Palette Derivation',
489
+ parameters: { layout: 'padded' },
490
+ } satisfies Meta;
491
+
492
+ export default meta;
493
+ type Story = StoryObj<typeof meta>;
494
+
495
+ // ── Story 1: Full derivation map ─────────────────────────────────────────────
496
+
497
+ export const FullDerivationMap: Story = {
498
+ name: '1 · Full Derivation Map',
499
+ render: () => (
500
+ <div style={S.page}>
501
+ <div style={S.header}>
502
+ <h1 style={S.h1}>Tonal Palette → Semantic Token Derivation Map</h1>
503
+ <p style={S.subtitle}>
504
+ Every M3 tonal primitive across all six palettes, showing which
505
+ semantic Panda CSS token each step derives into.{' '}
506
+ <strong>
507
+ Highlighted rows have a semantic equivalent — use that token
508
+ instead.
509
+ </strong>
510
+ <br />
511
+ Tonal steps have no direct Panda CSS tokens — they exist only as Figma
512
+ Primitives variables. The CSS variable column shows what they{' '}
513
+ <em>would</em> be named if exposed, for reference when auditing
514
+ existing code that uses them.
515
+ </p>
516
+
517
+ {/* Legend */}
518
+ <div
519
+ style={{
520
+ display: 'flex',
521
+ gap: '24px',
522
+ flexWrap: 'wrap',
523
+ padding: '12px 16px',
524
+ background: '#f8f8f8',
525
+ borderRadius: '6px',
526
+ fontSize: '12px',
527
+ marginBottom: '8px',
528
+ }}
529
+ >
530
+ <span style={{ fontWeight: '600', color: '#444' }}>
531
+ Semantic role colours:
532
+ </span>
533
+ <span>
534
+ <span
535
+ style={{ ...S.pill, background: '#e8f4e8', color: '#2a6a2a' }}
536
+ >
537
+ primary role
538
+ </span>
539
+ </span>
540
+ <span>
541
+ <span
542
+ style={{ ...S.pill, background: '#e8f0ff', color: '#2a4a8a' }}
543
+ >
544
+ container role
545
+ </span>
546
+ </span>
547
+ <span>
548
+ <span
549
+ style={{ ...S.pill, background: '#f3e8ff', color: '#6a2a8a' }}
550
+ >
551
+ inverse role
552
+ </span>
553
+ </span>
554
+ <span>
555
+ <span
556
+ style={{ ...S.pill, background: '#fff3e0', color: '#8a5a00' }}
557
+ >
558
+ outline role
559
+ </span>
560
+ </span>
561
+ <span>
562
+ <span
563
+ style={{ ...S.pill, background: '#f0f0f0', color: '#444444' }}
564
+ >
565
+ surface role
566
+ </span>
567
+ </span>
568
+ <span style={{ color: '#ccc' }}>
569
+ — no semantic mapping (raw primitive only)
570
+ </span>
571
+ </div>
572
+ </div>
573
+
574
+ {(Object.keys(PALETTES) as (keyof typeof PALETTES)[]).map((key) => (
575
+ <DerivationTable key={key} paletteKey={key} />
576
+ ))}
577
+ </div>
578
+ ),
579
+ };
580
+
581
+ // ── Story 2: Migration guide — which primitives have semantic equivalents ─────
582
+
583
+ export const MigrationGuide: Story = {
584
+ name: '2 · Migration Guide',
585
+ render: () => {
586
+ // Flatten all steps that have semantic roles
587
+ const mapped: Array<{
588
+ figmaName: string;
589
+ hex: string;
590
+ semanticTokens: string[];
591
+ migrate: string;
592
+ note?: string;
593
+ }> = [];
594
+
595
+ for (const [paletteKey, palette] of Object.entries(PALETTES)) {
596
+ for (const [stepStr, hex] of Object.entries(palette.steps)) {
597
+ const step = parseInt(stepStr);
598
+ const figmaName = `${paletteKey}/${step}`;
599
+ const semanticRoles = DERIVATION[figmaName];
600
+ if (semanticRoles && semanticRoles.length > 0) {
601
+ // Pick the most useful token for migration guidance
602
+ const primary = semanticRoles[0];
603
+ mapped.push({
604
+ figmaName,
605
+ hex,
606
+ semanticTokens: semanticRoles,
607
+ migrate: primary,
608
+ });
609
+ }
610
+ }
611
+ }
612
+
613
+ // Special case: neutral/20 gets a note
614
+ const neutral20 = mapped.find((r) => r.figmaName === 'neutral/20');
615
+ if (neutral20) {
616
+ neutral20.note =
617
+ 'inverseSurface is its semantic role, but its intent is tooltip/snackbar bg. ' +
618
+ 'For dark text use: consider adding fg.strong (onSurface is darker at neutral/10).';
619
+ }
620
+
621
+ return (
622
+ <div style={S.page}>
623
+ <div style={S.header}>
624
+ <h1 style={S.h1}>
625
+ Migration Guide — Primitives with Semantic Equivalents
626
+ </h1>
627
+ <p style={S.subtitle}>
628
+ If you are using any of these tonal step values directly in Figma or
629
+ in code, switch to the semantic token in the third column. Semantic
630
+ tokens are theme-aware and switch automatically in dark mode.
631
+ <br />
632
+ Steps with <em>no semantic mapping</em> are not listed — those have
633
+ no current equivalent and require a custom token or can remain
634
+ as-is.
635
+ </p>
636
+ </div>
637
+
638
+ <table style={{ ...S.table, tableLayout: 'auto' as const }}>
639
+ <thead>
640
+ <tr>
641
+ <th style={S.th}>Swatch</th>
642
+ <th style={S.th}>Figma Primitive</th>
643
+ <th style={S.th}>Hex</th>
644
+ <th style={S.th}>→ Use This Semantic Token</th>
645
+ <th style={S.th}>In Code</th>
646
+ <th style={S.th}>Notes</th>
647
+ </tr>
648
+ </thead>
649
+ <tbody>
650
+ {mapped.map((row, i) => (
651
+ <tr
652
+ key={row.figmaName}
653
+ style={{ background: i % 2 === 0 ? '#fff' : '#fafafa' }}
654
+ >
655
+ {/* Swatch */}
656
+ <td style={{ ...S.td, width: '40px' }}>
657
+ <div
658
+ style={{
659
+ width: '36px',
660
+ height: '36px',
661
+ background: row.hex,
662
+ margin: '4px 6px',
663
+ borderRadius: '4px',
664
+ border: '1px solid rgba(0,0,0,0.1)',
665
+ }}
666
+ />
667
+ </td>
668
+
669
+ {/* Figma name */}
670
+ <td style={S.td}>
671
+ <span style={{ ...cellPad(), ...S.mono, display: 'block' }}>
672
+ {row.figmaName}
673
+ </span>
674
+ </td>
675
+
676
+ {/* Hex */}
677
+ <td style={S.td}>
678
+ <span style={{ ...cellPad(), ...S.mono, display: 'block' }}>
679
+ {row.hex}
680
+ </span>
681
+ </td>
682
+
683
+ {/* Semantic token(s) */}
684
+ <td style={S.td}>
685
+ <div
686
+ style={{
687
+ ...cellPad(),
688
+ display: 'flex',
689
+ flexWrap: 'wrap',
690
+ gap: '3px',
691
+ }}
692
+ >
693
+ {row.semanticTokens.map((t) => (
694
+ <SemanticPill key={t} token={t} />
695
+ ))}
696
+ </div>
697
+ </td>
698
+
699
+ {/* Code example */}
700
+ <td style={S.td}>
701
+ <span
702
+ style={{
703
+ ...cellPad(),
704
+ ...S.mono,
705
+ display: 'block',
706
+ background: '#eef4e8',
707
+ borderRadius: '3px',
708
+ fontSize: '11px',
709
+ color: '#2a6a2a',
710
+ }}
711
+ >
712
+ {/* Guess the right prop based on token name */}
713
+ {row.migrate.startsWith('on') ||
714
+ row.migrate.includes('fg') ||
715
+ row.migrate.includes('text')
716
+ ? `color="${row.migrate}"`
717
+ : row.migrate.includes('border') ||
718
+ row.migrate.includes('outline')
719
+ ? `borderColor="${row.migrate}"`
720
+ : `bg="${row.migrate}"`}
721
+ </span>
722
+ </td>
723
+
724
+ {/* Notes */}
725
+ <td
726
+ style={{
727
+ ...S.td,
728
+ fontSize: '11px',
729
+ color: '#888',
730
+ lineHeight: 1.5,
731
+ }}
732
+ >
733
+ <span style={cellPad()}>
734
+ {row.note ? (
735
+ <span style={{ color: '#c07000' }}>⚠ {row.note}</span>
736
+ ) : row.semanticTokens.length > 1 ? (
737
+ `Also used for: ${row.semanticTokens.slice(1).join(', ')}`
738
+ ) : (
739
+ ''
740
+ )}
741
+ </span>
742
+ </td>
743
+ </tr>
744
+ ))}
745
+ </tbody>
746
+ </table>
747
+
748
+ {/* Unmapped steps summary */}
749
+ <div
750
+ style={{
751
+ marginTop: '40px',
752
+ padding: '16px 20px',
753
+ background: '#f8f8f8',
754
+ borderRadius: '8px',
755
+ fontSize: '12px',
756
+ color: '#555',
757
+ lineHeight: 1.7,
758
+ }}
759
+ >
760
+ <strong>Steps with no semantic mapping</strong> — these have no
761
+ current DDS semantic equivalent. If you are using them, either keep
762
+ them as tonal step values (accepting no dark mode support) or add a
763
+ custom semantic token to <code>src/preset/semantic-tokens.ts</code>.
764
+ <br />
765
+ <br />
766
+ <strong>neutral/20</strong> (<code>#30312C</code>) — your most-used
767
+ unmapped step. Its M3 semantic role is <code>inverseSurface</code>{' '}
768
+ (tooltip bg) which is not the right intent for text. Closest available
769
+ alternatives:
770
+ <br />
771
+ &nbsp;&nbsp;• <code>onSurface</code> (<code>#1A1C16</code>,
772
+ neutral/10) — slightly darker, primary text intent ✓<br />
773
+ &nbsp;&nbsp;• <code>onSurface.variant</code> (<code>#44483D</code>,
774
+ neutralVariant/30) — slightly lighter, secondary text intent ✓<br />
775
+ &nbsp;&nbsp;• Add <code>fg.strong</code> to semantic-tokens.ts mapping
776
+ neutral/20 → neutral/80 for dark mode — recommended if you need this
777
+ exact shade with dark mode support
778
+ </div>
779
+ </div>
780
+ );
781
+ },
782
+ };