@motion-proto/live-tokens 0.54.2 → 0.55.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
CHANGED
|
@@ -1,5 +1,72 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.55.1 — The anchored step is the base color
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Editing the anchored palette step edits the base color.** The anchor pins
|
|
8
|
+
the lightness, saturation and hue curves through one step, so that swatch was
|
|
9
|
+
already rendering the base color. Clicking it opened a per-step override,
|
|
10
|
+
which forked the two apart: the ramp moved, the family's base swatch and hex
|
|
11
|
+
did not, and the palette stopped passing through the base color the anchor
|
|
12
|
+
guarantees it passes through. That step now opens the base color itself. The
|
|
13
|
+
whole ramp re-derives, a bound harmony axis follows the hue, and the header
|
|
14
|
+
swatch moves with it. Both swatches carry the selection ring together and the
|
|
15
|
+
panel titles itself "Base Color > 500", so the pairing is stated rather than
|
|
16
|
+
inferred. An override left on the anchored step by an earlier build is cleared
|
|
17
|
+
when the step is opened, inside the edit session, so Cancel and undo both put
|
|
18
|
+
it back. Every other step keeps its own override, unchanged.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- **The color editor docks under the row it edits.** It rendered at the bottom
|
|
23
|
+
of the family block, past the curve editors and the Text, Surfaces and Borders
|
|
24
|
+
section, far enough from the swatch that opened it to read as unrelated
|
|
25
|
+
chrome. It now opens directly beneath the ramp for a palette step, or beneath
|
|
26
|
+
the derived row that owns the step, with a caret on the step's own column. The
|
|
27
|
+
caret is placed on the grid rather than positioned, so it stays under its
|
|
28
|
+
swatch at any width. Showing and hiding use the disclosure motion the
|
|
29
|
+
expandable sections already use.
|
|
30
|
+
|
|
31
|
+
- **Selection reads by weight.** The editor's chrome is greyscale by design, so
|
|
32
|
+
a selected swatch is marked by a heavy ring drawn inside it: a white band
|
|
33
|
+
between two dark keylines, which survives both ends of a ramp running white to
|
|
34
|
+
black where a single-tone ring disappears at one end. The ring is inset rather
|
|
35
|
+
than an outline, which would close the gutter between neighbouring swatches,
|
|
36
|
+
or a thicker border, which would resize the swatch inside its own grid.
|
|
37
|
+
|
|
38
|
+
## 0.55.0 — A focused palette arrives ready
|
|
39
|
+
|
|
40
|
+
### Changed
|
|
41
|
+
|
|
42
|
+
- **A palette focused before mount opens without the reveal.** `paletteEditorOpen`
|
|
43
|
+
started false and flipped in an effect, so a deep link that set the focus
|
|
44
|
+
ahead of mount landed the visitor mid-animation, reading as a page still
|
|
45
|
+
loading. The editor now reads the pending focus when it constructs and opens
|
|
46
|
+
on the first frame; the scroll that brings it into view jumps rather than
|
|
47
|
+
gliding, since nothing expanded for the motion to match. A jump from the
|
|
48
|
+
Colors view, where the editor is already mounted, keeps its reveal.
|
|
49
|
+
|
|
50
|
+
- **"Base color must appear in palette" moved to the family band.** The toggle
|
|
51
|
+
sets `anchorToBase`, which is family-level config persisted in the theme,
|
|
52
|
+
but it sat in the base-colour panel among the controls that edit the colour's
|
|
53
|
+
*value* — and it only occupied a row of its own because it wrapped out of that
|
|
54
|
+
panel's title row. It now sits beside the family's name as
|
|
55
|
+
"Must appear in palette", the swatch alongside supplying the subject the label
|
|
56
|
+
drops; the full sentence stays as its title. The colour panel closes back up
|
|
57
|
+
to a title row and its sliders. Collapsed families are untouched: the setting
|
|
58
|
+
shows only while that family's editor is open, so a page of ten collapsed
|
|
59
|
+
palettes does not repeat it ten times.
|
|
60
|
+
|
|
61
|
+
- **The palette band holds two columns at every width.** With the setting on
|
|
62
|
+
the band's row, a band too narrow for it wrapped the buttons to the left edge,
|
|
63
|
+
under the swatch. The setting sits in the identity column instead, a third
|
|
64
|
+
line under the hex — name, value and constraint all describe the family and
|
|
65
|
+
share its left rail — which leaves the buttons a column of their own that no
|
|
66
|
+
width takes away. They wrap among themselves against the right edge before the
|
|
67
|
+
cluster ever drops a line. The swatch keeps its square: the taller column
|
|
68
|
+
would otherwise stretch the chip into a rectangle.
|
|
69
|
+
|
|
3
70
|
## 0.54.2 — Palette focus is public API
|
|
4
71
|
|
|
5
72
|
### Added
|
package/package.json
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
interface Props {
|
|
10
10
|
title?: string | null;
|
|
11
|
+
/** Ties the panel to the swatch it was opened from, when it renders docked
|
|
12
|
+
* beneath that swatch's row. */
|
|
13
|
+
highlighted?: boolean;
|
|
11
14
|
showRemoveOverride?: boolean;
|
|
12
15
|
/** Hide the confirm/cancel actions for live-apply consumers (Colors view
|
|
13
16
|
* readout) where edits commit through the store as they happen. */
|
|
@@ -37,6 +40,7 @@
|
|
|
37
40
|
|
|
38
41
|
let {
|
|
39
42
|
title = null,
|
|
43
|
+
highlighted = false,
|
|
40
44
|
showRemoveOverride = false,
|
|
41
45
|
hideActions = false,
|
|
42
46
|
hidePreview = false,
|
|
@@ -133,7 +137,7 @@
|
|
|
133
137
|
}
|
|
134
138
|
</script>
|
|
135
139
|
|
|
136
|
-
<div class="hsl-panel">
|
|
140
|
+
<div class="hsl-panel" class:highlighted>
|
|
137
141
|
<div class="hsl-panel-header">
|
|
138
142
|
{#if !hidePreview}
|
|
139
143
|
<div class="hsl-preview" style="background: {previewHex}"></div>
|
|
@@ -250,6 +254,17 @@
|
|
|
250
254
|
border-radius: var(--ui-radius-md);
|
|
251
255
|
}
|
|
252
256
|
|
|
257
|
+
/* Docked to a swatch: the border thickens to the weight of that swatch's
|
|
258
|
+
selection ring, so the pair reads as one object. */
|
|
259
|
+
.hsl-panel.highlighted {
|
|
260
|
+
border-width: 2px;
|
|
261
|
+
border-color: var(--ui-border-higher);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.hsl-panel.highlighted .hsl-panel-title {
|
|
265
|
+
color: var(--ui-text-primary);
|
|
266
|
+
}
|
|
267
|
+
|
|
253
268
|
.hsl-panel-header {
|
|
254
269
|
display: flex;
|
|
255
270
|
align-items: center;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { onMount, onDestroy, tick } from 'svelte';
|
|
3
|
+
import { get } from 'svelte/store';
|
|
3
4
|
import { oklchToHexClamped, type Oklch } from '../core/palettes/oklch';
|
|
4
5
|
import { type CurveAnchor, lightnessCurveConfig, saturationCurveConfig, hueCurveConfig } from './curveEngine';
|
|
5
6
|
import ColorEditPanel from './ColorEditPanel.svelte';
|
|
@@ -114,7 +115,14 @@
|
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
let showDerived = $state(false);
|
|
117
|
-
|
|
118
|
+
// A deep link sets the focus before this mounts, so the controls are open on
|
|
119
|
+
// the first frame: UIReveal's `{#if}` never toggles and there is no reveal to
|
|
120
|
+
// sit through. Landing mid-animation reads as a page still loading.
|
|
121
|
+
// svelte-ignore state_referenced_locally
|
|
122
|
+
// The mount-time value is the whole point: a family's label is fixed for the
|
|
123
|
+
// life of the instance (PALETTE_SPECS keys the `{#each}` by it).
|
|
124
|
+
const focusedOnMount = get(pendingPaletteFocus) === label;
|
|
125
|
+
let paletteEditorOpen = $state(focusedOnMount);
|
|
118
126
|
let rootEl: HTMLElement | undefined = $state();
|
|
119
127
|
|
|
120
128
|
// Arriving from the Colors view's Edit button: open this family's controls and
|
|
@@ -123,7 +131,11 @@
|
|
|
123
131
|
if ($pendingPaletteFocus !== label) return;
|
|
124
132
|
pendingPaletteFocus.set(null);
|
|
125
133
|
paletteEditorOpen = true;
|
|
126
|
-
|
|
134
|
+
// Nothing expanded when we opened on mount, so there is no motion for a
|
|
135
|
+
// smooth scroll to match — jump, and the first frame is the final state.
|
|
136
|
+
tick().then(() =>
|
|
137
|
+
rootEl?.scrollIntoView({ behavior: focusedOnMount ? 'auto' : 'smooth', block: 'start' }),
|
|
138
|
+
);
|
|
127
139
|
});
|
|
128
140
|
|
|
129
141
|
function setLightnessCurve(a: CurveAnchor[]) { edit('lightnessCurve', a); }
|
|
@@ -195,8 +207,19 @@
|
|
|
195
207
|
openSession();
|
|
196
208
|
}
|
|
197
209
|
|
|
198
|
-
function handlePaletteClick(ps: { label: string;
|
|
210
|
+
function handlePaletteClick(ps: { label: string; index: number; anchored: boolean }) {
|
|
199
211
|
const k = paletteStepKey(ps.label);
|
|
212
|
+
// The anchored step is not a copy of the base color, it IS the base color:
|
|
213
|
+
// the anchor pins the curves through it. So editing it edits the base, and
|
|
214
|
+
// the header swatch and the whole ramp follow. An override here would fork
|
|
215
|
+
// the two apart and quietly break the guarantee the anchor makes.
|
|
216
|
+
if (ps.anchored) {
|
|
217
|
+
if (isEditingBase) { confirmEdit(); return; }
|
|
218
|
+
startBaseEdit();
|
|
219
|
+
// After openSession, so cancel and undo both put it back.
|
|
220
|
+
if (k in overrides) removeOverride(k);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
200
223
|
if (editingKey === k) {
|
|
201
224
|
confirmEdit();
|
|
202
225
|
return;
|
|
@@ -493,6 +516,10 @@
|
|
|
493
516
|
let overridesHex = $derived(Object.fromEntries(Object.entries(overrides).map(([k, v]) => [k, toHex(v)])));
|
|
494
517
|
|
|
495
518
|
let isEditingBase = $derived(isBaseEdit(editing));
|
|
519
|
+
// Naming the step the base sits on is what says the header swatch and that
|
|
520
|
+
// ramp swatch are one value, not two that happen to match.
|
|
521
|
+
let anchoredStep = $derived(paletteComputed.find(ps => ps.anchored) ?? null);
|
|
522
|
+
let basePanelTitle = $derived(anchoredStep ? `Base Color \u203A ${anchoredStep.label}` : 'Base Color');
|
|
496
523
|
let editingColor = $derived(isEditingBase ? baseColor : editingDraft);
|
|
497
524
|
let editingStepInfo = $derived((() => {
|
|
498
525
|
if (!editingKey || isEditingBase) return null;
|
|
@@ -508,11 +535,28 @@
|
|
|
508
535
|
return null;
|
|
509
536
|
})());
|
|
510
537
|
let panelOpen = $derived(editingKey !== null && (isEditingBase || (editingDraft !== null && editingStepInfo !== null)));
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
538
|
+
// The panel docks under the row that owns the step being edited, so these
|
|
539
|
+
// place it: a column for the palette caret, a row slot for the derived scales.
|
|
540
|
+
let editingPaletteIndex = $derived.by(() => {
|
|
541
|
+
if (!panelOpen || isEditingBase) return null;
|
|
542
|
+
const i = paletteComputed.findIndex(ps => ps.key === editingKey);
|
|
543
|
+
return i === -1 ? null : i;
|
|
544
|
+
});
|
|
545
|
+
let editingDerivedRow = $derived.by(() => {
|
|
546
|
+
if (!panelOpen || isEditingBase || editingPaletteIndex !== null) return null;
|
|
547
|
+
const scale = scales.find(s => s.title === editingStepInfo?.scale);
|
|
548
|
+
if (!scale) return null;
|
|
549
|
+
return scale.isText ? 'text' : 'solid';
|
|
550
|
+
});
|
|
551
|
+
let editPanelTitle = $derived(editingStepInfo ? `${editingStepInfo.scale} \u203A ${editingStepInfo.step}` : null);
|
|
552
|
+
// The reveal's outro needs something to render: closing the panel resets
|
|
553
|
+
// `editing` to idle, and every value below it goes null in the same tick, so a
|
|
554
|
+
// live read would slide an empty box shut. Held until the next edit replaces it.
|
|
555
|
+
let dock: { title: string | null; color: Oklch; paletteIndex: number | null } | null = $state(null);
|
|
556
|
+
$effect(() => {
|
|
557
|
+
if (!panelOpen || isEditingBase || !editingColor) return;
|
|
558
|
+
dock = { title: editPanelTitle, color: editingColor, paletteIndex: editingPaletteIndex };
|
|
559
|
+
});
|
|
516
560
|
$effect(() => {
|
|
517
561
|
// Touch each input so the effect tracks them (resnapScales reads indirectly).
|
|
518
562
|
void baseColor;
|
|
@@ -525,6 +569,24 @@
|
|
|
525
569
|
});
|
|
526
570
|
</script>
|
|
527
571
|
|
|
572
|
+
{#snippet stepEditPanel()}
|
|
573
|
+
{#if dock}
|
|
574
|
+
<ColorEditPanel
|
|
575
|
+
title={dock.title}
|
|
576
|
+
highlighted
|
|
577
|
+
showRemoveOverride
|
|
578
|
+
hue={dock.color.h}
|
|
579
|
+
chroma={dock.color.c}
|
|
580
|
+
lightness={dock.color.l * 100}
|
|
581
|
+
onHueChromaChange={(h, c, l) => handleColorChange({ l: l / 100, c, h })}
|
|
582
|
+
onConfirm={confirmEdit}
|
|
583
|
+
onCancel={cancelEdit}
|
|
584
|
+
onRemoveOverride={() => editingKey && removeOverride(editingKey)}
|
|
585
|
+
onSliderStart={() => beginSliderGesture(`edit ${label} ${editingKey ?? 'color'}`)}
|
|
586
|
+
/>
|
|
587
|
+
{/if}
|
|
588
|
+
{/snippet}
|
|
589
|
+
|
|
528
590
|
<div class="palette-editor" bind:this={rootEl} style="--editor-base: {toHex(baseColor)}">
|
|
529
591
|
<PaletteBase
|
|
530
592
|
{label}
|
|
@@ -532,10 +594,23 @@
|
|
|
532
594
|
{baseColor}
|
|
533
595
|
{isEditingBase}
|
|
534
596
|
pinnedOpen={paletteEditorOpen}
|
|
597
|
+
selected={isEditingBase}
|
|
535
598
|
{copiedKey}
|
|
536
599
|
onStartEdit={startBaseEdit}
|
|
537
600
|
onCopyBaseHex={copyHex}
|
|
538
601
|
>
|
|
602
|
+
{#snippet setting()}
|
|
603
|
+
<!-- The family's own swatch and name sit alongside, so the label drops
|
|
604
|
+
its subject; the title carries the full sentence. -->
|
|
605
|
+
<span class="base-anchor-toggle" title="Base color must appear in palette">
|
|
606
|
+
<Toggle
|
|
607
|
+
checked={anchorToBase}
|
|
608
|
+
onchange={(v) => setAnchorToBase(v ?? !anchorToBase)}
|
|
609
|
+
label="Must appear in palette"
|
|
610
|
+
/>
|
|
611
|
+
</span>
|
|
612
|
+
{/snippet}
|
|
613
|
+
|
|
539
614
|
{#snippet actions()}
|
|
540
615
|
<PaletteJumpButton family={label} {displayLabel} target="wheel" />
|
|
541
616
|
<UIPillButton size="compact" variant="outline" onclick={clearPaletteOverrides}>Clear Overrides</UIPillButton>
|
|
@@ -549,7 +624,7 @@
|
|
|
549
624
|
<div class="swatch-grid" style="--swatch-cols: {paletteStepLightness.length + 2}">
|
|
550
625
|
<div class="base-panel" style="grid-column: 2 / {paletteStepLightness.length + 2}">
|
|
551
626
|
<ColorEditPanel
|
|
552
|
-
title={
|
|
627
|
+
title={basePanelTitle}
|
|
553
628
|
showRemoveOverride={false}
|
|
554
629
|
hideActions={!isEditingBase}
|
|
555
630
|
hidePreview
|
|
@@ -563,18 +638,7 @@
|
|
|
563
638
|
onCancel={cancelEdit}
|
|
564
639
|
onRemoveOverride={() => {}}
|
|
565
640
|
onSliderStart={() => beginSliderGesture(`edit ${label} base`)}
|
|
566
|
-
|
|
567
|
-
{#snippet actions()}
|
|
568
|
-
<div class="base-anchor-toggle">
|
|
569
|
-
<Toggle
|
|
570
|
-
checked={anchorToBase}
|
|
571
|
-
onchange={(v) => setAnchorToBase(v ?? !anchorToBase)}
|
|
572
|
-
label="Base color must appear in palette"
|
|
573
|
-
labelFirst
|
|
574
|
-
/>
|
|
575
|
-
</div>
|
|
576
|
-
{/snippet}
|
|
577
|
-
</ColorEditPanel>
|
|
641
|
+
/>
|
|
578
642
|
</div>
|
|
579
643
|
</div>
|
|
580
644
|
</UIReveal>
|
|
@@ -604,21 +668,21 @@
|
|
|
604
668
|
</div>
|
|
605
669
|
{#each paletteComputed as ps}
|
|
606
670
|
<div class="step-column">
|
|
607
|
-
<button class="step-label copyable-label" class:copied={copiedLabelKey === ps.key} type="button" onclick={(e) => copyVarName(ps.key, `--color-${cssNamespace}-${ps.label}`, e)}>
|
|
671
|
+
<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)}>
|
|
608
672
|
{copiedLabelKey === ps.key ? 'copied!' : ps.label}
|
|
609
673
|
</button>
|
|
610
674
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
611
675
|
<div
|
|
612
676
|
class="swatch gray-swatch"
|
|
613
|
-
class:active={editingKey === ps.key}
|
|
677
|
+
class:active={editingKey === ps.key || (isEditingBase && ps.anchored)}
|
|
614
678
|
class:overridden={ps.key in overrides}
|
|
615
679
|
class:anchored={ps.anchored}
|
|
616
680
|
title={ps.anchored ? 'Base color' : undefined}
|
|
617
681
|
style="background: {ps.effective}"
|
|
618
|
-
onclick={() => handlePaletteClick(
|
|
682
|
+
onclick={() => handlePaletteClick(ps)}
|
|
619
683
|
role="button"
|
|
620
684
|
tabindex="0"
|
|
621
|
-
onkeydown={(e) => e.key === 'Enter' && handlePaletteClick(
|
|
685
|
+
onkeydown={(e) => e.key === 'Enter' && handlePaletteClick(ps)}
|
|
622
686
|
>
|
|
623
687
|
{#if ps.key in overrides}
|
|
624
688
|
<span class="override-lock" title="Palette override: this step is set by hand, not derived from the curve">
|
|
@@ -640,6 +704,17 @@
|
|
|
640
704
|
</button>
|
|
641
705
|
<div class="swatch gray-swatch bookend" style="background: #000000"></div>
|
|
642
706
|
</div>
|
|
707
|
+
|
|
708
|
+
<UIReveal open={editingPaletteIndex !== null} style="grid-column: 2 / {paletteStepLightness.length + 2}">
|
|
709
|
+
<!-- The reveal has to own the grid slot, so the dock rebuilds the
|
|
710
|
+
ramp's tracks inside it: the caret is then placed on the step's
|
|
711
|
+
own column and points at it at any width, no arithmetic. -->
|
|
712
|
+
<div class="dock" style="--dock-cols: {paletteStepLightness.length}">
|
|
713
|
+
<div class="edit-caret" style="grid-column: {(dock?.paletteIndex ?? 0) + 1}"></div>
|
|
714
|
+
<div class="docked-panel">{@render stepEditPanel()}</div>
|
|
715
|
+
</div>
|
|
716
|
+
</UIReveal>
|
|
717
|
+
|
|
643
718
|
<UIReveal open={paletteEditorOpen} style="grid-column: 2 / {paletteStepLightness.length + 2}">
|
|
644
719
|
<div class="curve-grid-span">
|
|
645
720
|
{#if anchorUnlockPrompt && anchorToBase}
|
|
@@ -774,29 +849,19 @@
|
|
|
774
849
|
{@render overridesPanel(scale, true, derivedHexForBase)}
|
|
775
850
|
{/each}
|
|
776
851
|
</div>
|
|
852
|
+
<UIReveal open={editingDerivedRow === 'text'}>
|
|
853
|
+
<div class="docked-panel">{@render stepEditPanel()}</div>
|
|
854
|
+
</UIReveal>
|
|
777
855
|
<div class="scales-row">
|
|
778
856
|
{#each scales.filter(s => !s.isText) as scale}
|
|
779
857
|
{@render overridesPanel(scale, true, derivedHexForBase)}
|
|
780
858
|
{/each}
|
|
781
859
|
</div>
|
|
860
|
+
<UIReveal open={editingDerivedRow === 'solid'}>
|
|
861
|
+
<div class="docked-panel">{@render stepEditPanel()}</div>
|
|
862
|
+
</UIReveal>
|
|
782
863
|
</div>
|
|
783
864
|
</UIReveal>
|
|
784
|
-
|
|
785
|
-
<!-- Color Edit Panel (non-base edits) -->
|
|
786
|
-
{#if !isEditingBase && panelOpen && editingColor}
|
|
787
|
-
<ColorEditPanel
|
|
788
|
-
title={editPanelTitle}
|
|
789
|
-
showRemoveOverride={!!editingKey}
|
|
790
|
-
hue={editingColor.h}
|
|
791
|
-
chroma={editingColor.c}
|
|
792
|
-
lightness={editingColor.l * 100}
|
|
793
|
-
onHueChromaChange={(h, c, l) => handleColorChange({ l: l / 100, c, h })}
|
|
794
|
-
onConfirm={confirmEdit}
|
|
795
|
-
onCancel={cancelEdit}
|
|
796
|
-
onRemoveOverride={() => editingKey && removeOverride(editingKey)}
|
|
797
|
-
onSliderStart={() => beginSliderGesture(`edit ${label} ${editingKey ?? 'color'}`)}
|
|
798
|
-
/>
|
|
799
|
-
{/if}
|
|
800
865
|
</div>
|
|
801
866
|
|
|
802
867
|
<style>
|
|
@@ -924,6 +989,12 @@
|
|
|
924
989
|
color: var(--ui-text-accent, var(--ui-text-primary));
|
|
925
990
|
}
|
|
926
991
|
|
|
992
|
+
/* The name the docked panel's title repeats back. */
|
|
993
|
+
.step-label.copyable-label.selected {
|
|
994
|
+
color: var(--ui-text-primary);
|
|
995
|
+
font-weight: var(--ui-font-weight-semibold);
|
|
996
|
+
}
|
|
997
|
+
|
|
927
998
|
/* Swatches */
|
|
928
999
|
|
|
929
1000
|
.swatch {
|
|
@@ -988,8 +1059,10 @@
|
|
|
988
1059
|
min-width: 0;
|
|
989
1060
|
}
|
|
990
1061
|
|
|
1062
|
+
/* A box of its own so the title has something to hover; placement in the
|
|
1063
|
+
band belongs to PaletteBase. */
|
|
991
1064
|
.base-anchor-toggle {
|
|
992
|
-
|
|
1065
|
+
display: flex;
|
|
993
1066
|
}
|
|
994
1067
|
|
|
995
1068
|
.curve-grid-span {
|
|
@@ -998,6 +1071,34 @@
|
|
|
998
1071
|
gap: var(--ui-space-8);
|
|
999
1072
|
}
|
|
1000
1073
|
|
|
1074
|
+
/* Same tracks and gutter as the ramp above, so a column here lands under the
|
|
1075
|
+
swatch of the same index. */
|
|
1076
|
+
.dock {
|
|
1077
|
+
display: grid;
|
|
1078
|
+
grid-template-columns: repeat(var(--dock-cols), minmax(0, 1fr));
|
|
1079
|
+
gap: var(--ui-space-4) var(--swatch-gap, var(--ui-space-4));
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/* Docked under the row it edits, pointed at the swatch it edits. */
|
|
1083
|
+
.docked-panel {
|
|
1084
|
+
grid-column: 1 / -1;
|
|
1085
|
+
min-width: 0;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
.edit-caret {
|
|
1089
|
+
display: flex;
|
|
1090
|
+
justify-content: center;
|
|
1091
|
+
align-items: flex-end;
|
|
1092
|
+
height: var(--ui-space-6);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
.edit-caret::after {
|
|
1096
|
+
content: '';
|
|
1097
|
+
border-left: var(--ui-space-6) solid transparent;
|
|
1098
|
+
border-right: var(--ui-space-6) solid transparent;
|
|
1099
|
+
border-bottom: var(--ui-space-6) solid var(--ui-border-higher);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1001
1102
|
.anchor-unlock-notice {
|
|
1002
1103
|
display: flex;
|
|
1003
1104
|
align-items: center;
|
|
@@ -1042,10 +1143,17 @@
|
|
|
1042
1143
|
border-color: var(--ui-border-high);
|
|
1043
1144
|
}
|
|
1044
1145
|
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1146
|
+
/* A ring drawn inside the swatch, weight only: the editor stays greyscale, so
|
|
1147
|
+
selection has to out-weigh the anchored step's emphasized border rather
|
|
1148
|
+
than out-colour it. Insets, because a 3px outline would eat the column gap
|
|
1149
|
+
and a 3px border would resize the swatch mid-grid. The dark keylines either
|
|
1150
|
+
side of the white band keep it legible at both ends of the ramp. */
|
|
1151
|
+
.swatch.gray-swatch.active,
|
|
1152
|
+
.swatch.gray-swatch.anchored.active {
|
|
1153
|
+
border-color: var(--ui-surface-lowest);
|
|
1154
|
+
box-shadow:
|
|
1155
|
+
inset 0 0 0 3px var(--ui-text-primary),
|
|
1156
|
+
inset 0 0 0 4px var(--ui-surface-lowest);
|
|
1049
1157
|
}
|
|
1050
1158
|
|
|
1051
1159
|
/* The step the base color is placed at: taller, with an emphasized border. It
|
|
@@ -424,9 +424,12 @@
|
|
|
424
424
|
border-color: var(--ui-border-high);
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
+
/* Same weight-only selection language as the palette ramp, at the smaller
|
|
428
|
+
swatch's scale. */
|
|
427
429
|
.override-slot.active {
|
|
428
|
-
border-
|
|
429
|
-
|
|
430
|
+
border-style: solid;
|
|
431
|
+
border-color: var(--ui-surface-lowest);
|
|
432
|
+
outline: 2px solid var(--ui-text-primary);
|
|
430
433
|
outline-offset: 1px;
|
|
431
434
|
}
|
|
432
435
|
|
|
@@ -14,10 +14,16 @@
|
|
|
14
14
|
isEditingBase: boolean;
|
|
15
15
|
/** Keep the swatch reading as active while the curve editors are visible. */
|
|
16
16
|
pinnedOpen?: boolean;
|
|
17
|
+
/** The same ring the ramp's anchored step wears, so the two swatches read
|
|
18
|
+
* as the one value they are. */
|
|
19
|
+
selected?: boolean;
|
|
17
20
|
copiedKey: string | null;
|
|
18
21
|
onStartEdit: () => void;
|
|
19
22
|
onCopyBaseHex: (key: string, hex: string, event?: MouseEvent) => void;
|
|
20
23
|
actions?: Snippet;
|
|
24
|
+
/** A family-level setting, shown beside the name while this family's
|
|
25
|
+
* editor is open. Collapsed families keep the band to identity + actions. */
|
|
26
|
+
setting?: Snippet;
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
let {
|
|
@@ -26,13 +32,19 @@
|
|
|
26
32
|
baseColor,
|
|
27
33
|
isEditingBase,
|
|
28
34
|
pinnedOpen = false,
|
|
35
|
+
selected = false,
|
|
29
36
|
copiedKey,
|
|
30
37
|
onStartEdit,
|
|
31
38
|
onCopyBaseHex,
|
|
32
|
-
actions
|
|
39
|
+
actions,
|
|
40
|
+
setting
|
|
33
41
|
}: Props = $props();
|
|
34
42
|
|
|
35
43
|
let baseHex = $derived(oklchToHexClamped(baseColor.l, baseColor.c, baseColor.h));
|
|
44
|
+
|
|
45
|
+
// `pinnedOpen || isEditingBase` is exactly when the base panel shows: editing
|
|
46
|
+
// the base implies both a panel and a colour to put in it.
|
|
47
|
+
let showSetting = $derived(!!setting && (pinnedOpen || isEditingBase));
|
|
36
48
|
</script>
|
|
37
49
|
|
|
38
50
|
<div class="editor-top">
|
|
@@ -41,6 +53,7 @@
|
|
|
41
53
|
<div
|
|
42
54
|
class="header-swatch"
|
|
43
55
|
class:active={isEditingBase || pinnedOpen}
|
|
56
|
+
class:selected
|
|
44
57
|
style="background: {baseHex}"
|
|
45
58
|
onclick={onStartEdit}
|
|
46
59
|
role="button"
|
|
@@ -64,6 +77,10 @@
|
|
|
64
77
|
aria-hidden="true"
|
|
65
78
|
></i>
|
|
66
79
|
</button>
|
|
80
|
+
|
|
81
|
+
{#if showSetting}
|
|
82
|
+
<div class="header-setting">{@render setting?.()}</div>
|
|
83
|
+
{/if}
|
|
67
84
|
</div>
|
|
68
85
|
</div>
|
|
69
86
|
|
|
@@ -100,12 +117,28 @@
|
|
|
100
117
|
.header-actions {
|
|
101
118
|
display: flex;
|
|
102
119
|
align-items: center;
|
|
120
|
+
flex-wrap: wrap;
|
|
121
|
+
justify-content: flex-end;
|
|
103
122
|
gap: var(--ui-space-8);
|
|
104
123
|
}
|
|
105
124
|
|
|
125
|
+
/* Third line of the identity, under the hex: name, value, constraint all
|
|
126
|
+
describe the family and share its left rail. Keeping it out of the band's
|
|
127
|
+
row is what leaves the buttons a column of their own at every width. */
|
|
128
|
+
.header-setting {
|
|
129
|
+
display: flex;
|
|
130
|
+
align-items: center;
|
|
131
|
+
margin-top: var(--ui-space-4);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* The setting makes the identity column taller than the swatch, and the row
|
|
135
|
+
stretches its items; left alone the chip would grow into a rectangle. It is
|
|
136
|
+
a colour sample, so it stays square and hangs from the top of the column. */
|
|
106
137
|
.header-swatch {
|
|
107
138
|
width: 4.5rem;
|
|
108
139
|
min-height: 4.5rem;
|
|
140
|
+
aspect-ratio: 1;
|
|
141
|
+
align-self: flex-start;
|
|
109
142
|
border-radius: var(--ui-radius-md);
|
|
110
143
|
border: 2px solid var(--ui-border);
|
|
111
144
|
flex-shrink: 0;
|
|
@@ -122,6 +155,14 @@
|
|
|
122
155
|
outline-offset: 1px;
|
|
123
156
|
}
|
|
124
157
|
|
|
158
|
+
.header-swatch.selected {
|
|
159
|
+
border-color: var(--ui-surface-lowest);
|
|
160
|
+
outline: none;
|
|
161
|
+
box-shadow:
|
|
162
|
+
inset 0 0 0 3px var(--ui-text-primary),
|
|
163
|
+
inset 0 0 0 4px var(--ui-surface-lowest);
|
|
164
|
+
}
|
|
165
|
+
|
|
125
166
|
.editor-label {
|
|
126
167
|
font-size: var(--ui-font-size-xl);
|
|
127
168
|
font-weight: var(--ui-font-weight-semibold);
|