@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.
- package/.claude/skills/live-tokens-create-page/SKILL.md +7 -3
- package/CHANGELOG.md +108 -0
- package/README.md +6 -6
- package/dist-plugin/{chunk-7XSUHJUH.js → chunk-4J6JPSVS.js} +53 -2
- package/dist-plugin/{chunk-W5VQV32K.js → chunk-GYRULB2X.js} +1 -1
- package/dist-plugin/{chunk-ONYAIXCZ.js → chunk-QQASYPW7.js} +32 -9
- package/dist-plugin/index.cjs +598 -17
- package/dist-plugin/index.d.cts +9 -1
- package/dist-plugin/index.d.ts +9 -1
- package/dist-plugin/index.js +513 -6
- package/dist-plugin/migrateData/index.cjs +32 -9
- package/dist-plugin/migrateData/index.js +2 -2
- package/dist-plugin/setColors/index.cjs +32 -22
- package/dist-plugin/setColors/index.js +2 -15
- package/dist-plugin/setGeometry/index.cjs +32 -9
- package/dist-plugin/setGeometry/index.js +1 -1
- package/dist-plugin/tokensCssMigrations/index.cjs +53 -2
- package/dist-plugin/tokensCssMigrations/index.d.cts +9 -2
- package/dist-plugin/tokensCssMigrations/index.d.ts +9 -2
- package/dist-plugin/tokensCssMigrations/index.js +1 -1
- package/package.json +8 -3
- package/src/demo/ChromeSelect.svelte +111 -0
- package/src/demo/Demo.svelte +39 -5
- package/src/demo/DemoBar.svelte +33 -14
- package/src/demo/FloatingTokenTags.css +33 -4
- package/src/demo/FloatingTokenTags.svelte +81 -68
- package/src/demo/Section.svelte +1 -7
- package/src/demo/SketchSelect.svelte +36 -12
- package/src/demo/SourceLinks.svelte +5 -4
- package/src/demo/TestingLoops.svelte +128 -19
- package/src/demo/ThemeSelect.svelte +34 -11
- package/src/demo/evalResults.ts +7 -7
- package/src/demo/sections/SectionClaude.svelte +20 -13
- package/src/demo/sections/SectionFooter.svelte +4 -5
- package/src/demo/sections/SectionGetStarted.svelte +5 -6
- package/src/demo/sections/SectionHero.svelte +43 -23
- package/src/demo/sections/SectionKit.svelte +141 -45
- package/src/demo/sections/SectionLayers.svelte +3 -3
- package/src/demo/sections/SectionOffering.svelte +8 -23
- package/src/editor/bootstrap.ts +12 -6
- package/src/editor/component-editor/ImageEditor.svelte +1 -1
- package/src/editor/component-editor/ImageLightboxEditor.svelte +3 -4
- package/src/editor/component-editor/registration.ts +36 -0
- package/src/editor/component-editor/registry.ts +5 -12
- package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +11 -2
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +7 -0
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +1 -2
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +17 -2
- package/src/editor/core/store/editorPersistence.ts +6 -1
- package/src/editor/core/store/editorRenderer.ts +1 -4
- package/src/editor/core/store/editorStore.ts +5 -9
- package/src/editor/core/store/editorTypes.ts +2 -2
- package/src/editor/core/themes/migrations/2026-10-15-responsive-columns.ts +40 -0
- package/src/editor/core/themes/migrations/index.ts +2 -0
- package/src/editor/core/themes/slices/columns.ts +112 -42
- package/src/editor/docs/Docs.svelte +25 -22
- package/src/editor/docs/chapters.ts +1 -0
- package/src/editor/docs/content/editing-tokens.md +5 -3
- package/src/editor/docs/content/production-css.md +116 -0
- package/src/editor/docs/content/themes-workflow.md +2 -0
- package/src/editor/docs/content.generated.ts +3 -2
- package/src/editor/index.ts +2 -1
- package/src/editor/overlay/LiveEditorOverlay.svelte +67 -19
- package/src/editor/overlay/LiveTokensRouter.svelte +8 -4
- package/src/editor/pages/Editor.svelte +15 -1
- package/src/editor/pages/EditorShell.svelte +312 -99
- package/src/editor/skill-atlas/SkillAtlas.svelte +111 -24
- package/src/editor/skill-atlas/TreeCanvas.svelte +14 -0
- package/src/editor/skill-atlas/skillSources.generated.ts +1 -1
- package/src/editor/skill-atlas/trees/create-page.ts +24 -24
- package/src/editor/skill-atlas/wireLayout.ts +62 -0
- package/src/editor/ui/BezierCurveEditor.svelte +3 -0
- package/src/editor/ui/FontStackEditor.svelte +2 -2
- package/src/editor/ui/PaletteEditor.svelte +64 -6
- package/src/editor/ui/UIPaletteSelector.svelte +2 -0
- package/src/editor/ui/colors/ColorsTab.svelte +5 -2
- package/src/editor/ui/palette/OverridesPanel.svelte +1 -0
- package/src/editor/ui/palette/ScaleCurveEditor.svelte +127 -18
- package/src/editor/ui/sections/ColumnsSection.svelte +79 -49
- package/src/editor/ui/sections/ShadowsSection.svelte +14 -0
- package/src/editor/ui/sections/WashesSection.svelte +11 -0
- package/src/editor/ui/sketch/SketchTab.svelte +1 -1
- package/src/live-tokens/data/colors-and-type/autumn.json +1 -4
- package/src/live-tokens/data/colors-and-type/default.json +1 -4
- package/src/live-tokens/data/colors-and-type/halloween.json +1 -4
- package/src/live-tokens/data/colors-and-type/midnight-study.json +1 -4
- package/src/live-tokens/data/colors-and-type/ocean.json +1 -4
- package/src/live-tokens/data/colors-and-type/royal-velvet.json +1 -4
- package/src/live-tokens/data/colors-and-type/sketchy.json +1 -4
- package/src/live-tokens/data/colors-and-type/spring-meadow.json +1 -4
- package/src/live-tokens/data/colors-and-type/sunset.json +1 -4
- package/src/live-tokens/data/themes/autumn.json +1 -4
- package/src/live-tokens/data/themes/halloween.json +1 -4
- package/src/live-tokens/data/themes/midnight-study.json +1 -4
- package/src/live-tokens/data/themes/ocean.json +1 -4
- package/src/live-tokens/data/themes/royal-velvet.json +1 -4
- package/src/live-tokens/data/themes/sketchy.json +1 -4
- package/src/live-tokens/data/themes/spring-meadow.json +1 -4
- package/src/live-tokens/data/themes/sunset.json +1 -4
- package/src/live-tokens/data/tokens.generated.css +1 -4
- package/src/system/components/SectionDivider.svelte +4 -0
- package/src/system/styles/CONVENTIONS.md +1 -1
- package/src/system/styles/tokens.css +30 -4
- package/template/src/main.ts +2 -2
- package/template/src/pages/Home.svelte +11 -8
- package/template/vite.config.ts +7 -3
- package/src/system/assets/offering.webp +0 -0
- /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>
|
|
@@ -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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
{cfg}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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;
|