@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
@@ -41,6 +41,8 @@ const CORNER = 12;
41
41
  * inside the next when two share a stretch of gutter. */
42
42
  const LANE_INSET = 40;
43
43
  const LANE_STEP = 16;
44
+ /** A comb rides inside the loop lanes, which start at LANE_INSET. */
45
+ const COMB_INSET = 20;
44
46
 
45
47
  /** Cards laid out in one row share a top to within a pixel; cards pushed onto
46
48
  * a second wrapped line do not, and a single bus cannot serve both. */
@@ -133,6 +135,52 @@ function fanIn(out: Drawing, parents: Box[], b: Box, edges: Edge[], lit: (i: num
133
135
  });
134
136
  }
135
137
 
138
+ /** A branch row wrapped to one card per line, as on a phone: the cards share
139
+ * a column and stand one under the next. */
140
+ function stacked(group: Box[]): boolean {
141
+ return group.length > 1 && !aligned(group.map((g) => g.top)) && group.every((g) => group.every((h) => g.left < h.right && h.left < g.right));
142
+ }
143
+
144
+ /** `fanOut` for a stacked row: one comb down the right of the stack and a
145
+ * tick into each card, instead of a lane per card nesting off the canvas. */
146
+ function combOut(out: Drawing, a: Box, kids: Box[], edges: Edge[], lit: (i: number) => boolean) {
147
+ const lane = Math.max(a.right, ...kids.map((k) => k.right)) + COMB_INSET;
148
+ const last = kids[kids.length - 1];
149
+ const r = Math.max(0, Math.min(CORNER, (lane - a.right - STANDOFF) / 2, (last.midY - a.midY) / 2));
150
+ out.wires.push({
151
+ back: false,
152
+ arrow: false,
153
+ lit: kids.some((_, i) => lit(i)),
154
+ d:
155
+ `M ${a.right + STANDOFF} ${a.midY} L ${lane - r} ${a.midY}` +
156
+ ` Q ${lane} ${a.midY}, ${lane} ${a.midY + r} L ${lane} ${last.midY}`,
157
+ });
158
+ kids.forEach((k, i) => {
159
+ out.wires.push({ back: false, arrow: true, lit: lit(i), d: `M ${lane} ${k.midY} L ${k.right + STANDOFF} ${k.midY}` });
160
+ label(out, edges[i].label, lane, ((i === 0 ? a.midY : kids[i - 1].midY) + k.midY) / 2, lit(i), true);
161
+ });
162
+ }
163
+
164
+ /** `fanIn` for a stacked row: each card ties into one comb down the left of
165
+ * the stack, which turns into the child. */
166
+ function combIn(out: Drawing, parents: Box[], b: Box, edges: Edge[], lit: (i: number) => boolean) {
167
+ const lane = Math.min(b.left, ...parents.map((p) => p.left)) - COMB_INSET;
168
+ const first = parents[0];
169
+ const r = Math.max(0, Math.min(CORNER, (b.left - STANDOFF - lane) / 2, (b.midY - first.midY) / 2));
170
+ parents.forEach((p, i) => {
171
+ out.wires.push({ back: false, arrow: false, lit: lit(i), d: `M ${p.left - STANDOFF} ${p.midY} L ${lane} ${p.midY}` });
172
+ label(out, edges[i].label, lane, (p.midY + (parents[i + 1]?.midY ?? b.midY)) / 2, lit(i), true);
173
+ });
174
+ out.wires.push({
175
+ back: false,
176
+ arrow: true,
177
+ lit: parents.some((_, i) => lit(i)),
178
+ d:
179
+ `M ${lane} ${first.midY} L ${lane} ${b.midY - r}` +
180
+ ` Q ${lane} ${b.midY}, ${lane + r} ${b.midY} L ${b.left - STANDOFF} ${b.midY}`,
181
+ });
182
+ }
183
+
136
184
  /** A plain parent-to-child curve, and the fallback wherever a bus would have
137
185
  * to cross a card: children wrapped onto a second line, or parents staggered
138
186
  * across rows. */
@@ -227,6 +275,13 @@ export function routeWires(boxes: Map<string, Box>, edges: Edge[], lit: (edge: E
227
275
  const kids = kidEdges.map((e) => boxes.get(e.to));
228
276
  if (!a || kidEdges.length < 2 || kids.some((k) => !k)) continue;
229
277
  const kept = kids as Box[];
278
+ if (stacked(kept)) {
279
+ const order = kept.map((_, i) => i).sort((i, j) => kept[i].top - kept[j].top);
280
+ const sortedEdges = order.map((i) => kidEdges[i]);
281
+ sortedEdges.forEach((e) => claimed.add(e));
282
+ combOut(out, a, order.map((i) => kept[i]), sortedEdges, (i) => lit(sortedEdges[i]));
283
+ continue;
284
+ }
230
285
  if (!aligned(kept.map((k) => k.top))) continue;
231
286
  kidEdges.forEach((e) => claimed.add(e));
232
287
  fanOut(out, a, kept, kidEdges, (i) => lit(kidEdges[i]));
@@ -238,6 +293,13 @@ export function routeWires(boxes: Map<string, Box>, edges: Edge[], lit: (edge: E
238
293
  const ups = open.map((e) => boxes.get(e.from));
239
294
  if (!b || open.length < 2 || ups.some((u) => !u)) continue;
240
295
  const kept = ups as Box[];
296
+ if (stacked(kept)) {
297
+ const order = kept.map((_, i) => i).sort((i, j) => kept[i].top - kept[j].top);
298
+ const sortedEdges = order.map((i) => open[i]);
299
+ sortedEdges.forEach((e) => claimed.add(e));
300
+ combIn(out, order.map((i) => kept[i]), b, sortedEdges, (i) => lit(sortedEdges[i]));
301
+ continue;
302
+ }
241
303
  if (!aligned(kept.map((u) => u.bottom))) continue;
242
304
  open.forEach((e) => claimed.add(e));
243
305
  fanIn(out, kept, b, open, (i) => lit(open[i]));
@@ -567,6 +567,8 @@
567
567
  transform: translateY(0);
568
568
  }
569
569
 
570
+ /* Chrome reads touch-action from the outer svg, never from the circles and
571
+ paths inside it, so the anchors' own rule alone leaves a drag scrolling. */
570
572
  .curve-svg {
571
573
  width: 100%;
572
574
  height: 100%;
@@ -575,6 +577,7 @@
575
577
  border-radius: var(--ui-radius-sm);
576
578
  cursor: crosshair;
577
579
  display: block;
580
+ touch-action: none;
578
581
  }
579
582
 
580
583
  .curve-out-of-range {
@@ -409,7 +409,7 @@
409
409
 
410
410
  @media (max-width: 720px) {
411
411
  .font-stacks-columns {
412
- grid-template-columns: 1fr;
412
+ grid-template-columns: minmax(0, 1fr);
413
413
  }
414
414
  }
415
415
 
@@ -471,7 +471,7 @@
471
471
 
472
472
  .slot-controls {
473
473
  display: grid;
474
- grid-template-columns: auto auto 1fr auto;
474
+ grid-template-columns: auto auto minmax(0, 1fr) auto;
475
475
  align-items: center;
476
476
  gap: var(--ui-space-8);
477
477
  }
@@ -704,14 +704,14 @@
704
704
  {/if}
705
705
  </div>
706
706
  <div class="swatch-grid" style="--swatch-cols: {paletteStepLightness.length + 2}">
707
- <div class="step-column">
707
+ <div class="step-column" style="--stacked-order: 0">
708
708
  <button class="step-label copyable-label" class:copied={copiedLabelKey === 'palette-white'} type="button" onclick={(e) => copyVarName('palette-white', `--color-${cssNamespace}-white`, e)}>
709
709
  {copiedLabelKey === 'palette-white' ? 'copied!' : 'white'}
710
710
  </button>
711
711
  <div class="swatch gray-swatch bookend" style="background: #ffffff"></div>
712
712
  </div>
713
- {#each paletteComputed as ps}
714
- <div class="step-column">
713
+ {#each paletteComputed as ps, i}
714
+ <div class="step-column" style="--stacked-order: {2 * (i + 1)}">
715
715
  <button class="step-label copyable-label" class:copied={copiedLabelKey === ps.key} class:selected={editingKey === ps.key || (isEditingBase && ps.anchored)} type="button" onclick={(e) => copyVarName(ps.key, `--color-${cssNamespace}-${ps.label}`, e)}>
716
716
  {copiedLabelKey === ps.key ? 'copied!' : ps.label}
717
717
  </button>
@@ -743,14 +743,14 @@
743
743
  >{copiedKey === ps.key ? 'copied!' : ps.effective}</button>
744
744
  </div>
745
745
  {/each}
746
- <div class="step-column">
746
+ <div class="step-column" style="--stacked-order: {2 * (paletteComputed.length + 1)}">
747
747
  <button class="step-label copyable-label" class:copied={copiedLabelKey === 'palette-black'} type="button" onclick={(e) => copyVarName('palette-black', `--color-${cssNamespace}-black`, e)}>
748
748
  {copiedLabelKey === 'palette-black' ? 'copied!' : 'black'}
749
749
  </button>
750
750
  <div class="swatch gray-swatch bookend" style="background: #000000"></div>
751
751
  </div>
752
752
 
753
- <UIReveal open={editingPaletteIndex !== null} style="grid-column: 2 / {paletteStepLightness.length + 2}">
753
+ <UIReveal open={editingPaletteIndex !== null} style="grid-column: 2 / {paletteStepLightness.length + 2}; --stacked-order: {2 * ((dock?.paletteIndex ?? 0) + 1) + 1}">
754
754
  <!-- The reveal has to own the grid slot, so the dock rebuilds the
755
755
  ramp's tracks inside it: the caret is then placed on the step's
756
756
  own column and points at it at any width, no arithmetic. -->
@@ -760,7 +760,7 @@
760
760
  </div>
761
761
  </UIReveal>
762
762
 
763
- <UIReveal open={paletteEditorOpen} style="grid-column: 2 / {paletteStepLightness.length + 2}">
763
+ <UIReveal open={paletteEditorOpen} style="grid-column: 2 / {paletteStepLightness.length + 2}; --stacked-order: {2 * (paletteComputed.length + 1) + 1}">
764
764
  <div class="curve-grid-span">
765
765
  {#if anchorUnlockPrompt && anchorToBase}
766
766
  <div class="anchor-unlock-notice" role="alert">
@@ -773,6 +773,7 @@
773
773
  </div>
774
774
  {/if}
775
775
  <ScaleCurveEditor
776
+ subject={displayLabel ?? label}
776
777
  curveKey="hue"
777
778
  anchors={hueCurve ?? DEFAULT_PALETTE_HUE()}
778
779
  cfg={hueCurveConfig}
@@ -789,6 +790,7 @@
789
790
  onAutoSmoothChange={handleAutoSmooth}
790
791
  />
791
792
  <ScaleCurveEditor
793
+ subject={displayLabel ?? label}
792
794
  curveKey="saturation"
793
795
  anchors={saturationCurve}
794
796
  cfg={saturationCurveConfig}
@@ -805,6 +807,7 @@
805
807
  onAutoSmoothChange={handleAutoSmooth}
806
808
  />
807
809
  <ScaleCurveEditor
810
+ subject={displayLabel ?? label}
808
811
  curveKey="lightness"
809
812
  anchors={lightnessCurve}
810
813
  cfg={lightnessCurveConfig}
@@ -1231,6 +1234,61 @@
1231
1234
  line-height: 1;
1232
1235
  }
1233
1236
 
1237
+ /* A phone-width editor stacks the ramp, one step per row, so each swatch is
1238
+ wide enough to read and tap. --stacked-order puts the step editor under the
1239
+ row it edits and the curves after black. */
1240
+ @media (max-width: 600px) {
1241
+ .scale-section {
1242
+ width: 100%;
1243
+ }
1244
+
1245
+ .swatch-grid {
1246
+ display: flex;
1247
+ flex-direction: column;
1248
+ align-items: stretch;
1249
+ width: 100%;
1250
+ }
1251
+
1252
+ .swatch-grid > :global(*) {
1253
+ order: var(--stacked-order, 0);
1254
+ }
1255
+
1256
+ .step-column {
1257
+ display: grid;
1258
+ grid-template-columns: 3rem minmax(0, 1fr) 5rem;
1259
+ align-items: center;
1260
+ column-gap: var(--ui-space-8);
1261
+ }
1262
+
1263
+ .step-label,
1264
+ .step-label.copyable-label {
1265
+ height: auto;
1266
+ justify-content: flex-start;
1267
+ }
1268
+
1269
+ .step-hex {
1270
+ margin-top: 0;
1271
+ text-align: left;
1272
+ }
1273
+
1274
+ .swatch.gray-swatch {
1275
+ height: 2.25rem;
1276
+ margin-bottom: 0;
1277
+ }
1278
+
1279
+ .swatch.gray-swatch.anchored {
1280
+ height: 3rem;
1281
+ }
1282
+
1283
+ .dock {
1284
+ grid-template-columns: minmax(0, 1fr);
1285
+ }
1286
+
1287
+ .edit-caret {
1288
+ display: none;
1289
+ }
1290
+ }
1291
+
1234
1292
  .empty-mode-toggle {
1235
1293
  display: flex;
1236
1294
  align-items: center;
@@ -1146,6 +1146,7 @@
1146
1146
  .palette-detail-row {
1147
1147
  grid-column: 2 / -1;
1148
1148
  display: flex;
1149
+ flex-wrap: wrap;
1149
1150
  align-items: center;
1150
1151
  gap: var(--ui-space-8);
1151
1152
  padding-top: var(--ui-space-4);
@@ -1192,6 +1193,7 @@
1192
1193
 
1193
1194
  .orientation-body {
1194
1195
  display: flex;
1196
+ flex-wrap: wrap;
1195
1197
  align-items: center;
1196
1198
  gap: var(--ui-space-8);
1197
1199
  flex: 1;
@@ -2,7 +2,7 @@
2
2
  import { cubicOut } from 'svelte/easing';
3
3
  import { oklchToHexClamped } from '../../core/palettes/oklch';
4
4
  import { PALETTE_SPECS } from '../../core/palettes/paletteDerivation';
5
- import { editorState, beginSliderGesture } from '../../core/store/editorStore';
5
+ import { editorState, beginSliderGesture, undo, canUndo } from '../../core/store/editorStore';
6
6
  import { selectedPalette } from '../../core/store/paletteFocus';
7
7
  import { applyHarmonyToAxes, axisStatuses, HARMONY_ELIGIBLE, type HarmonyMode } from '../../core/palettes/colorHarmony';
8
8
  import AxisNumeral from './AxisNumeral.svelte';
@@ -13,6 +13,7 @@
13
13
  import PaletteStepStrip from './PaletteStepStrip.svelte';
14
14
  import HarmonyAxesList from './HarmonyAxesList.svelte';
15
15
  import PaletteJumpButton from '../palette/PaletteJumpButton.svelte';
16
+ import UIPillButton from '../UIPillButton.svelte';
16
17
  import { isSwatchCopy, swatchPasteColor, copySwatchColor, showSwatchColor } from '../swatchClipboard';
17
18
  import {
18
19
  setBaseColor,
@@ -151,7 +152,7 @@
151
152
  onSwatchDragEnd();
152
153
  }
153
154
 
154
- const swatchEls: Record<string, HTMLButtonElement> = {};
155
+ const swatchEls: Record<string, HTMLButtonElement> = $state({});
155
156
 
156
157
  // Copy and paste act on the selected swatch, never the focused one: Safari
157
158
  // and macOS Firefox do not focus a button on click.
@@ -270,6 +271,8 @@
270
271
  >
271
272
  {#snippet actions()}
272
273
  <div class="wheel-opts">
274
+ <!-- The editor keeps one history, so this steps back the last edit on any view, as Cmd+Z does. -->
275
+ <UIPillButton size="compact" variant="outline" icon="fa-rotate-left" title="Undo the last edit" disabled={!$canUndo} onclick={() => undo()}>Undo</UIPillButton>
273
276
  <label class="opt">
274
277
  <input type="checkbox" bind:checked={absoluteChroma} />
275
278
  <span>Absolute Chroma</span>
@@ -262,6 +262,7 @@
262
262
  <div class="curve-grid-span">
263
263
  {#each curveDescriptors as curve (curve.key)}
264
264
  <ScaleCurveEditor
265
+ subject={scale.title}
265
266
  curveKey={curve.key}
266
267
  anchors={curve.anchors}
267
268
  cfg={curve.cfg}
@@ -1,6 +1,8 @@
1
1
  <script lang="ts">
2
+ import { MediaQuery } from 'svelte/reactivity';
2
3
  import BezierCurveEditor from '../BezierCurveEditor.svelte';
3
4
  import UIReveal from '../UIReveal.svelte';
5
+ import { portal } from '../../../system/internal/portal';
4
6
  import { isAutoSmoothCurve, type CurveAnchor, type CurveConfig } from '../curveEngine';
5
7
  import { curveSummary } from './curveSummary';
6
8
 
@@ -30,6 +32,8 @@
30
32
  * switching on is self-evident from the shape it produces. */
31
33
  autoSmooth?: boolean;
32
34
  lockedAnchorIndex?: number | null;
35
+ /** The scale the curve shapes, named in the full-screen header. */
36
+ subject?: string;
33
37
  open: boolean;
34
38
  onToggleOpen: () => void;
35
39
  onAnchorsChange: (anchors: CurveAnchor[]) => void;
@@ -47,6 +51,7 @@
47
51
  offset = 0,
48
52
  autoSmooth = undefined,
49
53
  lockedAnchorIndex = null,
54
+ subject = undefined,
50
55
  open,
51
56
  onToggleOpen,
52
57
  onAnchorsChange,
@@ -57,37 +62,78 @@
57
62
 
58
63
  let summary = $derived(curveSummary(anchors, defaults, offset, cfg.unit ?? ''));
59
64
  let autoSmoothResolved = $derived(autoSmooth ?? isAutoSmoothCurve(anchors));
65
+
66
+ // A phone-width editor has no room to drag handles inline, so a curve opens
67
+ // over the whole screen. The layer opens only on a tap: the parent's open
68
+ // flag is the desktop disclosure, and Lightness starts open there.
69
+ const fullScreen = new MediaQuery('max-width: 600px');
70
+ let layerOpen = $state(false);
71
+
72
+ function focusOnMount(node: HTMLElement) {
73
+ node.focus();
74
+ }
60
75
  </script>
61
76
 
77
+ <svelte:window onkeydown={(e) => { if (layerOpen && e.key === 'Escape') layerOpen = false; }} />
78
+
79
+ {#snippet editor()}
80
+ <BezierCurveEditor
81
+ {anchors}
82
+ {cfg}
83
+ {stepCount}
84
+ defaultAnchors={defaults}
85
+ {offset}
86
+ autoSmooth={autoSmoothResolved}
87
+ {lockedAnchorIndex}
88
+ {onLockedAnchorUnlock}
89
+ {onAnchorsChange}
90
+ onOffsetChange={(v) => onOffsetChange(curveKey, v)}
91
+ onAutoSmoothChange={(v) => onAutoSmoothChange(curveKey, v)}
92
+ />
93
+ {/snippet}
94
+
62
95
  <div class="curve-section">
63
96
  <button
64
97
  class="curve-section-toggle"
65
98
  type="button"
66
- aria-expanded={open}
67
- onclick={onToggleOpen}
99
+ aria-expanded={fullScreen.current ? layerOpen : open}
100
+ onclick={() => (fullScreen.current ? (layerOpen = true) : onToggleOpen())}
68
101
  >
69
- <i class="fas" class:fa-chevron-right={!open} class:fa-chevron-down={open} aria-hidden="true"></i>
102
+ {#if fullScreen.current}
103
+ <i class="fas fa-up-right-and-down-left-from-center" aria-hidden="true"></i>
104
+ {:else}
105
+ <i class="fas" class:fa-chevron-right={!open} class:fa-chevron-down={open} aria-hidden="true"></i>
106
+ {/if}
70
107
  <span class="curve-section-label">{cfg.label}</span>
71
108
  <span class="curve-section-summary">
72
109
  {#if summary !== 'default'}<span class="curve-section-dot" aria-hidden="true"></span>{/if}
73
110
  {summary}
74
111
  </span>
75
112
  </button>
76
- <UIReveal {open}>
77
- <BezierCurveEditor
78
- {anchors}
79
- {cfg}
80
- {stepCount}
81
- defaultAnchors={defaults}
82
- {offset}
83
- autoSmooth={autoSmoothResolved}
84
- {lockedAnchorIndex}
85
- {onLockedAnchorUnlock}
86
- {onAnchorsChange}
87
- onOffsetChange={(v) => onOffsetChange(curveKey, v)}
88
- onAutoSmoothChange={(v) => onAutoSmoothChange(curveKey, v)}
89
- />
90
- </UIReveal>
113
+ {#if fullScreen.current}
114
+ {#if layerOpen}
115
+ <!-- editor-page carries the --ui-* tokens to the layer's new place in body. -->
116
+ <div class="curve-full-screen editor-page" role="dialog" aria-modal="true" aria-label="{subject ? `${subject} ` : ''}{cfg.label} curve" use:portal>
117
+ <header class="curve-full-screen-bar">
118
+ <span class="curve-full-screen-title">
119
+ {#if subject}<span class="curve-full-screen-subject">{subject}</span>{/if}
120
+ <span class="curve-section-label">{cfg.label}</span>
121
+ </span>
122
+ <span class="curve-section-summary">{summary}</span>
123
+ <button type="button" class="curve-full-screen-close" aria-label="Close the {cfg.label} curve" onclick={() => (layerOpen = false)} use:focusOnMount>
124
+ <i class="fas fa-xmark" aria-hidden="true"></i>
125
+ </button>
126
+ </header>
127
+ <div class="curve-full-screen-body">
128
+ {@render editor()}
129
+ </div>
130
+ </div>
131
+ {/if}
132
+ {:else}
133
+ <UIReveal {open}>
134
+ {@render editor()}
135
+ </UIReveal>
136
+ {/if}
91
137
  </div>
92
138
 
93
139
  <style>
@@ -136,6 +182,69 @@
136
182
  color: var(--ui-text-muted);
137
183
  }
138
184
 
185
+ .curve-full-screen {
186
+ position: fixed;
187
+ inset: 0;
188
+ z-index: 100;
189
+ display: flex;
190
+ flex-direction: column;
191
+ background: black;
192
+ color: var(--ui-text-primary);
193
+ }
194
+
195
+ .curve-full-screen-bar {
196
+ display: flex;
197
+ align-items: center;
198
+ gap: var(--ui-space-8);
199
+ padding: var(--ui-space-8) var(--ui-space-12);
200
+ border-bottom: 1px solid var(--ui-border-low);
201
+ }
202
+
203
+ .curve-full-screen-title {
204
+ display: flex;
205
+ align-items: baseline;
206
+ gap: var(--ui-space-8);
207
+ min-width: 0;
208
+ }
209
+
210
+ .curve-full-screen-subject {
211
+ font-size: var(--ui-font-size-md);
212
+ color: var(--ui-text-tertiary);
213
+ }
214
+
215
+ .curve-full-screen-close {
216
+ display: flex;
217
+ align-items: center;
218
+ justify-content: center;
219
+ flex: none;
220
+ width: 40px;
221
+ height: 40px;
222
+ background: none;
223
+ border: none;
224
+ border-radius: var(--ui-radius-md);
225
+ color: var(--ui-text-primary);
226
+ font-size: var(--ui-font-size-lg);
227
+ cursor: pointer;
228
+ }
229
+
230
+ .curve-full-screen-close:hover {
231
+ background: var(--ui-hover);
232
+ }
233
+
234
+ .curve-full-screen-body {
235
+ flex: 1;
236
+ min-height: 0;
237
+ overflow-y: auto;
238
+ padding: var(--ui-space-12);
239
+ container-type: inline-size;
240
+ }
241
+
242
+ /* The handles scale with the plot's height, so a plot stretched to a tall
243
+ screen balloons them. Square, up to 60% of the screen. */
244
+ .curve-full-screen-body :global(.curve-container) {
245
+ height: min(100cqw, 60vh);
246
+ }
247
+
139
248
  .curve-section-dot {
140
249
  width: 0.375rem;
141
250
  height: 0.375rem;