@motion-proto/live-tokens 0.64.0 → 0.64.2

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,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.64.2 — A stack can gain a second font
4
+
5
+ ### Fixed
6
+
7
+ - **A stack could not gain a second font.** "+ add fallback" only ever walked
8
+ the system-and-generic ladder, and the row `<select>` could only retarget a
9
+ row that already existed — so the one path to a font was to add a fallback
10
+ and then change it, and once every fallback was in use the button disabled
11
+ itself and the path closed. The add button now leads with any project font
12
+ the stack doesn't carry, names it (`+ add Domine`), and drops it in with the
13
+ other fonts above the fallbacks.
14
+
15
+ - **Duplicate slots survived 0.64 and made a stack look frozen.** 0.64 kept a
16
+ stack persisted by the old add bug renderable, but left the repeats in place:
17
+ two identical rows are indistinguishable, so dragging one past the other
18
+ changed nothing on screen, and each repeat ate a rung of the add ladder.
19
+ Repeats are now dropped as the stack is read, and no row offers a value
20
+ another row in the same stack holds, so the state can't be re-entered.
21
+
22
+ ## 0.64.1 — The sketch entry point carries its last symbol
23
+
24
+ ### Added
25
+
26
+ - **`hasPersistedSketchState` joins `@motion-proto/live-tokens/sketch`.** A site
27
+ moving its visitors off its own storage key has to guard that one-time carry
28
+ on whether the store has already recorded a decision, or it overwrites a pick
29
+ the visitor has since made in the Sketchstyle view with the stale one. It was
30
+ the only symbol that errand still needed and the only one 0.64.0 left inside,
31
+ so the bundler alias the entry point set out to retire survived for it alone.
32
+ Reading `lt.sketchTouched` directly was never the answer: the key is ours to
33
+ rename.
34
+
3
35
  ## 0.64.0 — A site picks the sketch through the front door
4
36
 
5
37
  ### 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.2",
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,23 @@
161
192
  return addCandidates(variable).find((c) => !existing.has(slotKey(c))) ?? null;
162
193
  }
163
194
 
195
+ function addLabel(variable: FontStackVariable): string {
196
+ const next = nextAddableSlot(variable);
197
+ return next?.kind === 'project' ? `+ add ${slotDisplayName(next)}` : '+ add fallback';
198
+ }
199
+
200
+ /** A font joins the other fonts, above the fallbacks; a fallback lands just
201
+ * above the terminal, which stays at the bottom. */
202
+ function insertIndexFor(slots: FontStackSlot[], slot: FontStackSlot): number {
203
+ if (slot.kind !== 'project') return Math.max(0, slots.length - 1);
204
+ return slots.reduce((acc, s, i) => (s.kind === 'project' ? i : acc), -1) + 1;
205
+ }
206
+
164
207
  function addSlot(variable: FontStackVariable) {
165
208
  const newSlot = nextAddableSlot(variable);
166
209
  if (!newSlot) return;
167
210
  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);
211
+ slots.splice(insertIndexFor(slots, newSlot), 0, newSlot);
172
212
  return slots;
173
213
  });
174
214
  }
@@ -265,6 +305,9 @@
265
305
  <div class="font-stack-list">
266
306
  {#each keyedSlots(stack.slots) as { slot, key }, i (key)}
267
307
  {@const isTerminal = i === stack.slots.length - 1}
308
+ <!-- A value another row already holds is off this row's menu, so the
309
+ stack can't be edited back into the duplicate state. -->
310
+ {@const taken = new Set(stack.slots.filter((_, j) => j !== i).map(slotKey))}
268
311
  <!-- svelte-ignore a11y_no_static_element_interactions -->
269
312
  <div
270
313
  class="slot-row"
@@ -296,28 +339,34 @@
296
339
  onchange={(e) => onSelectChange(e, stack.variable, i)}
297
340
  >
298
341
  {#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>
342
+ {#each terminalOptions(stack.variable, slot, taken) as opt}
343
+ <option value={slotKey(opt)}>{slotDisplayName(opt)}</option>
344
+ {/each}
303
345
  {:else}
304
- {#if allFamilies.length > 0}
346
+ {@const families = allFamilies.filter((f) => !taken.has(`project:${f.id}`))}
347
+ {@const presets = SYSTEM_PRESETS.filter((p) => !taken.has(`system:${p}`))}
348
+ {@const generics = GENERIC_VALUES.filter((g) => !taken.has(`generic:${g}`))}
349
+ {#if families.length > 0}
305
350
  <optgroup label="Project fonts">
306
- {#each allFamilies as fam}
351
+ {#each families as fam}
307
352
  <option value={`project:${fam.id}`}>{fam.name}</option>
308
353
  {/each}
309
354
  </optgroup>
310
355
  {/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>
356
+ {#if presets.length > 0}
357
+ <optgroup label="System cascade">
358
+ {#each presets as p}
359
+ <option value={`system:${p}`}>{p === 'system-ui-sans' ? 'System UI (sans)' : p === 'system-ui-serif' ? 'System UI (serif)' : 'System UI (mono)'}</option>
360
+ {/each}
361
+ </optgroup>
362
+ {/if}
363
+ {#if generics.length > 0}
364
+ <optgroup label="Generic">
365
+ {#each generics as g}
366
+ <option value={`generic:${g}`}>{g}</option>
367
+ {/each}
368
+ </optgroup>
369
+ {/if}
321
370
  {/if}
322
371
  </select>
323
372
  {#if isTerminal}
@@ -343,10 +392,10 @@
343
392
  type="button"
344
393
  class="add-fallback"
345
394
  disabled={nextAddableSlot(stack.variable) === null}
346
- title={nextAddableSlot(stack.variable) === null ? 'Every system and generic fallback is already in this stack' : undefined}
395
+ title={nextAddableSlot(stack.variable) === null ? 'Every project font and fallback is already in this stack' : undefined}
347
396
  onclick={() => addSlot(stack.variable)}
348
397
  >
349
- + add fallback
398
+ {addLabel(stack.variable)}
350
399
  </button>
351
400
  </div>
352
401
  {/each}