@motion-proto/live-tokens 0.94.3 → 0.95.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 (104) hide show
  1. package/.claude/skills/live-tokens-create-page/SKILL.md +7 -3
  2. package/CHANGELOG.md +97 -0
  3. package/README.md +6 -6
  4. package/dist-plugin/{chunk-7XSUHJUH.js → chunk-4J6JPSVS.js} +53 -2
  5. package/dist-plugin/{chunk-W5VQV32K.js → chunk-GYRULB2X.js} +1 -1
  6. package/dist-plugin/{chunk-ONYAIXCZ.js → chunk-QQASYPW7.js} +32 -9
  7. package/dist-plugin/index.cjs +598 -17
  8. package/dist-plugin/index.d.cts +9 -1
  9. package/dist-plugin/index.d.ts +9 -1
  10. package/dist-plugin/index.js +513 -6
  11. package/dist-plugin/migrateData/index.cjs +32 -9
  12. package/dist-plugin/migrateData/index.js +2 -2
  13. package/dist-plugin/setColors/index.cjs +32 -22
  14. package/dist-plugin/setColors/index.js +2 -15
  15. package/dist-plugin/setGeometry/index.cjs +32 -9
  16. package/dist-plugin/setGeometry/index.js +1 -1
  17. package/dist-plugin/tokensCssMigrations/index.cjs +53 -2
  18. package/dist-plugin/tokensCssMigrations/index.d.cts +9 -2
  19. package/dist-plugin/tokensCssMigrations/index.d.ts +9 -2
  20. package/dist-plugin/tokensCssMigrations/index.js +1 -1
  21. package/package.json +8 -3
  22. package/src/demo/Demo.svelte +39 -5
  23. package/src/demo/DemoBar.svelte +23 -5
  24. package/src/demo/FloatingTokenTags.css +33 -4
  25. package/src/demo/FloatingTokenTags.svelte +81 -68
  26. package/src/demo/Section.svelte +1 -7
  27. package/src/demo/SourceLinks.svelte +5 -4
  28. package/src/demo/TestingLoops.svelte +128 -19
  29. package/src/demo/evalResults.ts +7 -7
  30. package/src/demo/sections/SectionClaude.svelte +20 -13
  31. package/src/demo/sections/SectionFooter.svelte +4 -5
  32. package/src/demo/sections/SectionGetStarted.svelte +5 -6
  33. package/src/demo/sections/SectionHero.svelte +43 -23
  34. package/src/demo/sections/SectionKit.svelte +141 -45
  35. package/src/demo/sections/SectionLayers.svelte +3 -3
  36. package/src/demo/sections/SectionOffering.svelte +8 -23
  37. package/src/editor/bootstrap.ts +12 -6
  38. package/src/editor/component-editor/ImageEditor.svelte +1 -1
  39. package/src/editor/component-editor/ImageLightboxEditor.svelte +3 -4
  40. package/src/editor/component-editor/registration.ts +36 -0
  41. package/src/editor/component-editor/registry.ts +5 -12
  42. package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +11 -2
  43. package/src/editor/component-editor/scaffolding/StateBlock.svelte +7 -0
  44. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +1 -2
  45. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +17 -2
  46. package/src/editor/core/store/editorPersistence.ts +6 -1
  47. package/src/editor/core/store/editorRenderer.ts +1 -4
  48. package/src/editor/core/store/editorStore.ts +5 -9
  49. package/src/editor/core/store/editorTypes.ts +2 -2
  50. package/src/editor/core/themes/migrations/2026-10-15-responsive-columns.ts +40 -0
  51. package/src/editor/core/themes/migrations/index.ts +2 -0
  52. package/src/editor/core/themes/slices/columns.ts +112 -42
  53. package/src/editor/docs/Docs.svelte +25 -22
  54. package/src/editor/docs/chapters.ts +1 -0
  55. package/src/editor/docs/content/editing-tokens.md +5 -3
  56. package/src/editor/docs/content/production-css.md +116 -0
  57. package/src/editor/docs/content/themes-workflow.md +2 -0
  58. package/src/editor/docs/content.generated.ts +3 -2
  59. package/src/editor/index.ts +2 -1
  60. package/src/editor/overlay/LiveEditorOverlay.svelte +67 -19
  61. package/src/editor/overlay/LiveTokensRouter.svelte +8 -4
  62. package/src/editor/pages/Editor.svelte +15 -1
  63. package/src/editor/pages/EditorShell.svelte +312 -99
  64. package/src/editor/skill-atlas/SkillAtlas.svelte +111 -24
  65. package/src/editor/skill-atlas/TreeCanvas.svelte +14 -0
  66. package/src/editor/skill-atlas/skillSources.generated.ts +1 -1
  67. package/src/editor/skill-atlas/trees/create-page.ts +24 -24
  68. package/src/editor/skill-atlas/wireLayout.ts +62 -0
  69. package/src/editor/ui/BezierCurveEditor.svelte +3 -0
  70. package/src/editor/ui/FontStackEditor.svelte +2 -2
  71. package/src/editor/ui/PaletteEditor.svelte +64 -6
  72. package/src/editor/ui/UIPaletteSelector.svelte +2 -0
  73. package/src/editor/ui/colors/ColorsTab.svelte +5 -2
  74. package/src/editor/ui/palette/OverridesPanel.svelte +1 -0
  75. package/src/editor/ui/palette/ScaleCurveEditor.svelte +127 -18
  76. package/src/editor/ui/sections/ColumnsSection.svelte +79 -49
  77. package/src/editor/ui/sections/ShadowsSection.svelte +14 -0
  78. package/src/editor/ui/sections/WashesSection.svelte +11 -0
  79. package/src/editor/ui/sketch/SketchTab.svelte +1 -1
  80. package/src/live-tokens/data/colors-and-type/autumn.json +1 -4
  81. package/src/live-tokens/data/colors-and-type/default.json +1 -4
  82. package/src/live-tokens/data/colors-and-type/halloween.json +1 -4
  83. package/src/live-tokens/data/colors-and-type/midnight-study.json +1 -4
  84. package/src/live-tokens/data/colors-and-type/ocean.json +1 -4
  85. package/src/live-tokens/data/colors-and-type/royal-velvet.json +1 -4
  86. package/src/live-tokens/data/colors-and-type/sketchy.json +1 -4
  87. package/src/live-tokens/data/colors-and-type/spring-meadow.json +1 -4
  88. package/src/live-tokens/data/colors-and-type/sunset.json +1 -4
  89. package/src/live-tokens/data/themes/autumn.json +1 -4
  90. package/src/live-tokens/data/themes/halloween.json +1 -4
  91. package/src/live-tokens/data/themes/midnight-study.json +1 -4
  92. package/src/live-tokens/data/themes/ocean.json +1 -4
  93. package/src/live-tokens/data/themes/royal-velvet.json +1 -4
  94. package/src/live-tokens/data/themes/sketchy.json +1 -4
  95. package/src/live-tokens/data/themes/spring-meadow.json +1 -4
  96. package/src/live-tokens/data/themes/sunset.json +1 -4
  97. package/src/live-tokens/data/tokens.generated.css +1 -4
  98. package/src/system/components/SectionDivider.svelte +4 -0
  99. package/src/system/styles/tokens.css +30 -4
  100. package/template/src/main.ts +2 -2
  101. package/template/src/pages/Home.svelte +11 -8
  102. package/template/vite.config.ts +7 -3
  103. package/src/system/assets/offering.webp +0 -0
  104. /package/dist-plugin/{chunk-O6GQ6GBH.js → chunk-A4TBP4MM.js} +0 -0
@@ -1,7 +1,7 @@
1
- /** One arm of an eval: the same case run with or without the skills, and with or without the CLI. */
1
+ /** One arm of an eval: the same case run with or without the skills, and with or without the lookup command. */
2
2
  export interface EvalArm {
3
3
  skills: boolean;
4
- cli: boolean;
4
+ lookup: boolean;
5
5
  rowsRight: string;
6
6
  turns: string;
7
7
  seconds: string;
@@ -28,7 +28,7 @@ export const pickComponentEval: EvalResult = {
28
28
  arms: [
29
29
  {
30
30
  skills: true,
31
- cli: true,
31
+ lookup: true,
32
32
  rowsRight: '12 of 12 in all 3 runs',
33
33
  turns: '9 to 10',
34
34
  seconds: '46 to 76',
@@ -36,15 +36,15 @@ export const pickComponentEval: EvalResult = {
36
36
  },
37
37
  {
38
38
  skills: false,
39
- cli: true,
40
- rowsRight: '12 of 12 in all 3 runs; 2 of 3 found the CLI on their own',
39
+ lookup: true,
40
+ rowsRight: '12 of 12 in all 3 runs; 2 of 3 found the lookup on their own',
41
41
  turns: '15 to 17',
42
42
  seconds: '65 to 102',
43
43
  cost: '$0.63 to $0.74',
44
44
  },
45
45
  {
46
46
  skills: true,
47
- cli: false,
47
+ lookup: false,
48
48
  rowsRight: '12 of 12 in all 3 runs',
49
49
  turns: '14 to 20',
50
50
  seconds: '66 to 84',
@@ -52,7 +52,7 @@ export const pickComponentEval: EvalResult = {
52
52
  },
53
53
  {
54
54
  skills: false,
55
- cli: false,
55
+ lookup: false,
56
56
  rowsRight: '11 of 12 in one run, which missed requirement 12',
57
57
  turns: '17 to 20',
58
58
  seconds: '113 to 169',
@@ -66,28 +66,26 @@
66
66
  .claude {
67
67
  grid-column: 1 / -1;
68
68
  display: grid;
69
- grid-template-columns: repeat(var(--columns-count), 1fr);
70
- column-gap: var(--columns-gutter);
69
+ grid-template-columns: subgrid;
71
70
  row-gap: var(--space-8);
72
71
  align-items: start;
73
- margin-bottom: var(--space-96);
74
72
  }
75
73
 
76
74
  .claude :global(.section-divider) {
77
- grid-column: 2 / -2;
75
+ grid-column: var(--kit-content);
78
76
  }
79
77
 
80
- /* Panel sits on page columns 2, 3, 4; the copy starts on column 5, hard
78
+ /* Panel sits on page columns 2 to 5; the copy starts on column 6, hard
81
79
  against the panel's right edge. */
82
80
  .panel-col {
83
- grid-column: 2 / 5;
81
+ grid-column: 2 / 6;
84
82
  grid-row: 2;
85
83
  align-self: start;
86
84
  margin-top: var(--space-16);
87
85
  }
88
86
 
89
87
  .copy {
90
- grid-column: 5 / 11;
88
+ grid-column: 6 / -2;
91
89
  grid-row: 2;
92
90
  align-self: start;
93
91
  display: flex;
@@ -98,7 +96,7 @@
98
96
 
99
97
  .intro {
100
98
  font-family: var(--font-sans);
101
- font-size: var(--font-size-xl);
99
+ font-size: var(--body-xl-font-size);
102
100
  line-height: var(--line-height-tighter);
103
101
  font-weight: var(--font-weight-semibold);
104
102
  color: var(--text-primary);
@@ -114,7 +112,7 @@
114
112
  .install-caption {
115
113
  margin: 0;
116
114
  font-family: var(--font-sans);
117
- font-size: var(--font-size-md);
115
+ font-size: var(--body-md-font-size);
118
116
  line-height: var(--line-height-normal);
119
117
  color: var(--text-secondary);
120
118
  }
@@ -171,17 +169,16 @@
171
169
 
172
170
  .skill-name {
173
171
  font-family: var(--font-sans);
174
- font-size: var(--font-size-2xl);
172
+ font-size: var(--heading-md-font-size);
175
173
  font-weight: var(--font-weight-medium);
176
174
  line-height: var(--line-height-normal);
177
175
  color: var(--text-primary);
178
176
  }
179
177
 
180
- @media (max-width: 960px) {
181
- .claude :global(.section-divider),
178
+ @media (max-width: 1200px) {
182
179
  .panel-col,
183
180
  .copy {
184
- grid-column: 1 / -1;
181
+ grid-column: var(--kit-content);
185
182
  }
186
183
 
187
184
  .panel-col,
@@ -189,4 +186,14 @@
189
186
  grid-row: auto;
190
187
  }
191
188
  }
189
+
190
+ @media (max-width: 600px) {
191
+ .atlas {
192
+ align-items: stretch;
193
+ }
194
+
195
+ .atlas-actions {
196
+ flex-direction: column;
197
+ }
198
+ }
192
199
  </style>
@@ -16,7 +16,7 @@
16
16
 
17
17
  <style>
18
18
  .provenance {
19
- grid-column: 2 / -2;
19
+ grid-column: var(--kit-content);
20
20
  padding-top: var(--space-24);
21
21
  border-top: var(--border-width-2) solid var(--border-neutral);
22
22
  display: flex;
@@ -34,7 +34,7 @@
34
34
  .license {
35
35
  margin: 0;
36
36
  font-family: var(--font-sans);
37
- font-size: var(--font-size-lg);
37
+ font-size: var(--body-lg-font-size);
38
38
  color: var(--text-secondary);
39
39
  }
40
40
 
@@ -52,13 +52,12 @@
52
52
  .copyright {
53
53
  margin: 0;
54
54
  font-family: var(--font-sans);
55
- font-size: var(--font-size-md);
55
+ font-size: var(--body-md-font-size);
56
56
  color: var(--text-secondary);
57
57
  }
58
58
 
59
- @media (max-width: 960px) {
59
+ @media (max-width: 1200px) {
60
60
  .provenance {
61
- grid-column: 1 / -1;
62
61
  flex-direction: column;
63
62
  align-items: flex-start;
64
63
  gap: var(--space-24);
@@ -34,12 +34,11 @@ npm run dev`;
34
34
  .get-started {
35
35
  grid-column: 1 / -1;
36
36
  display: grid;
37
- grid-template-columns: repeat(var(--columns-count), 1fr);
38
- column-gap: var(--columns-gutter);
37
+ grid-template-columns: subgrid;
39
38
  }
40
39
 
41
40
  .gs-panel {
42
- grid-column: 2 / -2;
41
+ grid-column: var(--kit-content);
43
42
  /* The .kit grid can stretch this section's row past its content (min-height:
44
43
  100vh on a short page); keep the panel sized to its content regardless. */
45
44
  align-self: start;
@@ -68,7 +67,7 @@ npm run dev`;
68
67
 
69
68
  .intro {
70
69
  font-family: var(--font-sans);
71
- font-size: var(--font-size-xl);
70
+ font-size: var(--body-xl-font-size);
72
71
  line-height: var(--line-height-tighter);
73
72
  font-weight: var(--font-weight-semibold);
74
73
  color: var(--text-primary);
@@ -91,9 +90,9 @@ npm run dev`;
91
90
  overflow-y: hidden;
92
91
  }
93
92
 
94
- @media (max-width: 960px) {
93
+ @media (max-width: 1200px) {
95
94
  .columns {
96
- grid-template-columns: 1fr;
95
+ grid-template-columns: minmax(0, 1fr);
97
96
  align-items: stretch;
98
97
  }
99
98
  }
@@ -29,7 +29,7 @@
29
29
 
30
30
  <p class="hero-tagline">
31
31
  Design system authoring for Svelte.<br />
32
- Edit tokens and components directly in the browser.<br />Ship plain CSS.
32
+ Edit tokens and components directly in the browser.<br class="desktop-break" /> Ship plain CSS.
33
33
  </p>
34
34
  </div>
35
35
 
@@ -63,18 +63,20 @@
63
63
  .hero {
64
64
  grid-column: 1 / -1;
65
65
  position: relative;
66
- padding: var(--space-48) 0 var(--space-16);
66
+ padding: var(--space-48) 0 0;
67
67
  display: grid;
68
- grid-template-columns: repeat(var(--columns-count), 1fr);
69
- column-gap: var(--columns-gutter);
68
+ grid-template-columns: subgrid;
70
69
  }
71
70
 
71
+ /* The title runs across the kite on the full content width; the tagline
72
+ keeps the first five columns. */
72
73
  .hero-text {
73
- grid-column: 2 / span 5;
74
+ grid-column: var(--kit-content);
74
75
  grid-row: 1;
75
- display: flex;
76
- flex-direction: column;
77
- gap: var(--space-16);
76
+ display: grid;
77
+ grid-template-columns: subgrid;
78
+ align-content: start;
79
+ row-gap: var(--space-16);
78
80
  position: relative;
79
81
  padding: var(--space-96) 0 0;
80
82
  }
@@ -89,36 +91,37 @@
89
91
 
90
92
  .hero-title {
91
93
  font-family: var(--font-display);
92
- font-size: var(--font-size-7xl);
94
+ font-size: var(--display-xl-font-size);
93
95
  font-weight: var(--font-weight-semibold);
94
96
  font-variation-settings: 'opsz' 144, 'SOFT' 30;
95
97
  color: var(--text-primary);
96
98
  line-height: .9;
97
99
  margin: 0;
100
+ grid-column: 1 / -1;
98
101
  }
99
102
 
100
103
  .hero-italic {
101
104
  font-family: var(--heading-xl-font-family);
102
- font-size: var(--font-size-6xl);
105
+ font-size: var(--display-lg-font-size);
103
106
  font-weight: var(--font-weight-normal);
104
107
  color: var(--text-brand);
105
108
  font-variation-settings: 'opsz' 144, 'SOFT' 100;
106
- white-space: nowrap;
107
109
  }
108
110
 
109
111
  .hero-tagline {
110
112
  font-family: var(--body-md-font-family);
111
- font-size: var(--font-size-2xl);
113
+ font-size: var(--body-2xl-font-size);
112
114
  color: var(--text-primary);
113
115
  line-height: 1.2;
114
- max-width: 48rem;
116
+ grid-column: 1 / span 5;
115
117
  margin: var(--space-8) 0 0;
118
+ text-wrap: pretty;
116
119
  }
117
120
 
118
121
  /* One row: the CTAs on the left rail, the credits on the right, sharing a
119
122
  top edge so the buttons sit on the brand-mark line. */
120
123
  .hero-actions {
121
- grid-column: 2 / -2;
124
+ grid-column: var(--kit-content);
122
125
  grid-row: 2;
123
126
  display: grid;
124
127
  grid-template-columns: max-content 1fr;
@@ -148,7 +151,7 @@
148
151
 
149
152
  .hero-byline {
150
153
  font-family: var(--font-sans);
151
- font-size: var(--font-size-md);
154
+ font-size: var(--body-md-font-size);
152
155
  font-weight: var(--font-weight-normal);
153
156
  line-height: var(--line-height-normal);
154
157
  color: var(--text-secondary);
@@ -164,20 +167,31 @@
164
167
  text-decoration: underline;
165
168
  }
166
169
 
167
- @media (max-width: 960px) {
168
- .hero-kite {
169
- grid-column: 1 / -1;
170
+ @media (max-width: 1200px) {
171
+ /* Stacked, the illustration sits between the title and the tagline, so the
172
+ wrapper steps aside and each takes its own row of the hero grid. */
173
+ .hero-text {
174
+ display: contents;
175
+ }
176
+ .hero-title {
170
177
  grid-row: 1;
171
- height: 28rem;
172
178
  }
173
- .hero-text {
179
+ .hero-kite {
174
180
  grid-column: 1 / -1;
175
181
  grid-row: 2;
176
- padding: 0;
182
+ height: 28rem;
183
+ margin-top: var(--space-32);
177
184
  }
178
- .hero-actions {
185
+ .hero-tagline {
179
186
  grid-column: 1 / -1;
180
187
  grid-row: 3;
188
+ margin-top: var(--space-16);
189
+ }
190
+ .desktop-break {
191
+ display: none;
192
+ }
193
+ .hero-actions {
194
+ grid-row: 4;
181
195
  grid-template-columns: minmax(0, 1fr);
182
196
  row-gap: var(--space-16);
183
197
  margin-top: var(--space-12);
@@ -189,9 +203,15 @@
189
203
  }
190
204
  }
191
205
 
206
+ /* A phone wraps the second line, so the title steps up a size and the two
207
+ lines get a gap to tell them apart. */
192
208
  @media (max-width: 600px) {
193
209
  .hero-title {
194
- font-size: var(--font-size-5xl);
210
+ font-size: var(--display-2xl-font-size);
211
+ }
212
+ .hero-italic {
213
+ display: block;
214
+ margin-top: var(--space-16);
195
215
  }
196
216
  .hero-controls {
197
217
  grid-template-columns: minmax(0, 1fr);
@@ -2,11 +2,15 @@
2
2
  import Button from '../../system/components/Button.svelte';
3
3
  import SectionDivider from '../../system/components/SectionDivider.svelte';
4
4
  import SegmentedControl from '../../system/components/SegmentedControl.svelte';
5
+ import { MediaQuery } from 'svelte/reactivity';
5
6
  import Panel from '../../system/components/Panel.svelte';
6
7
  import Section from '../Section.svelte';
7
8
 
8
9
  let { prose = true }: { prose?: boolean } = $props();
9
10
 
11
+ // Matches the demo's phone breakpoint in Demo.svelte.
12
+ const phone = new MediaQuery('max-width: 600px');
13
+
10
14
  type Override = 'shape' | 'color';
11
15
 
12
16
  let shape = $state(false);
@@ -35,39 +39,98 @@
35
39
  return out;
36
40
  })();
37
41
 
38
- const colorOverride: Record<string, string> = {
39
- '--button-primary-surface': 'var(--surface-special-high)',
40
- '--button-primary-border': 'var(--border-special)',
41
- '--button-primary-hover-surface': 'var(--surface-special-higher)',
42
- '--button-primary-hover-border': 'var(--border-special-strong)',
43
-
44
- '--button-outline-border': 'var(--border-special)',
45
- '--button-outline-text': 'var(--text-special)',
46
- '--button-outline-hover-border': 'var(--border-special-strong)',
47
-
48
- '--button-success-surface': 'var(--surface-accent-low)',
49
- '--button-success-border': 'var(--border-accent)',
50
- '--button-success-text': 'var(--text-accent)',
51
- '--button-success-hover-surface': 'var(--surface-accent-higher)',
52
- '--button-success-hover-border': 'var(--border-accent-strong)',
53
-
54
- '--button-warning-surface': 'var(--surface-info-low)',
55
- '--button-warning-border': 'var(--border-info)',
56
- '--button-warning-text': 'var(--text-info)',
57
- '--button-warning-hover-surface': 'var(--surface-info-high)',
58
- '--button-warning-hover-border': 'var(--border-info-medium)',
59
-
60
- '--button-danger-surface': 'var(--surface-brand-low)',
61
- '--button-danger-border': 'var(--border-brand)',
62
- '--button-danger-text': 'var(--text-brand)',
63
- '--button-danger-hover-surface': 'var(--surface-brand-high)',
64
- '--button-danger-hover-border': 'var(--border-brand-medium)',
65
- };
42
+ const FAMILIES = ['brand', 'accent', 'special', 'info', 'success', 'warning', 'danger'] as const;
43
+ type Family = (typeof FAMILIES)[number];
44
+ // The family each swapped variant paints from. Outline follows primary.
45
+ const SOURCES = { primary: 'brand', success: 'success', warning: 'warning', danger: 'danger' } as const;
46
+ type Swapped = keyof typeof SOURCES;
47
+ // Themes can give two families near-identical hues (Autumn's warning and
48
+ // accent share one), so a fixed swap can land on the colour it replaces.
49
+ const MIN_HUE_SHIFT = 60;
50
+ const SAME_HUE = 15;
51
+
52
+ let colorMap = $state<Record<Swapped, Family> | null>(null);
53
+ let colorStep = 0;
54
+
55
+ function familyHues(): Record<Family, number> {
56
+ const probe = document.createElement('span');
57
+ document.body.appendChild(probe);
58
+ const hues = {} as Record<Family, number>;
59
+ for (const family of FAMILIES) {
60
+ probe.style.color = `var(--color-${family}-500)`;
61
+ hues[family] = Number(/oklch\(\S+ \S+ ([\d.]+)/.exec(getComputedStyle(probe).color)?.[1] ?? 0);
62
+ }
63
+ probe.remove();
64
+ return hues;
65
+ }
66
+
67
+ function hueGap(a: number, b: number): number {
68
+ const d = Math.abs(a - b) % 360;
69
+ return Math.min(d, 360 - d);
70
+ }
71
+
72
+ /** Each variant takes a family far enough round the wheel from its own,
73
+ * on a hue no other variant has where the theme has enough, rotated by
74
+ * `step`. Hue, not family: two families can share one. */
75
+ function pickColorMap(step: number): Record<Swapped, Family> {
76
+ const hues = familyHues();
77
+ const used: number[] = [];
78
+ const map = {} as Record<Swapped, Family>;
79
+ for (const [variant, source] of Object.entries(SOURCES) as [Swapped, Family][]) {
80
+ const byGap = FAMILIES.filter((f) => f !== source).sort((a, b) => hueGap(hues[b], hues[source]) - hueGap(hues[a], hues[source]));
81
+ const far = byGap.filter((f) => hueGap(hues[f], hues[source]) >= MIN_HUE_SHIFT);
82
+ const pool = far.length > 0 ? far : byGap.slice(0, 1);
83
+ const rotated = pool.map((_, i) => pool[(i + step) % pool.length]);
84
+ map[variant] = rotated.find((f) => used.every((h) => hueGap(h, hues[f]) >= SAME_HUE)) ?? rotated[0];
85
+ used.push(hues[map[variant]]);
86
+ }
87
+ return map;
88
+ }
89
+
90
+ function nextColorMap(): Record<Swapped, Family> {
91
+ const current = JSON.stringify(colorMap);
92
+ for (let tries = 0; tries < FAMILIES.length; tries++) {
93
+ const map = pickColorMap(++colorStep);
94
+ if (JSON.stringify(map) !== current) return map;
95
+ }
96
+ return pickColorMap(colorStep);
97
+ }
98
+
99
+ function colorOverride({ primary: p, success: s, warning: w, danger: d }: Record<Swapped, Family>): Record<string, string> {
100
+ return {
101
+ '--button-primary-surface': `var(--surface-${p}-high)`,
102
+ '--button-primary-border': `var(--border-${p})`,
103
+ '--button-primary-hover-surface': `var(--surface-${p}-higher)`,
104
+ '--button-primary-hover-border': `var(--border-${p}-strong)`,
105
+
106
+ '--button-outline-border': `var(--border-${p})`,
107
+ '--button-outline-text': `var(--text-${p})`,
108
+ '--button-outline-hover-border': `var(--border-${p}-strong)`,
109
+
110
+ '--button-success-surface': `var(--surface-${s}-low)`,
111
+ '--button-success-border': `var(--border-${s})`,
112
+ '--button-success-text': `var(--text-${s})`,
113
+ '--button-success-hover-surface': `var(--surface-${s}-higher)`,
114
+ '--button-success-hover-border': `var(--border-${s}-strong)`,
115
+
116
+ '--button-warning-surface': `var(--surface-${w}-low)`,
117
+ '--button-warning-border': `var(--border-${w})`,
118
+ '--button-warning-text': `var(--text-${w})`,
119
+ '--button-warning-hover-surface': `var(--surface-${w}-high)`,
120
+ '--button-warning-hover-border': `var(--border-${w}-medium)`,
121
+
122
+ '--button-danger-surface': `var(--surface-${d}-low)`,
123
+ '--button-danger-border': `var(--border-${d})`,
124
+ '--button-danger-text': `var(--text-${d})`,
125
+ '--button-danger-hover-surface': `var(--surface-${d}-high)`,
126
+ '--button-danger-hover-border': `var(--border-${d}-medium)`,
127
+ };
128
+ }
66
129
 
67
130
  let stageStyle = $derived.by(() => {
68
131
  const merged: Record<string, string> = {};
69
132
  if (shape) Object.assign(merged, shapeOverride);
70
- if (color) Object.assign(merged, colorOverride);
133
+ if (color && colorMap) Object.assign(merged, colorOverride(colorMap));
71
134
  return Object.entries(merged)
72
135
  .map(([k, v]) => `${k}: ${v}`)
73
136
  .join('; ');
@@ -96,9 +159,11 @@
96
159
  lastApplied = shape ? 'shape' : color ? 'color' : null;
97
160
  return;
98
161
  }
162
+ // Every press of Color is a new swap, so it never toggles back off.
99
163
  if (v === 'color') {
100
- color = !color;
101
- lastApplied = color ? 'color' : shape ? 'shape' : null;
164
+ colorMap = nextColorMap();
165
+ color = true;
166
+ lastApplied = 'color';
102
167
  }
103
168
  }
104
169
  </script>
@@ -156,7 +221,7 @@
156
221
  <div class="swap-block">
157
222
  <div class="swap-control">
158
223
  <SectionDivider title="Swap tokens with a click" variant="sm" />
159
- <SegmentedControl {segments} value={activeSegment} onchange={handleSelect} />
224
+ <SegmentedControl {segments} value={activeSegment} onchange={handleSelect} size={phone.current ? 'small' : 'default'} />
160
225
  </div>
161
226
 
162
227
  <Panel style={stageStyle}>
@@ -184,7 +249,7 @@
184
249
  columns and the row stops at column 9, leaving column 10 open on the right. */
185
250
  .kit-grid {
186
251
  display: grid;
187
- grid-template-columns: repeat(calc(var(--columns-count) - 2), 1fr);
252
+ grid-template-columns: repeat(calc(var(--columns-count) - 2), minmax(0, 1fr));
188
253
  column-gap: var(--columns-gutter);
189
254
  row-gap: var(--space-32);
190
255
  }
@@ -202,7 +267,8 @@
202
267
  padding: 0 0 var(--space-12);
203
268
  }
204
269
 
205
- /* Title, body and icon mirror the Card typography used in the Layers section. */
270
+ /* Title and body match the Card in the Layers section at desktop sizes, and
271
+ take the text styles' phone sizes below it. */
206
272
  .kit-icon {
207
273
  font-size: var(--icon-size-2xl);
208
274
  color: var(--text-primary);
@@ -211,7 +277,7 @@
211
277
  .kit-title {
212
278
  color: var(--text-primary);
213
279
  font-family: var(--font-sans);
214
- font-size: var(--font-size-2xl);
280
+ font-size: var(--heading-md-font-size);
215
281
  font-weight: var(--font-weight-medium);
216
282
  line-height: var(--line-height-normal);
217
283
  }
@@ -219,7 +285,7 @@
219
285
  .kit-body {
220
286
  padding: var(--space-12) 0 0;
221
287
  font-family: var(--font-sans);
222
- font-size: var(--font-size-xl);
288
+ font-size: var(--body-xl-font-size);
223
289
  line-height: var(--line-height-normal);
224
290
  font-weight: var(--font-weight-normal);
225
291
  color: var(--text-secondary);
@@ -270,7 +336,7 @@
270
336
  --sketch-hatch-color: var(--color-brand-500);
271
337
  }
272
338
 
273
- /* The head only carries a surface of its own when stacked (see the 960
339
+ /* The head only carries a surface of its own when stacked (see the 1200
274
340
  breakpoint). Here it has none, and --sketch-fill inherits, so without this
275
341
  the layer would draw the lead's wash a second time behind the head. */
276
342
  .kit-lead .kit-head {
@@ -288,14 +354,14 @@
288
354
  }
289
355
 
290
356
  .kit-lead .kit-title {
291
- font-size: var(--font-size-4xl);
357
+ font-size: var(--heading-xl-font-size);
292
358
  font-weight: var(--font-weight-bold);
293
359
  }
294
360
 
295
361
  .kit-lead .kit-body {
296
362
  grid-column: 4 / span 6;
297
363
  padding: var(--space-32) 0;
298
- font-size: var(--font-size-2xl);
364
+ font-size: var(--body-2xl-font-size);
299
365
  color: var(--text-primary);
300
366
  }
301
367
 
@@ -304,7 +370,7 @@
304
370
  .kit-preview {
305
371
  margin-top: var(--space-32);
306
372
  display: grid;
307
- grid-template-columns: repeat(calc(var(--columns-count) - 2), 1fr);
373
+ grid-template-columns: repeat(calc(var(--columns-count) - 2), minmax(0, 1fr));
308
374
  column-gap: var(--columns-gutter);
309
375
  }
310
376
 
@@ -349,7 +415,7 @@
349
415
  .stage-eyebrow {
350
416
  align-self: center;
351
417
  font-family: var(--font-sans);
352
- font-size: var(--font-size-md);
418
+ font-size: var(--body-md-font-size);
353
419
  font-weight: var(--font-weight-medium);
354
420
  letter-spacing: var(--letter-spacing-wide);
355
421
  text-transform: uppercase;
@@ -373,9 +439,10 @@
373
439
  justify-content: center;
374
440
  }
375
441
 
376
- @media (max-width: 960px) {
377
- .kit-grid {
378
- grid-template-columns: 1fr;
442
+ @media (max-width: 1200px) {
443
+ .kit-grid,
444
+ .kit-preview {
445
+ grid-template-columns: minmax(0, 1fr);
379
446
  }
380
447
  .kit-item {
381
448
  grid-column: auto;
@@ -404,7 +471,36 @@
404
471
  }
405
472
  .kit-lead .kit-body {
406
473
  padding: var(--space-16) var(--space-20);
407
- font-size: var(--font-size-xl);
474
+ font-size: var(--body-xl-font-size);
475
+ }
476
+ }
477
+
478
+ /* Phone: the lead's text lines up with the page margin, and the head's tint
479
+ bleeds out to the screen edges. The tint is a border-image outset, which
480
+ paints past the box without widening the section. The swap control spans
481
+ the column, its segments sharing the width. */
482
+ @media (max-width: 600px) {
483
+ .swap-control {
484
+ margin-top: var(--space-8);
485
+ flex-direction: column;
486
+ align-items: stretch;
487
+ gap: var(--space-8);
488
+ }
489
+ .swap-control :global(.segmented-control) {
490
+ display: flex;
491
+ }
492
+ .swap-control :global(.segment) {
493
+ flex: 1 1 0;
494
+ min-width: 0;
495
+ justify-content: center;
496
+ }
497
+ .kit-lead .kit-head {
498
+ padding-inline: 0;
499
+ background: none;
500
+ border-image: conic-gradient(var(--kit-head-tint) 0 0) fill 0 / 0 / 0 var(--columns-margin);
501
+ }
502
+ .kit-lead .kit-body {
503
+ padding-inline: 0;
408
504
  }
409
505
  }
410
506
  </style>
@@ -28,7 +28,7 @@
28
28
  <style>
29
29
  .arch {
30
30
  display: grid;
31
- grid-template-columns: repeat(2, 1fr);
31
+ grid-template-columns: repeat(2, minmax(0, 1fr));
32
32
  column-gap: var(--columns-gutter);
33
33
  row-gap: var(--space-32);
34
34
  }
@@ -46,9 +46,9 @@
46
46
  border-radius: var(--radius-sm);
47
47
  }
48
48
 
49
- @media (max-width: 960px) {
49
+ @media (max-width: 1200px) {
50
50
  .arch {
51
- grid-template-columns: 1fr;
51
+ grid-template-columns: minmax(0, 1fr);
52
52
  }
53
53
  }
54
54
  </style>