@motion-proto/live-tokens 0.64.0 → 0.64.3

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,47 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.64.3 — The add button names the act
4
+
5
+ ### Changed
6
+
7
+ - **The add button names the act, not the font.** It read `+ add Domine` — the
8
+ next family it would reach for — so the label changed under the pointer as a
9
+ stack filled up, and a control that renames itself between clicks is hard to
10
+ aim at. It now reads `+ add font`, and still falls back to `+ add fallback`
11
+ once every project font is in the stack.
12
+
13
+ ## 0.64.2 — A stack can gain a second font
14
+
15
+ ### Fixed
16
+
17
+ - **A stack could not gain a second font.** "+ add fallback" only ever walked
18
+ the system-and-generic ladder, and the row `<select>` could only retarget a
19
+ row that already existed — so the one path to a font was to add a fallback
20
+ and then change it, and once every fallback was in use the button disabled
21
+ itself and the path closed. The add button now leads with any project font
22
+ the stack doesn't carry, names it (`+ add Domine`), and drops it in with the
23
+ other fonts above the fallbacks.
24
+
25
+ - **Duplicate slots survived 0.64 and made a stack look frozen.** 0.64 kept a
26
+ stack persisted by the old add bug renderable, but left the repeats in place:
27
+ two identical rows are indistinguishable, so dragging one past the other
28
+ changed nothing on screen, and each repeat ate a rung of the add ladder.
29
+ Repeats are now dropped as the stack is read, and no row offers a value
30
+ another row in the same stack holds, so the state can't be re-entered.
31
+
32
+ ## 0.64.1 — The sketch entry point carries its last symbol
33
+
34
+ ### Added
35
+
36
+ - **`hasPersistedSketchState` joins `@motion-proto/live-tokens/sketch`.** A site
37
+ moving its visitors off its own storage key has to guard that one-time carry
38
+ on whether the store has already recorded a decision, or it overwrites a pick
39
+ the visitor has since made in the Sketchstyle view with the stale one. It was
40
+ the only symbol that errand still needed and the only one 0.64.0 left inside,
41
+ so the bundler alias the entry point set out to retire survived for it alone.
42
+ Reading `lt.sketchTouched` directly was never the answer: the key is ours to
43
+ rename.
44
+
3
45
  ## 0.64.0 — A site picks the sketch through the front door
4
46
 
5
47
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@motion-proto/live-tokens",
3
- "version": "0.64.0",
3
+ "version": "0.64.3",
4
4
  "type": "module",
5
5
  "description": "Design token editor with live CSS variable editing. Svelte 5 + Vite 8.",
6
6
  "keywords": [
@@ -58,3 +58,13 @@ export function setSketch(id: string | null): void {
58
58
  selectSketchStyle(id);
59
59
  setSketchEnabled(true);
60
60
  }
61
+
62
+ /**
63
+ * Whether the store has recorded a decision of its own in this browser.
64
+ *
65
+ * For a consumer carrying visitors over from its own storage key: a one-time
66
+ * carry has to be guarded on this, or it overwrites a pick the visitor has
67
+ * since made in the editor's Sketchstyle view with the stale one. Reading the
68
+ * key directly is not an option worth offering — it is ours to rename.
69
+ */
70
+ export { hasPersistedSketchState } from './sketchStore';
@@ -64,11 +64,25 @@
64
64
  return slots;
65
65
  }
66
66
 
67
+ /** A repeated slot contributes nothing to the CSS list, and two identical
68
+ * rows are indistinguishable — dragging one past the other looks like a
69
+ * dead control. Stacks written by the pre-0.64 "+ add fallback" bug are
70
+ * still in localStorage, so drop repeats on the way in. */
71
+ function dedupeSlots(slots: FontStackSlot[]): FontStackSlot[] {
72
+ const seen = new Set<string>();
73
+ return slots.filter((slot) => {
74
+ const key = slotKey(slot);
75
+ if (seen.has(key)) return false;
76
+ seen.add(key);
77
+ return true;
78
+ });
79
+ }
80
+
67
81
  function ensureAllStacksPresent(current: FontStack[]): FontStack[] {
68
82
  const byVar = new Map(current.map((s) => [s.variable, s]));
69
83
  return STACK_VARIABLES.map((v) => {
70
84
  const stack = byVar.get(v);
71
- const slots = withTerminalFallback(v, stack?.slots ?? []);
85
+ const slots = withTerminalFallback(v, dedupeSlots(stack?.slots ?? []));
72
86
  return stack ? { ...stack, slots } : { variable: v, slots };
73
87
  });
74
88
  }
@@ -136,16 +150,19 @@
136
150
  });
137
151
  }
138
152
 
139
- /** Fallbacks offered by "+ add fallback", best match for the variable first.
140
- * Every stack already ships with its preferred generic *and* its preferred
141
- * preset, so both leading candidates are usually taken; the ladder gives the
142
- * button something unused to reach for. */
153
+ /** What the add button reaches for, in order of preference: a project font
154
+ * the stack doesn't carry yet, then the variable's preferred generic and
155
+ * preset, then the rest of the system-and-generic ladder. Project fonts lead
156
+ * because a stack that already holds every fallback otherwise had no way to
157
+ * gain a second font — the row's own <select> could only retarget a row that
158
+ * the button had to create first. */
143
159
  function addCandidates(variable: FontStackVariable): FontStackSlot[] {
144
160
  const generic: GenericFamily =
145
161
  variable === '--font-mono' ? 'monospace' : variable === '--font-serif' ? 'serif' : 'sans-serif';
146
162
  const preset: SystemCascadePreset =
147
163
  variable === '--font-mono' ? 'system-ui-mono' : variable === '--font-serif' ? 'system-ui-serif' : 'system-ui-sans';
148
164
  return [
165
+ ...allFamilies.map((f) => ({ kind: 'project' as const, familyId: f.id })),
149
166
  { kind: 'generic', value: generic },
150
167
  { kind: 'system', preset },
151
168
  ...SYSTEM_PRESETS.map((p) => ({ kind: 'system' as const, preset: p })),
@@ -153,6 +170,20 @@
153
170
  ];
154
171
  }
155
172
 
173
+ /** The terminal row offers its stack's matching preset and generic, minus
174
+ * whatever a row above holds — plus its own current value whatever that is.
175
+ * A <select> with no option matching its value renders the first option
176
+ * instead, so the row would read as a slot the stack doesn't have. */
177
+ function terminalOptions(variable: FontStackVariable, slot: FontStackSlot, taken: Set<string>): FontStackSlot[] {
178
+ const current = slotKey(slot);
179
+ const pair: FontStackSlot[] = [
180
+ { kind: 'system', preset: TERMINAL_SYSTEM_BY_VAR[variable] },
181
+ { kind: 'generic', value: TERMINAL_FALLBACK_BY_VAR[variable] },
182
+ ];
183
+ const offered = pair.filter((c) => slotKey(c) === current || !taken.has(slotKey(c)));
184
+ return offered.some((c) => slotKey(c) === current) ? offered : [slot, ...offered];
185
+ }
186
+
156
187
  /** A slot duplicated within a stack collides with itself in the keyed each,
157
188
  * so only one the stack doesn't already hold may be added. */
158
189
  function nextAddableSlot(variable: FontStackVariable): FontStackSlot | null {
@@ -161,14 +192,22 @@
161
192
  return addCandidates(variable).find((c) => !existing.has(slotKey(c))) ?? null;
162
193
  }
163
194
 
195
+ function addLabel(variable: FontStackVariable): string {
196
+ return nextAddableSlot(variable)?.kind === 'project' ? '+ add font' : '+ add fallback';
197
+ }
198
+
199
+ /** A font joins the other fonts, above the fallbacks; a fallback lands just
200
+ * above the terminal, which stays at the bottom. */
201
+ function insertIndexFor(slots: FontStackSlot[], slot: FontStackSlot): number {
202
+ if (slot.kind !== 'project') return Math.max(0, slots.length - 1);
203
+ return slots.reduce((acc, s, i) => (s.kind === 'project' ? i : acc), -1) + 1;
204
+ }
205
+
164
206
  function addSlot(variable: FontStackVariable) {
165
207
  const newSlot = nextAddableSlot(variable);
166
208
  if (!newSlot) return;
167
209
  updateStack(variable, (slots) => {
168
- // Insert above the terminal fallback (always the last slot) so the
169
- // terminal stays at the bottom.
170
- const insertAt = Math.max(0, slots.length - 1);
171
- slots.splice(insertAt, 0, newSlot);
210
+ slots.splice(insertIndexFor(slots, newSlot), 0, newSlot);
172
211
  return slots;
173
212
  });
174
213
  }
@@ -265,6 +304,9 @@
265
304
  <div class="font-stack-list">
266
305
  {#each keyedSlots(stack.slots) as { slot, key }, i (key)}
267
306
  {@const isTerminal = i === stack.slots.length - 1}
307
+ <!-- A value another row already holds is off this row's menu, so the
308
+ stack can't be edited back into the duplicate state. -->
309
+ {@const taken = new Set(stack.slots.filter((_, j) => j !== i).map(slotKey))}
268
310
  <!-- svelte-ignore a11y_no_static_element_interactions -->
269
311
  <div
270
312
  class="slot-row"
@@ -296,28 +338,34 @@
296
338
  onchange={(e) => onSelectChange(e, stack.variable, i)}
297
339
  >
298
340
  {#if isTerminal}
299
- {@const sys = TERMINAL_SYSTEM_BY_VAR[stack.variable]}
300
- {@const gen = TERMINAL_FALLBACK_BY_VAR[stack.variable]}
301
- <option value={`system:${sys}`}>{sys === 'system-ui-sans' ? 'System UI (sans)' : sys === 'system-ui-serif' ? 'System UI (serif)' : 'System UI (mono)'}</option>
302
- <option value={`generic:${gen}`}>{gen}</option>
341
+ {#each terminalOptions(stack.variable, slot, taken) as opt}
342
+ <option value={slotKey(opt)}>{slotDisplayName(opt)}</option>
343
+ {/each}
303
344
  {:else}
304
- {#if allFamilies.length > 0}
345
+ {@const families = allFamilies.filter((f) => !taken.has(`project:${f.id}`))}
346
+ {@const presets = SYSTEM_PRESETS.filter((p) => !taken.has(`system:${p}`))}
347
+ {@const generics = GENERIC_VALUES.filter((g) => !taken.has(`generic:${g}`))}
348
+ {#if families.length > 0}
305
349
  <optgroup label="Project fonts">
306
- {#each allFamilies as fam}
350
+ {#each families as fam}
307
351
  <option value={`project:${fam.id}`}>{fam.name}</option>
308
352
  {/each}
309
353
  </optgroup>
310
354
  {/if}
311
- <optgroup label="System cascade">
312
- {#each SYSTEM_PRESETS as p}
313
- <option value={`system:${p}`}>{p === 'system-ui-sans' ? 'System UI (sans)' : p === 'system-ui-serif' ? 'System UI (serif)' : 'System UI (mono)'}</option>
314
- {/each}
315
- </optgroup>
316
- <optgroup label="Generic">
317
- {#each GENERIC_VALUES as g}
318
- <option value={`generic:${g}`}>{g}</option>
319
- {/each}
320
- </optgroup>
355
+ {#if presets.length > 0}
356
+ <optgroup label="System cascade">
357
+ {#each presets as p}
358
+ <option value={`system:${p}`}>{p === 'system-ui-sans' ? 'System UI (sans)' : p === 'system-ui-serif' ? 'System UI (serif)' : 'System UI (mono)'}</option>
359
+ {/each}
360
+ </optgroup>
361
+ {/if}
362
+ {#if generics.length > 0}
363
+ <optgroup label="Generic">
364
+ {#each generics as g}
365
+ <option value={`generic:${g}`}>{g}</option>
366
+ {/each}
367
+ </optgroup>
368
+ {/if}
321
369
  {/if}
322
370
  </select>
323
371
  {#if isTerminal}
@@ -343,10 +391,10 @@
343
391
  type="button"
344
392
  class="add-fallback"
345
393
  disabled={nextAddableSlot(stack.variable) === null}
346
- title={nextAddableSlot(stack.variable) === null ? 'Every system and generic fallback is already in this stack' : undefined}
394
+ title={nextAddableSlot(stack.variable) === null ? 'Every project font and fallback is already in this stack' : undefined}
347
395
  onclick={() => addSlot(stack.variable)}
348
396
  >
349
- + add fallback
397
+ {addLabel(stack.variable)}
350
398
  </button>
351
399
  </div>
352
400
  {/each}