@motion-proto/live-tokens 0.94.2 → 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 (108) hide show
  1. package/.claude/skills/live-tokens-create-page/SKILL.md +7 -3
  2. package/CHANGELOG.md +108 -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/ChromeSelect.svelte +111 -0
  23. package/src/demo/Demo.svelte +39 -5
  24. package/src/demo/DemoBar.svelte +33 -14
  25. package/src/demo/FloatingTokenTags.css +33 -4
  26. package/src/demo/FloatingTokenTags.svelte +81 -68
  27. package/src/demo/Section.svelte +1 -7
  28. package/src/demo/SketchSelect.svelte +36 -12
  29. package/src/demo/SourceLinks.svelte +5 -4
  30. package/src/demo/TestingLoops.svelte +128 -19
  31. package/src/demo/ThemeSelect.svelte +34 -11
  32. package/src/demo/evalResults.ts +7 -7
  33. package/src/demo/sections/SectionClaude.svelte +20 -13
  34. package/src/demo/sections/SectionFooter.svelte +4 -5
  35. package/src/demo/sections/SectionGetStarted.svelte +5 -6
  36. package/src/demo/sections/SectionHero.svelte +43 -23
  37. package/src/demo/sections/SectionKit.svelte +141 -45
  38. package/src/demo/sections/SectionLayers.svelte +3 -3
  39. package/src/demo/sections/SectionOffering.svelte +8 -23
  40. package/src/editor/bootstrap.ts +12 -6
  41. package/src/editor/component-editor/ImageEditor.svelte +1 -1
  42. package/src/editor/component-editor/ImageLightboxEditor.svelte +3 -4
  43. package/src/editor/component-editor/registration.ts +36 -0
  44. package/src/editor/component-editor/registry.ts +5 -12
  45. package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +11 -2
  46. package/src/editor/component-editor/scaffolding/StateBlock.svelte +7 -0
  47. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +1 -2
  48. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +17 -2
  49. package/src/editor/core/store/editorPersistence.ts +6 -1
  50. package/src/editor/core/store/editorRenderer.ts +1 -4
  51. package/src/editor/core/store/editorStore.ts +5 -9
  52. package/src/editor/core/store/editorTypes.ts +2 -2
  53. package/src/editor/core/themes/migrations/2026-10-15-responsive-columns.ts +40 -0
  54. package/src/editor/core/themes/migrations/index.ts +2 -0
  55. package/src/editor/core/themes/slices/columns.ts +112 -42
  56. package/src/editor/docs/Docs.svelte +25 -22
  57. package/src/editor/docs/chapters.ts +1 -0
  58. package/src/editor/docs/content/editing-tokens.md +5 -3
  59. package/src/editor/docs/content/production-css.md +116 -0
  60. package/src/editor/docs/content/themes-workflow.md +2 -0
  61. package/src/editor/docs/content.generated.ts +3 -2
  62. package/src/editor/index.ts +2 -1
  63. package/src/editor/overlay/LiveEditorOverlay.svelte +67 -19
  64. package/src/editor/overlay/LiveTokensRouter.svelte +8 -4
  65. package/src/editor/pages/Editor.svelte +15 -1
  66. package/src/editor/pages/EditorShell.svelte +312 -99
  67. package/src/editor/skill-atlas/SkillAtlas.svelte +111 -24
  68. package/src/editor/skill-atlas/TreeCanvas.svelte +14 -0
  69. package/src/editor/skill-atlas/skillSources.generated.ts +1 -1
  70. package/src/editor/skill-atlas/trees/create-page.ts +24 -24
  71. package/src/editor/skill-atlas/wireLayout.ts +62 -0
  72. package/src/editor/ui/BezierCurveEditor.svelte +3 -0
  73. package/src/editor/ui/FontStackEditor.svelte +2 -2
  74. package/src/editor/ui/PaletteEditor.svelte +64 -6
  75. package/src/editor/ui/UIPaletteSelector.svelte +2 -0
  76. package/src/editor/ui/colors/ColorsTab.svelte +5 -2
  77. package/src/editor/ui/palette/OverridesPanel.svelte +1 -0
  78. package/src/editor/ui/palette/ScaleCurveEditor.svelte +127 -18
  79. package/src/editor/ui/sections/ColumnsSection.svelte +79 -49
  80. package/src/editor/ui/sections/ShadowsSection.svelte +14 -0
  81. package/src/editor/ui/sections/WashesSection.svelte +11 -0
  82. package/src/editor/ui/sketch/SketchTab.svelte +1 -1
  83. package/src/live-tokens/data/colors-and-type/autumn.json +1 -4
  84. package/src/live-tokens/data/colors-and-type/default.json +1 -4
  85. package/src/live-tokens/data/colors-and-type/halloween.json +1 -4
  86. package/src/live-tokens/data/colors-and-type/midnight-study.json +1 -4
  87. package/src/live-tokens/data/colors-and-type/ocean.json +1 -4
  88. package/src/live-tokens/data/colors-and-type/royal-velvet.json +1 -4
  89. package/src/live-tokens/data/colors-and-type/sketchy.json +1 -4
  90. package/src/live-tokens/data/colors-and-type/spring-meadow.json +1 -4
  91. package/src/live-tokens/data/colors-and-type/sunset.json +1 -4
  92. package/src/live-tokens/data/themes/autumn.json +1 -4
  93. package/src/live-tokens/data/themes/halloween.json +1 -4
  94. package/src/live-tokens/data/themes/midnight-study.json +1 -4
  95. package/src/live-tokens/data/themes/ocean.json +1 -4
  96. package/src/live-tokens/data/themes/royal-velvet.json +1 -4
  97. package/src/live-tokens/data/themes/sketchy.json +1 -4
  98. package/src/live-tokens/data/themes/spring-meadow.json +1 -4
  99. package/src/live-tokens/data/themes/sunset.json +1 -4
  100. package/src/live-tokens/data/tokens.generated.css +1 -4
  101. package/src/system/components/SectionDivider.svelte +4 -0
  102. package/src/system/styles/CONVENTIONS.md +1 -1
  103. package/src/system/styles/tokens.css +30 -4
  104. package/template/src/main.ts +2 -2
  105. package/template/src/pages/Home.svelte +11 -8
  106. package/template/vite.config.ts +7 -3
  107. package/src/system/assets/offering.webp +0 -0
  108. /package/dist-plugin/{chunk-O6GQ6GBH.js → chunk-A4TBP4MM.js} +0 -0
@@ -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>
@@ -17,13 +17,15 @@
17
17
  .offering {
18
18
  grid-column: 1 / -1;
19
19
  display: grid;
20
- grid-template-columns: repeat(var(--columns-count), 1fr);
21
- column-gap: var(--columns-gutter);
20
+ grid-template-columns: subgrid;
22
21
  }
23
22
 
24
23
  .offering-quote {
25
- grid-column: 3 / -3;
24
+ grid-column: var(--kit-narrow);
25
+ justify-self: center;
26
+ max-width: 24em;
26
27
  margin: 0;
28
+ font-size: var(--heading-lg-font-size);
27
29
  }
28
30
 
29
31
  .offering-quote blockquote {
@@ -38,12 +40,13 @@
38
40
 
39
41
  .offering-quote p {
40
42
  font-family: var(--font-serif);
41
- font-size: var(--font-size-3xl);
43
+ font-size: var(--heading-lg-font-size);
42
44
  font-weight: var(--font-weight-normal);
43
45
  color: var(--text-primary);
44
46
  line-height: 1.2;
45
47
  margin: 0;
46
48
  hyphens: none;
49
+ text-wrap: pretty;
47
50
  text-indent: -0.55em;
48
51
  }
49
52
 
@@ -65,29 +68,11 @@
65
68
  margin-top: 0;
66
69
  font-family: var(--font-serif);
67
70
  font-weight: var(--font-weight-normal);
68
- font-size: var(--font-size-3xl);
71
+ font-size: var(--heading-lg-font-size);
69
72
  font-variation-settings: 'opsz' 144, 'SOFT' 100;
70
73
  letter-spacing: 0.01em;
71
74
  color: var(--text-primary);
72
75
  text-align: right;
73
76
  padding-right: 0.4em;
74
77
  }
75
-
76
- @media (max-width: 960px) {
77
- .offering-quote {
78
- grid-column: 2 / -2;
79
- }
80
- .offering-quote p {
81
- font-size: var(--font-size-xl);
82
- }
83
- }
84
-
85
- @media (max-width: 600px) {
86
- .offering-quote {
87
- grid-column: 1 / -1;
88
- }
89
- .offering-quote p {
90
- font-size: var(--font-size-4xl);
91
- }
92
- }
93
78
  </style>
@@ -18,11 +18,8 @@ import { mount, type Component } from 'svelte';
18
18
  import * as cssVarSync from './core/cssVarSync';
19
19
  import * as router from './core/routing/router';
20
20
  import * as columnsOverlay from './overlay/columnsOverlay';
21
- import * as editorStore from './core/store/editorStore';
22
- import * as themeDocumentSync from './core/themes/themeDocumentSync';
23
- import * as liveStateStream from './core/themes/liveStateStream';
24
- import { initializeTheme } from './core/themes/themeInit';
25
- import { registerComponent, type RegisterComponentEntry } from './component-editor/registry';
21
+ import { registerComponent } from './component-editor/registration';
22
+ import type { RegisterComponentEntry } from './component-editor/registry';
26
23
  import { registerSketchStyle, type RegisterSketchStyleInput } from './core/sketch/sketchRegistry';
27
24
 
28
25
  export interface BootLiveTokensOptions {
@@ -43,7 +40,6 @@ export async function bootLiveTokens(
43
40
  cssVarSync.init();
44
41
  router.init();
45
42
  columnsOverlay.init();
46
- editorStore.init();
47
43
 
48
44
  // Before the DEV block: `initializeTheme` recovers the open theme's style by
49
45
  // name against the pool, so the pool has to be whole by the time it runs.
@@ -52,6 +48,16 @@ export async function bootLiveTokens(
52
48
  }
53
49
 
54
50
  if (import.meta.env.DEV) {
51
+ // Imported here, never statically, so a build drops the editor. The
52
+ // registry alone imports every shipped component and editor.
53
+ const [editorStore, themeDocumentSync, liveStateStream, { initializeTheme }] = await Promise.all([
54
+ import('./core/store/editorStore'),
55
+ import('./core/themes/themeDocumentSync'),
56
+ import('./core/themes/liveStateStream'),
57
+ import('./core/themes/themeInit'),
58
+ import('./component-editor/registry'),
59
+ ]);
60
+ editorStore.init();
55
61
  themeDocumentSync.init();
56
62
  liveStateStream.init();
57
63
  if (opts.components) {
@@ -50,7 +50,7 @@
50
50
  import VariantGroup from './scaffolding/VariantGroup.svelte';
51
51
  import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
52
52
  import { buildSiblings } from './scaffolding/siblings';
53
- import demoImageUrl from '../../system/assets/offering.webp';
53
+ import demoImageUrl from '../../system/assets/newspaper.webp';
54
54
  import { editorState, setComponentAlias } from '../core/store/editorStore';
55
55
 
56
56
  let aliases = $derived(
@@ -45,13 +45,12 @@
45
45
  import ImageLightbox from '../../system/components/ImageLightbox.svelte';
46
46
  import VariantGroup from './scaffolding/VariantGroup.svelte';
47
47
  import ComponentEditorBase from './scaffolding/ComponentEditorBase.svelte';
48
- import offeringUrl from '../../system/assets/offering.webp';
49
48
  import newspaperUrl from '../../system/assets/newspaper.webp';
50
49
 
51
50
  const demoImages = [
52
- // Offering carries explicit dimensions (the no-reflow path); Newspaper omits
53
- // them to exercise self-measure from the loaded image.
54
- { src: offeringUrl, alt: 'Offering', width: 1455, height: 970 },
51
+ // The first entry carries explicit dimensions (the no-reflow path); the second
52
+ // omits them to exercise self-measure from the loaded image.
53
+ { src: newspaperUrl, alt: 'Newspaper', width: 2048, height: 1152 },
55
54
  { src: newspaperUrl, alt: 'Newspaper' },
56
55
  ];
57
56
 
@@ -0,0 +1,36 @@
1
+ import type { RegisterComponentEntry } from './registry';
2
+
3
+ /*
4
+ * `registerComponent` lives apart from `registry.ts` so a production build can
5
+ * call it without bundling the registry, which imports every shipped component
6
+ * and editor. Entries wait here until the registry loads, which happens in dev
7
+ * only.
8
+ */
9
+
10
+ type Sink = (entry: RegisterComponentEntry) => void;
11
+
12
+ const pending: RegisterComponentEntry[] = [];
13
+ let sink: Sink | null = null;
14
+
15
+ /**
16
+ * Register a consumer-authored component with the editor. Call from `main.ts`
17
+ * before app mount.
18
+ *
19
+ * Collision rule: if `entry.id` matches a built-in id, a warning is logged and
20
+ * the custom entry wins (the custom editor and schema replace the built-in for
21
+ * the rest of the session).
22
+ *
23
+ * Side effect: registers the schema with the editor store so reset-to-default
24
+ * and sibling-group resolution work for the new component.
25
+ */
26
+ export function registerComponent(entry: RegisterComponentEntry): void {
27
+ if (sink) sink(entry);
28
+ else pending.push(entry);
29
+ }
30
+
31
+ /** Called once by `registry.ts` as it loads. Drains the entries registered
32
+ before it, in call order, and takes every later one directly. */
33
+ export function attachRegistry(next: Sink): void {
34
+ sink = next;
35
+ for (const entry of pending.splice(0)) next(entry);
36
+ }
@@ -1,6 +1,9 @@
1
1
  import type { Component } from 'svelte';
2
2
  import type { CatalogueEntry, Token, IntrinsicSpec } from './scaffolding/types';
3
3
  import { registerComponentSchema } from '../core/store/editorStore';
4
+ import { attachRegistry } from './registration';
5
+
6
+ export { registerComponent } from './registration';
4
7
 
5
8
  import BadgeEditor, { allTokens as badgeTokens } from './BadgeEditor.svelte';
6
9
  import { catalogue as badgeCatalogue } from '../../system/components/Badge.svelte';
@@ -433,18 +436,7 @@ const customRegistry = new Map<string, RegistryEntry>();
433
436
  /** Argument shape for `registerComponent()`. `origin` is set internally to `'custom'`. */
434
437
  export type RegisterComponentEntry = Omit<RegistryEntry, 'origin'>;
435
438
 
436
- /**
437
- * Register a consumer-authored component at runtime. Call from `main.ts`
438
- * before app mount.
439
- *
440
- * Collision rule: if `entry.id` matches a built-in id, a warning is logged and
441
- * the custom entry wins (the custom editor and schema replace the built-in for
442
- * the rest of the session).
443
- *
444
- * Side effect: registers the schema with the editor store so reset-to-default
445
- * and sibling-group resolution work for the new component.
446
- */
447
- export function registerComponent(entry: RegisterComponentEntry): void {
439
+ function addCustomComponent(entry: RegisterComponentEntry): void {
448
440
  if (entry.id in builtInRegistry) {
449
441
  console.warn(
450
442
  `[registerComponent] custom component "${entry.id}" overrides a built-in. The custom editor will be used.`,
@@ -496,6 +488,7 @@ export function getComponentIds(): ReadonlyArray<string> {
496
488
  for (const entry of Object.values(builtInRegistry)) {
497
489
  registerComponentSchema(entry.id, entry.schema);
498
490
  }
491
+ attachRegistry(addCustomComponent);
499
492
 
500
493
  /**
501
494
  * Validate that the server's filesystem scan matches the merged registry's id list.