@motion-proto/live-tokens 0.46.1 → 0.47.1
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/CHANGELOG.md +63 -0
- package/dist-plugin/index.cjs +52 -2
- package/dist-plugin/index.js +52 -2
- package/package.json +1 -1
- package/src/editor/core/palettes/paletteDerivation.ts +45 -3
- package/src/editor/core/store/paletteFocus.ts +26 -0
- package/src/editor/ui/BezierCurveEditor.svelte +11 -4
- package/src/editor/ui/ColorEditPanel.svelte +1 -1
- package/src/editor/ui/PaletteEditor.svelte +79 -13
- package/src/editor/ui/UIMenuButton.svelte +14 -0
- package/src/editor/ui/UIPillButton.svelte +7 -2
- package/src/editor/ui/colors/ColorWheel.svelte +34 -1
- package/src/editor/ui/colors/ColorsTab.svelte +16 -4
- package/src/editor/ui/colors/HarmonyAxesList.svelte +44 -9
- package/src/editor/ui/colors/paletteBaseColor.ts +30 -10
- package/src/editor/ui/curveEngine.ts +48 -0
- package/src/editor/ui/palette/PaletteBase.svelte +81 -42
- package/src/editor/ui/palette/PaletteJumpButton.svelte +44 -0
- package/src/editor/ui/palette/ScaleCurveEditor.svelte +4 -1
- package/src/live-tokens/data/themes/default.json +571 -461
- package/src/live-tokens/data/tokens.generated.css +118 -118
|
@@ -62,6 +62,8 @@
|
|
|
62
62
|
const EXT_OFFSET = 20; // external handle radius beyond the disc rim (room for the dotted tether)
|
|
63
63
|
const NUM_OFFSET = 24; // numeral radius beyond the external handles
|
|
64
64
|
const FREE_NUM_OFFSET = 15; // free-dot numeral, diagonal clearance past the enlarged dot
|
|
65
|
+
const NUM_PERP = 20; // sideways clearance for a numeral moved beside its own marker
|
|
66
|
+
const NUM_STACK = 20; // radial gap between the numerals of co-hued unbound axes
|
|
65
67
|
const MIN_SIZE = 240;
|
|
66
68
|
const MAX_SIZE = 560;
|
|
67
69
|
|
|
@@ -128,6 +130,7 @@
|
|
|
128
130
|
return {
|
|
129
131
|
...t,
|
|
130
132
|
hue,
|
|
133
|
+
dotR,
|
|
131
134
|
selected: t.bound && selected === t.family,
|
|
132
135
|
dot: { x: center + dotR * Math.cos(rad(hue)), y: center - dotR * Math.sin(rad(hue)) },
|
|
133
136
|
ext: { x: center + extRadius * Math.cos(rad(hue)), y: center - extRadius * Math.sin(rad(hue)) },
|
|
@@ -141,7 +144,37 @@
|
|
|
141
144
|
}),
|
|
142
145
|
);
|
|
143
146
|
|
|
144
|
-
|
|
147
|
+
// Monochromatic stacks every axis on one hue (custom does too, once the hues
|
|
148
|
+
// are dragged together), so the outer numeral slot is the same point for all
|
|
149
|
+
// of them. Each numeral then moves beside the marker it names — its own dot,
|
|
150
|
+
// or the external handle when the axis carries no color — which is the only
|
|
151
|
+
// thing that still tells the axes apart on a shared rail.
|
|
152
|
+
let visibleRender = $derived.by(() => {
|
|
153
|
+
const vis = axisRender.filter(isVisible);
|
|
154
|
+
const hueKey = (hue: number) => Math.round(normDeg(hue)) % 360;
|
|
155
|
+
const byHue = new Map<number, typeof vis>();
|
|
156
|
+
for (const t of vis) {
|
|
157
|
+
const group = byHue.get(hueKey(t.hue));
|
|
158
|
+
if (group) group.push(t);
|
|
159
|
+
else byHue.set(hueKey(t.hue), [t]);
|
|
160
|
+
}
|
|
161
|
+
return vis.map((t) => {
|
|
162
|
+
const group = byHue.get(hueKey(t.hue))!;
|
|
163
|
+
if (group.length < 2) return t;
|
|
164
|
+
// Bound axes separate by chroma radius on their own; unbound ones share the
|
|
165
|
+
// handle track, so they step outward past it instead.
|
|
166
|
+
const unboundBefore = group.slice(0, group.indexOf(t)).filter((g) => !g.bound).length;
|
|
167
|
+
const r = t.bound ? t.dotR : extRadius + unboundBefore * NUM_STACK;
|
|
168
|
+
const a = rad(t.hue);
|
|
169
|
+
return {
|
|
170
|
+
...t,
|
|
171
|
+
num: {
|
|
172
|
+
x: center + r * Math.cos(a) - NUM_PERP * Math.sin(a),
|
|
173
|
+
y: center - r * Math.sin(a) - NUM_PERP * Math.cos(a),
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
});
|
|
177
|
+
});
|
|
145
178
|
|
|
146
179
|
// Free dot for a selected family on no ACTIVE axis (Neutral, Alternate, …,
|
|
147
180
|
// plus families whose axis the applied geometry deals no distinct slot): a 2D
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { oklchToHexClamped } from '../../core/palettes/oklch';
|
|
4
4
|
import { PALETTE_SPECS } from '../../core/palettes/paletteDerivation';
|
|
5
5
|
import { editorState, beginSliderGesture } from '../../core/store/editorStore';
|
|
6
|
+
import { selectedPalette } from '../../core/store/paletteFocus';
|
|
6
7
|
import { applyHarmonyToAxes, axisStatuses, HARMONY_ELIGIBLE, type HarmonyMode } from '../../core/palettes/colorHarmony';
|
|
7
8
|
import AxisNumeral from './AxisNumeral.svelte';
|
|
8
9
|
import ColorEditPanel from '../ColorEditPanel.svelte';
|
|
@@ -11,6 +12,7 @@
|
|
|
11
12
|
import ColorStory from './ColorStory.svelte';
|
|
12
13
|
import PaletteStepStrip from './PaletteStepStrip.svelte';
|
|
13
14
|
import HarmonyAxesList from './HarmonyAxesList.svelte';
|
|
15
|
+
import PaletteJumpButton from '../palette/PaletteJumpButton.svelte';
|
|
14
16
|
import {
|
|
15
17
|
setBaseColor,
|
|
16
18
|
setAxisHues,
|
|
@@ -21,7 +23,9 @@
|
|
|
21
23
|
import { HARMONY_DRAG_TYPE, startFamilyDrag } from './harmonyDrag';
|
|
22
24
|
import { maxChroma } from './colorWheelMath';
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
// Shared with the Tokens view: its wheel link hands a family over by writing
|
|
27
|
+
// the store, and this view is already showing it when the switch lands.
|
|
28
|
+
let selected = $derived($selectedPalette);
|
|
25
29
|
let activeMode = $state<HarmonyMode>('custom');
|
|
26
30
|
// Local UI only — deliberately NOT stored in PaletteConfig (shape unchanged).
|
|
27
31
|
let absoluteChroma = $state(false);
|
|
@@ -31,7 +35,7 @@
|
|
|
31
35
|
let axesInfoWrap: HTMLElement | undefined = $state();
|
|
32
36
|
|
|
33
37
|
function select(label: string) {
|
|
34
|
-
|
|
38
|
+
selectedPalette.set(label);
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
function toggleInfo() {
|
|
@@ -285,7 +289,7 @@
|
|
|
285
289
|
{#if axesInfoOpen}
|
|
286
290
|
<div class="info-box" role="region" aria-label="Harmony axes">
|
|
287
291
|
<strong class="info-title">Harmony axes</strong>
|
|
288
|
-
<p>Each axis owns a hue. Assign a color from the row menu, or drag one onto the axis.
|
|
292
|
+
<p>Each axis owns a hue. Assign a color from the row menu, or drag one onto the axis. On assign picks which hue survives: Adopt swatch moves the axis to the color's hue, Adopt axis repaints the color to the hue the axis already holds, which keeps an applied harmony true. Either way the color follows the axis from then on, so applying a harmony rotates every assigned color at once. To unassign, use the row menu, or drag the color off its axis and drop it anywhere else.</p>
|
|
289
293
|
</div>
|
|
290
294
|
{/if}
|
|
291
295
|
</div>
|
|
@@ -305,8 +309,9 @@
|
|
|
305
309
|
</section>
|
|
306
310
|
|
|
307
311
|
<section id="colors-selected" class="block">
|
|
308
|
-
<header class="block-head">
|
|
312
|
+
<header class="block-head head-row">
|
|
309
313
|
<h2 class="title">Palette <span class="mode-name">· {selectedSpec.displayLabel ?? selected}</span></h2>
|
|
314
|
+
<PaletteJumpButton family={selected} displayLabel={selectedSpec.displayLabel} target="tokens" />
|
|
310
315
|
</header>
|
|
311
316
|
|
|
312
317
|
<div class="group">
|
|
@@ -380,6 +385,13 @@
|
|
|
380
385
|
gap: var(--ui-space-2);
|
|
381
386
|
}
|
|
382
387
|
|
|
388
|
+
.block-head.head-row {
|
|
389
|
+
flex-direction: row;
|
|
390
|
+
align-items: center;
|
|
391
|
+
justify-content: space-between;
|
|
392
|
+
gap: var(--ui-space-12);
|
|
393
|
+
}
|
|
394
|
+
|
|
383
395
|
/* Every labeled region on this page carries the same heading, section and
|
|
384
396
|
group alike: the old xs tertiary eyebrows read as captions and got skipped. */
|
|
385
397
|
.title {
|
|
@@ -7,8 +7,9 @@
|
|
|
7
7
|
import UIMenuButton from '../UIMenuButton.svelte';
|
|
8
8
|
import UIOptionItem from '../UIOptionItem.svelte';
|
|
9
9
|
import UIOptionList from '../UIOptionList.svelte';
|
|
10
|
+
import UISegmentedControl from '../UISegmentedControl.svelte';
|
|
10
11
|
import { modeLabel } from './harmonyModeIcons';
|
|
11
|
-
import { bindFamilyToAxis, unbindFamily } from './paletteBaseColor';
|
|
12
|
+
import { bindFamilyToAxis, unbindFamily, type AdoptOnAssign } from './paletteBaseColor';
|
|
12
13
|
import { HARMONY_DRAG_TYPE, startFamilyDrag } from './harmonyDrag';
|
|
13
14
|
|
|
14
15
|
interface Props {
|
|
@@ -24,8 +25,23 @@
|
|
|
24
25
|
|
|
25
26
|
let { activeMode, selected, oncustomize }: Props = $props();
|
|
26
27
|
|
|
28
|
+
const ADOPT_OPTIONS = [
|
|
29
|
+
{
|
|
30
|
+
value: 'axis',
|
|
31
|
+
label: 'Adopt axis',
|
|
32
|
+
title: 'The color moves to the hue its new axis holds, so an applied harmony stays true',
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
value: 'swatch',
|
|
36
|
+
label: 'Adopt swatch',
|
|
37
|
+
title: "The axis moves to the color's hue, which leaves the harmony custom",
|
|
38
|
+
},
|
|
39
|
+
] as const satisfies ReadonlyArray<{ value: AdoptOnAssign; label: string; title: string }>;
|
|
40
|
+
|
|
41
|
+
let adopt = $state<AdoptOnAssign>('swatch');
|
|
42
|
+
|
|
27
43
|
function assign(family: string, index: number) {
|
|
28
|
-
if (bindFamilyToAxis(family, index)) oncustomize();
|
|
44
|
+
if (bindFamilyToAxis(family, index, adopt)) oncustomize();
|
|
29
45
|
}
|
|
30
46
|
|
|
31
47
|
// Empty-row swatch shows the axis's own hue, at fixed preview L/C so only the
|
|
@@ -194,6 +210,15 @@
|
|
|
194
210
|
{/snippet}
|
|
195
211
|
|
|
196
212
|
<div class="axes-list" bind:this={listEl}>
|
|
213
|
+
<div class="adopt-row">
|
|
214
|
+
<span class="adopt-caption">On assign</span>
|
|
215
|
+
<UISegmentedControl
|
|
216
|
+
bind:value={adopt}
|
|
217
|
+
options={ADOPT_OPTIONS}
|
|
218
|
+
ariaLabel="Which hue survives an assignment"
|
|
219
|
+
/>
|
|
220
|
+
</div>
|
|
221
|
+
<span class="anchor-caption">Anchor</span>
|
|
197
222
|
{#each axes as axis, i (i)}
|
|
198
223
|
{@const status = statuses[i]}
|
|
199
224
|
{@const unused = status === 'unused'}
|
|
@@ -215,7 +240,6 @@
|
|
|
215
240
|
ondragleave={() => onDragLeave(i)}
|
|
216
241
|
ondrop={(e) => onDrop(e, i)}
|
|
217
242
|
>
|
|
218
|
-
<span class="role">{i === 0 ? 'Anchor' : ''}</span>
|
|
219
243
|
{#if axis.family !== null}
|
|
220
244
|
{@const family = axis.family}
|
|
221
245
|
<span class="swatch" style="--fill: {familyHex(family)}"></span>
|
|
@@ -317,13 +341,24 @@
|
|
|
317
341
|
background: var(--ui-surface-low);
|
|
318
342
|
}
|
|
319
343
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
344
|
+
.adopt-row {
|
|
345
|
+
display: flex;
|
|
346
|
+
align-items: center;
|
|
347
|
+
gap: var(--ui-space-8);
|
|
348
|
+
margin-bottom: var(--ui-space-4);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.adopt-caption {
|
|
326
352
|
color: var(--ui-text-secondary);
|
|
353
|
+
font-size: var(--ui-font-size-xs);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/* Names the first row alone. Sitting above the list rather than in a column
|
|
357
|
+
every row reserves, so no row carries dead space for a word only one has. */
|
|
358
|
+
.anchor-caption {
|
|
359
|
+
padding-inline-start: calc(var(--ui-space-8) + 1px);
|
|
360
|
+
color: var(--ui-text-secondary);
|
|
361
|
+
font-size: var(--ui-font-size-xs);
|
|
327
362
|
}
|
|
328
363
|
|
|
329
364
|
/* Unused slot: nothing bound and no position dealt. The row states the reason
|
|
@@ -145,26 +145,31 @@ function applyAxisHue(s: EditorState, index: number, hue: number, chroma: number
|
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
-
/**
|
|
148
|
+
/** Which side of an assignment keeps its hue: `swatch` moves the axis onto the
|
|
149
|
+
* color, `axis` repaints the color onto the axis. */
|
|
150
|
+
export type AdoptOnAssign = 'swatch' | 'axis';
|
|
151
|
+
|
|
152
|
+
/** Bind a family to an axis; `adopt` decides which hue survives. Trade-places
|
|
149
153
|
* semantics: whatever occupied the destination takes the source's position
|
|
150
|
-
* (another axis,
|
|
154
|
+
* (another axis, reconciled the same way, or unassigned). One mutate.
|
|
151
155
|
* Returns whether any axis hue moved, which is what makes an applied harmony
|
|
152
156
|
* mode untrue — the caller drops to Custom on that. */
|
|
153
|
-
export function bindFamilyToAxis(family: string, index: number): boolean {
|
|
157
|
+
export function bindFamilyToAxis(family: string, index: number, adopt: AdoptOnAssign): boolean {
|
|
154
158
|
const srcIndex = get(editorState).harmonyAxes.findIndex((a) => a.family === family);
|
|
155
159
|
if (srcIndex === index) return false;
|
|
160
|
+
const reconcile = adopt === 'swatch' ? takeSeedHue : giveAxisHue;
|
|
156
161
|
let hueMoved = false;
|
|
157
162
|
mutate(`colors: assign ${family}`, (s) => {
|
|
158
163
|
const axes = s.harmonyAxes;
|
|
159
164
|
const occupant = axes[index].family;
|
|
160
165
|
axes[index].family = family;
|
|
161
|
-
hueMoved =
|
|
162
|
-
// Source was another axis: the occupant trades into it
|
|
163
|
-
//
|
|
164
|
-
//
|
|
166
|
+
hueMoved = reconcile(s, index, family);
|
|
167
|
+
// Source was another axis: the occupant trades into it and reconciles with
|
|
168
|
+
// it in turn. Source was unassigned (-1): the occupant floats free, and
|
|
169
|
+
// keeps whatever hue this assignment left it.
|
|
165
170
|
if (srcIndex !== -1) {
|
|
166
171
|
axes[srcIndex].family = occupant;
|
|
167
|
-
if (occupant !== null) hueMoved =
|
|
172
|
+
if (occupant !== null) hueMoved = reconcile(s, srcIndex, occupant) || hueMoved;
|
|
168
173
|
}
|
|
169
174
|
});
|
|
170
175
|
return hueMoved;
|
|
@@ -179,8 +184,8 @@ export function unbindFamily(family: string): void {
|
|
|
179
184
|
});
|
|
180
185
|
}
|
|
181
186
|
|
|
182
|
-
/**
|
|
183
|
-
*
|
|
187
|
+
/** The axis adopts the swatch: the seed the user chose is never repainted, so
|
|
188
|
+
* the axis is what moves. */
|
|
184
189
|
function takeSeedHue(s: EditorState, index: number, family: string): boolean {
|
|
185
190
|
const hue = normHue(ensureConfig(s, family).baseColor.h);
|
|
186
191
|
if (s.harmonyAxes[index].hue === hue) return false;
|
|
@@ -188,6 +193,21 @@ function takeSeedHue(s: EditorState, index: number, family: string): boolean {
|
|
|
188
193
|
return true;
|
|
189
194
|
}
|
|
190
195
|
|
|
196
|
+
/** The swatch adopts the axis: the color lands on the hue its new slot holds,
|
|
197
|
+
* which leaves the geometry untouched — hence never a hue move to report, so
|
|
198
|
+
* an applied harmony mode survives assignment. */
|
|
199
|
+
function giveAxisHue(s: EditorState, index: number, family: string): boolean {
|
|
200
|
+
// The axis hue is taken raw: the color must land exactly on it, and normHue
|
|
201
|
+
// is not identity in range — its round trip shifts the last bit.
|
|
202
|
+
const hue = s.harmonyAxes[index].hue;
|
|
203
|
+
const cfg = ensureConfig(s, family);
|
|
204
|
+
if (cfg.baseColor.h !== hue) {
|
|
205
|
+
cfg.baseColor = { l: cfg.baseColor.l, c: cfg.baseColor.c, h: hue };
|
|
206
|
+
syncBaseAnchor(cfg);
|
|
207
|
+
}
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
191
211
|
/** Set several seed colors in ONE undo entry (harmony apply, global rotate). */
|
|
192
212
|
export function setBaseColors(patch: Record<string, Oklch>, historyLabel = 'colors: harmony'): void {
|
|
193
213
|
transaction(historyLabel, (s) => {
|
|
@@ -97,6 +97,54 @@ export function makeAnchor(x: number, y: number, tangentLen = 15): CurveAnchor {
|
|
|
97
97
|
return { x, y, inDx: -tangentLen, inDy: 0, outDx: tangentLen, outDy: 0 };
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/** Share of a segment's x-span a tangent handle reaches across. At a third the
|
|
101
|
+
* cubic is the exact Hermite form of the slope, so the handle length carries no
|
|
102
|
+
* shape of its own, and the two facing handles can never cross. */
|
|
103
|
+
const HANDLE_SPAN = 1 / 3;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Slope to carry through (x, y), given whichever neighbours it has.
|
|
107
|
+
*
|
|
108
|
+
* The weighted harmonic mean of the two secants (Fritsch–Carlson): it always
|
|
109
|
+
* lands between them, so the segment cannot overshoot its own endpoints. The
|
|
110
|
+
* plain average (Catmull-Rom) can overshoot when the gaps differ, and on a
|
|
111
|
+
* lightness ramp an overshoot puts a step out of order with its neighbours,
|
|
112
|
+
* which is the one thing a ramp must not do.
|
|
113
|
+
*/
|
|
114
|
+
function tangentSlope(
|
|
115
|
+
prev: CurveAnchor | null,
|
|
116
|
+
x: number,
|
|
117
|
+
y: number,
|
|
118
|
+
next: CurveAnchor | null,
|
|
119
|
+
): number {
|
|
120
|
+
const hPrev = prev ? x - prev.x : 0;
|
|
121
|
+
const hNext = next ? next.x - x : 0;
|
|
122
|
+
const dPrev = hPrev > 0 ? (y - prev!.y) / hPrev : null;
|
|
123
|
+
const dNext = hNext > 0 ? (next!.y - y) / hNext : null;
|
|
124
|
+
if (dPrev === null) return dNext ?? 0;
|
|
125
|
+
if (dNext === null) return dPrev;
|
|
126
|
+
// Secants disagreeing in sign make this a turning point, where flat is the
|
|
127
|
+
// true slope rather than a failure to find one.
|
|
128
|
+
if (dPrev * dNext <= 0) return 0;
|
|
129
|
+
const wPrev = 2 * hNext + hPrev;
|
|
130
|
+
const wNext = hNext + 2 * hPrev;
|
|
131
|
+
return (wPrev + wNext) / (wPrev / dPrev + wNext / dNext);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** An anchor at (x, y) whose handles lie along the local slope, so dropping it
|
|
135
|
+
* onto a run bends the line rather than flattening it. */
|
|
136
|
+
export function tangentAnchor(
|
|
137
|
+
x: number,
|
|
138
|
+
y: number,
|
|
139
|
+
prev: CurveAnchor | null,
|
|
140
|
+
next: CurveAnchor | null,
|
|
141
|
+
): CurveAnchor {
|
|
142
|
+
const m = tangentSlope(prev, x, y, next);
|
|
143
|
+
const inDx = prev ? -(x - prev.x) * HANDLE_SPAN : 0;
|
|
144
|
+
const outDx = next ? (next.x - x) * HANDLE_SPAN : 0;
|
|
145
|
+
return { x, y, inDx, inDy: m * inDx, outDx, outDy: m * outDx };
|
|
146
|
+
}
|
|
147
|
+
|
|
100
148
|
export function isCornerAnchor(a: CurveAnchor): boolean {
|
|
101
149
|
return a.inDx === 0 && a.inDy === 0 && a.outDx === 0 && a.outDy === 0;
|
|
102
150
|
}
|
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
anchorStepLabel?: string | null;
|
|
33
33
|
isEditingBase: boolean;
|
|
34
34
|
panelOpen: boolean;
|
|
35
|
+
/** Keep the panel open in live-apply mode (no confirm/cancel session) while the curve editors are visible. */
|
|
36
|
+
pinnedOpen?: boolean;
|
|
35
37
|
editingColor: Oklch | null;
|
|
36
38
|
editPanelTitle: string | null;
|
|
37
39
|
copiedKey: string | null;
|
|
@@ -52,6 +54,7 @@
|
|
|
52
54
|
anchorStepLabel = null,
|
|
53
55
|
isEditingBase,
|
|
54
56
|
panelOpen,
|
|
57
|
+
pinnedOpen = false,
|
|
55
58
|
editingColor,
|
|
56
59
|
editPanelTitle,
|
|
57
60
|
copiedKey,
|
|
@@ -73,7 +76,7 @@
|
|
|
73
76
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
74
77
|
<div
|
|
75
78
|
class="header-swatch"
|
|
76
|
-
class:active={isEditingBase}
|
|
79
|
+
class:active={isEditingBase || pinnedOpen}
|
|
77
80
|
style="background: {baseHex}"
|
|
78
81
|
onclick={onStartEdit}
|
|
79
82
|
role="button"
|
|
@@ -83,40 +86,56 @@
|
|
|
83
86
|
<div class="primary-info">
|
|
84
87
|
<span class="editor-label">{displayLabel ?? label}</span>
|
|
85
88
|
<button
|
|
86
|
-
class="base-hex
|
|
89
|
+
class="base-hex"
|
|
90
|
+
class:copied={copiedKey === '__base__'}
|
|
87
91
|
type="button"
|
|
92
|
+
title="Copy {baseHex}"
|
|
88
93
|
onclick={(e) => onCopyBaseHex('__base__', baseHex, e)}
|
|
89
|
-
>
|
|
94
|
+
>
|
|
95
|
+
<span class="hex-value">{baseHex}</span>
|
|
96
|
+
<i
|
|
97
|
+
class="fas"
|
|
98
|
+
class:fa-copy={copiedKey !== '__base__'}
|
|
99
|
+
class:fa-check={copiedKey === '__base__'}
|
|
100
|
+
aria-hidden="true"
|
|
101
|
+
></i>
|
|
102
|
+
</button>
|
|
90
103
|
</div>
|
|
91
104
|
</div>
|
|
92
|
-
</div>
|
|
93
105
|
|
|
94
|
-
{#if isEditingBase && panelOpen && editingColor}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
106
|
+
{#if pinnedOpen || (isEditingBase && panelOpen && editingColor)}
|
|
107
|
+
<div class="editor-controls">
|
|
108
|
+
<ColorEditPanel
|
|
109
|
+
title={isEditingBase ? editPanelTitle : 'Base Color'}
|
|
110
|
+
showRemoveOverride={false}
|
|
111
|
+
hideActions={!isEditingBase}
|
|
112
|
+
hidePreview
|
|
113
|
+
hue={baseOklch.h}
|
|
114
|
+
chroma={baseOklch.c}
|
|
115
|
+
lightness={baseOklch.l * 100}
|
|
116
|
+
chromaMax={CHROMA_MAX}
|
|
117
|
+
chromaHint={pickerChromaHint}
|
|
118
|
+
onHueChromaChange={onBaseChange}
|
|
119
|
+
onConfirm={onConfirm}
|
|
120
|
+
onCancel={onCancel}
|
|
121
|
+
onRemoveOverride={() => {}}
|
|
122
|
+
onSliderStart={() => beginSliderGesture(`edit ${label} base`)}
|
|
123
|
+
>
|
|
124
|
+
{#snippet actions()}
|
|
125
|
+
<Toggle
|
|
126
|
+
checked={anchorToBase}
|
|
127
|
+
onchange={(v) => onAnchorToBaseChange(v ?? !anchorToBase)}
|
|
128
|
+
label={anchorStepLabel ? `Base color placed at ${anchorStepLabel}` : 'Place base color in palette'}
|
|
129
|
+
/>
|
|
130
|
+
{/snippet}
|
|
131
|
+
</ColorEditPanel>
|
|
132
|
+
</div>
|
|
133
|
+
{/if}
|
|
134
|
+
</div>
|
|
118
135
|
|
|
119
136
|
<style>
|
|
137
|
+
/* Identity and controls read as one band: the sliders sit beside the swatch
|
|
138
|
+
rather than under it, which is what leaves the palette below its height. */
|
|
120
139
|
.editor-top {
|
|
121
140
|
display: flex;
|
|
122
141
|
align-items: flex-start;
|
|
@@ -126,20 +145,28 @@
|
|
|
126
145
|
|
|
127
146
|
.editor-primary {
|
|
128
147
|
display: flex;
|
|
129
|
-
align-items:
|
|
130
|
-
gap: var(--ui-space-
|
|
148
|
+
align-items: stretch;
|
|
149
|
+
gap: var(--ui-space-12);
|
|
131
150
|
flex-shrink: 0;
|
|
132
151
|
}
|
|
133
152
|
|
|
134
153
|
.primary-info {
|
|
135
154
|
display: flex;
|
|
136
155
|
flex-direction: column;
|
|
156
|
+
align-items: flex-start;
|
|
137
157
|
gap: var(--ui-space-2);
|
|
138
158
|
}
|
|
139
159
|
|
|
160
|
+
/* Basis, not width: the panel takes the rest of the row and drops below only
|
|
161
|
+
when it can't hold that much. */
|
|
162
|
+
.editor-controls {
|
|
163
|
+
flex: 1 1 26rem;
|
|
164
|
+
min-width: 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
140
167
|
.header-swatch {
|
|
141
|
-
width:
|
|
142
|
-
height:
|
|
168
|
+
width: 4.5rem;
|
|
169
|
+
min-height: 4.5rem;
|
|
143
170
|
border-radius: var(--ui-radius-md);
|
|
144
171
|
border: 2px solid var(--ui-border);
|
|
145
172
|
flex-shrink: 0;
|
|
@@ -163,13 +190,9 @@
|
|
|
163
190
|
}
|
|
164
191
|
|
|
165
192
|
.base-hex {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
.clickable-hex {
|
|
172
|
-
align-self: flex-start;
|
|
193
|
+
display: inline-flex;
|
|
194
|
+
align-items: center;
|
|
195
|
+
gap: var(--ui-space-6);
|
|
173
196
|
background: none;
|
|
174
197
|
border: none;
|
|
175
198
|
cursor: pointer;
|
|
@@ -182,15 +205,31 @@
|
|
|
182
205
|
text-align: left;
|
|
183
206
|
}
|
|
184
207
|
|
|
185
|
-
|
|
208
|
+
/* The glyph only announces itself on approach; the hex is the content. */
|
|
209
|
+
.base-hex i {
|
|
210
|
+
font-size: var(--ui-font-size-xs);
|
|
211
|
+
color: var(--ui-text-muted);
|
|
212
|
+
transition: color var(--ui-transition-fast);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.base-hex:hover {
|
|
186
216
|
background: var(--ui-surface-highest);
|
|
187
217
|
color: var(--ui-text-primary);
|
|
188
218
|
}
|
|
189
219
|
|
|
220
|
+
.base-hex:hover i {
|
|
221
|
+
color: var(--ui-text-secondary);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
.base-hex.copied,
|
|
225
|
+
.base-hex.copied i {
|
|
226
|
+
color: var(--ui-text-accent);
|
|
227
|
+
}
|
|
228
|
+
|
|
190
229
|
@media (max-width: 1280px) {
|
|
191
230
|
.header-swatch {
|
|
192
|
-
width:
|
|
193
|
-
height:
|
|
231
|
+
width: 3.5rem;
|
|
232
|
+
min-height: 3.5rem;
|
|
194
233
|
}
|
|
195
234
|
}
|
|
196
235
|
</style>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import UIPillButton from '../UIPillButton.svelte';
|
|
3
|
+
import { openPaletteInTokens, openPaletteInWheel } from '../../core/store/paletteFocus';
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
family: string;
|
|
7
|
+
displayLabel?: string | null;
|
|
8
|
+
/** Which view the button hands this family over to. */
|
|
9
|
+
target: 'wheel' | 'tokens';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { family, displayLabel = null, target }: Props = $props();
|
|
13
|
+
|
|
14
|
+
let name = $derived(displayLabel ?? family);
|
|
15
|
+
let icon = $derived(target === 'wheel' ? 'fa-palette' : 'fa-sliders');
|
|
16
|
+
let label = $derived(target === 'wheel' ? 'Wheel' : 'Edit');
|
|
17
|
+
let title = $derived(
|
|
18
|
+
target === 'wheel'
|
|
19
|
+
? `Open ${name} on the color wheel`
|
|
20
|
+
: `Edit the ${name} palette in Tokens`
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
function jump() {
|
|
24
|
+
if (target === 'wheel') openPaletteInWheel(family);
|
|
25
|
+
else openPaletteInTokens(family);
|
|
26
|
+
}
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<span class="palette-jump">
|
|
30
|
+
<UIPillButton size="compact" variant="outline" {icon} {title} onclick={jump}>{label}</UIPillButton>
|
|
31
|
+
</span>
|
|
32
|
+
|
|
33
|
+
<style>
|
|
34
|
+
.palette-jump {
|
|
35
|
+
display: inline-flex;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* fa-palette loses its bowl at the pill's default icon size; the compact
|
|
39
|
+
pill's min-height carries the taller glyph without changing the pill's
|
|
40
|
+
height, so it still lines up with the plain pills beside it. */
|
|
41
|
+
.palette-jump :global(.ui-pill i) {
|
|
42
|
+
font-size: var(--ui-font-size-md);
|
|
43
|
+
}
|
|
44
|
+
</style>
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
lockedAnchorIndex?: number | null;
|
|
27
27
|
onAnchorsChange: (anchors: CurveAnchor[]) => void;
|
|
28
28
|
onOffsetChange: (key: string, value: number) => void;
|
|
29
|
+
onLockedAnchorUnlock?: (() => void) | null;
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
let {
|
|
@@ -37,7 +38,8 @@
|
|
|
37
38
|
offset = 0,
|
|
38
39
|
lockedAnchorIndex = null,
|
|
39
40
|
onAnchorsChange,
|
|
40
|
-
onOffsetChange
|
|
41
|
+
onOffsetChange,
|
|
42
|
+
onLockedAnchorUnlock = null
|
|
41
43
|
}: Props = $props();
|
|
42
44
|
</script>
|
|
43
45
|
|
|
@@ -48,6 +50,7 @@
|
|
|
48
50
|
defaultAnchors={defaults}
|
|
49
51
|
{offset}
|
|
50
52
|
{lockedAnchorIndex}
|
|
53
|
+
{onLockedAnchorUnlock}
|
|
51
54
|
{onAnchorsChange}
|
|
52
55
|
onOffsetChange={(v) => onOffsetChange(curveKey, v)}
|
|
53
56
|
/>
|