@motion-proto/live-tokens 0.55.0 → 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,40 @@
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
+
3
38
  ## 0.55.0 — A focused palette arrives ready
4
39
 
5
40
  ### Changed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion-proto/live-tokens",
3
- "version": "0.55.0",
3
+ "version": "0.55.1",
4
4
  "type": "module",
5
5
  "description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 8.",
6
6
  "keywords": [
@@ -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;
@@ -207,8 +207,19 @@
207
207
  openSession();
208
208
  }
209
209
 
210
- function handlePaletteClick(ps: { label: string; lightness: number; index: number }) {
210
+ function handlePaletteClick(ps: { label: string; index: number; anchored: boolean }) {
211
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
+ }
212
223
  if (editingKey === k) {
213
224
  confirmEdit();
214
225
  return;
@@ -505,6 +516,10 @@
505
516
  let overridesHex = $derived(Object.fromEntries(Object.entries(overrides).map(([k, v]) => [k, toHex(v)])));
506
517
 
507
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');
508
523
  let editingColor = $derived(isEditingBase ? baseColor : editingDraft);
509
524
  let editingStepInfo = $derived((() => {
510
525
  if (!editingKey || isEditingBase) return null;
@@ -520,11 +535,28 @@
520
535
  return null;
521
536
  })());
522
537
  let panelOpen = $derived(editingKey !== null && (isEditingBase || (editingDraft !== null && editingStepInfo !== null)));
523
- let editPanelTitle = $derived(isEditingBase
524
- ? 'Base Color'
525
- : editingStepInfo
526
- ? `${editingStepInfo.scale} \u203A ${editingStepInfo.step}`
527
- : null);
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
+ });
528
560
  $effect(() => {
529
561
  // Touch each input so the effect tracks them (resnapScales reads indirectly).
530
562
  void baseColor;
@@ -537,6 +569,24 @@
537
569
  });
538
570
  </script>
539
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
+
540
590
  <div class="palette-editor" bind:this={rootEl} style="--editor-base: {toHex(baseColor)}">
541
591
  <PaletteBase
542
592
  {label}
@@ -544,6 +594,7 @@
544
594
  {baseColor}
545
595
  {isEditingBase}
546
596
  pinnedOpen={paletteEditorOpen}
597
+ selected={isEditingBase}
547
598
  {copiedKey}
548
599
  onStartEdit={startBaseEdit}
549
600
  onCopyBaseHex={copyHex}
@@ -573,7 +624,7 @@
573
624
  <div class="swatch-grid" style="--swatch-cols: {paletteStepLightness.length + 2}">
574
625
  <div class="base-panel" style="grid-column: 2 / {paletteStepLightness.length + 2}">
575
626
  <ColorEditPanel
576
- title={isEditingBase ? editPanelTitle : 'Base Color'}
627
+ title={basePanelTitle}
577
628
  showRemoveOverride={false}
578
629
  hideActions={!isEditingBase}
579
630
  hidePreview
@@ -617,21 +668,21 @@
617
668
  </div>
618
669
  {#each paletteComputed as ps}
619
670
  <div class="step-column">
620
- <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)}>
621
672
  {copiedLabelKey === ps.key ? 'copied!' : ps.label}
622
673
  </button>
623
674
  <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
624
675
  <div
625
676
  class="swatch gray-swatch"
626
- class:active={editingKey === ps.key}
677
+ class:active={editingKey === ps.key || (isEditingBase && ps.anchored)}
627
678
  class:overridden={ps.key in overrides}
628
679
  class:anchored={ps.anchored}
629
680
  title={ps.anchored ? 'Base color' : undefined}
630
681
  style="background: {ps.effective}"
631
- onclick={() => handlePaletteClick({ label: ps.label, lightness: ps.lightness, index: ps.index })}
682
+ onclick={() => handlePaletteClick(ps)}
632
683
  role="button"
633
684
  tabindex="0"
634
- onkeydown={(e) => e.key === 'Enter' && handlePaletteClick({ label: ps.label, lightness: ps.lightness, index: ps.index })}
685
+ onkeydown={(e) => e.key === 'Enter' && handlePaletteClick(ps)}
635
686
  >
636
687
  {#if ps.key in overrides}
637
688
  <span class="override-lock" title="Palette override: this step is set by hand, not derived from the curve">
@@ -653,6 +704,17 @@
653
704
  </button>
654
705
  <div class="swatch gray-swatch bookend" style="background: #000000"></div>
655
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
+
656
718
  <UIReveal open={paletteEditorOpen} style="grid-column: 2 / {paletteStepLightness.length + 2}">
657
719
  <div class="curve-grid-span">
658
720
  {#if anchorUnlockPrompt && anchorToBase}
@@ -787,29 +849,19 @@
787
849
  {@render overridesPanel(scale, true, derivedHexForBase)}
788
850
  {/each}
789
851
  </div>
852
+ <UIReveal open={editingDerivedRow === 'text'}>
853
+ <div class="docked-panel">{@render stepEditPanel()}</div>
854
+ </UIReveal>
790
855
  <div class="scales-row">
791
856
  {#each scales.filter(s => !s.isText) as scale}
792
857
  {@render overridesPanel(scale, true, derivedHexForBase)}
793
858
  {/each}
794
859
  </div>
860
+ <UIReveal open={editingDerivedRow === 'solid'}>
861
+ <div class="docked-panel">{@render stepEditPanel()}</div>
862
+ </UIReveal>
795
863
  </div>
796
864
  </UIReveal>
797
-
798
- <!-- Color Edit Panel (non-base edits) -->
799
- {#if !isEditingBase && panelOpen && editingColor}
800
- <ColorEditPanel
801
- title={editPanelTitle}
802
- showRemoveOverride={!!editingKey}
803
- hue={editingColor.h}
804
- chroma={editingColor.c}
805
- lightness={editingColor.l * 100}
806
- onHueChromaChange={(h, c, l) => handleColorChange({ l: l / 100, c, h })}
807
- onConfirm={confirmEdit}
808
- onCancel={cancelEdit}
809
- onRemoveOverride={() => editingKey && removeOverride(editingKey)}
810
- onSliderStart={() => beginSliderGesture(`edit ${label} ${editingKey ?? 'color'}`)}
811
- />
812
- {/if}
813
865
  </div>
814
866
 
815
867
  <style>
@@ -937,6 +989,12 @@
937
989
  color: var(--ui-text-accent, var(--ui-text-primary));
938
990
  }
939
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
+
940
998
  /* Swatches */
941
999
 
942
1000
  .swatch {
@@ -1013,6 +1071,34 @@
1013
1071
  gap: var(--ui-space-8);
1014
1072
  }
1015
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
+
1016
1102
  .anchor-unlock-notice {
1017
1103
  display: flex;
1018
1104
  align-items: center;
@@ -1057,10 +1143,17 @@
1057
1143
  border-color: var(--ui-border-high);
1058
1144
  }
1059
1145
 
1060
- .swatch.gray-swatch.active {
1061
- border-color: var(--ui-border-higher);
1062
- outline: 2px solid var(--ui-border-high);
1063
- outline-offset: 1px;
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);
1064
1157
  }
1065
1158
 
1066
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-color: var(--ui-border-higher);
429
- outline: 1px solid var(--ui-border-high);
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,6 +14,9 @@
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;
@@ -29,6 +32,7 @@
29
32
  baseColor,
30
33
  isEditingBase,
31
34
  pinnedOpen = false,
35
+ selected = false,
32
36
  copiedKey,
33
37
  onStartEdit,
34
38
  onCopyBaseHex,
@@ -49,6 +53,7 @@
49
53
  <div
50
54
  class="header-swatch"
51
55
  class:active={isEditingBase || pinnedOpen}
56
+ class:selected
52
57
  style="background: {baseHex}"
53
58
  onclick={onStartEdit}
54
59
  role="button"
@@ -150,6 +155,14 @@
150
155
  outline-offset: 1px;
151
156
  }
152
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
+
153
166
  .editor-label {
154
167
  font-size: var(--ui-font-size-xl);
155
168
  font-weight: var(--ui-font-weight-semibold);