@motion-proto/live-tokens 0.7.1 → 0.8.0

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.
Files changed (85) hide show
  1. package/dist-plugin/index.cjs +707 -90
  2. package/dist-plugin/index.d.cts +1 -0
  3. package/dist-plugin/index.d.ts +1 -0
  4. package/dist-plugin/index.js +707 -90
  5. package/package.json +2 -1
  6. package/src/app/site.css +1 -1
  7. package/src/editor/component-editor/CollapsibleSectionEditor.svelte +34 -27
  8. package/src/editor/component-editor/DialogEditor.svelte +4 -4
  9. package/src/editor/component-editor/NotificationEditor.svelte +3 -1
  10. package/src/editor/component-editor/SectionDividerEditor.svelte +439 -112
  11. package/src/editor/component-editor/StandardButtonsEditor.svelte +13 -1
  12. package/src/editor/component-editor/editors.d.ts +10 -0
  13. package/src/editor/component-editor/scaffolding/AngleDial.svelte +52 -13
  14. package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +10 -11
  15. package/src/editor/component-editor/scaffolding/LinkedBlock.svelte +0 -1
  16. package/src/editor/component-editor/scaffolding/RadialShapePad.svelte +483 -0
  17. package/src/editor/component-editor/scaffolding/ShadowBackdrop.svelte +15 -2
  18. package/src/editor/component-editor/scaffolding/StateBlock.svelte +103 -15
  19. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +9 -6
  20. package/src/editor/component-editor/scaffolding/TypeEditor.svelte +13 -1
  21. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +239 -25
  22. package/src/editor/component-editor/scaffolding/buildTypeGroupTokens.ts +1 -0
  23. package/src/editor/component-editor/scaffolding/types.ts +11 -0
  24. package/src/editor/core/components/componentConfigKeys.ts +8 -0
  25. package/src/editor/core/components/componentConfigService.ts +2 -2
  26. package/src/editor/core/components/componentPersist.ts +7 -5
  27. package/src/editor/core/manifests/manifestService.ts +58 -3
  28. package/src/editor/core/palettes/familySwap.ts +99 -0
  29. package/src/editor/core/palettes/paletteDerivation.ts +69 -0
  30. package/src/editor/core/palettes/tokenRegistry.ts +4 -1
  31. package/src/editor/core/store/editorStore.ts +206 -12
  32. package/src/editor/core/store/editorTypes.ts +55 -12
  33. package/src/editor/core/store/gradientSource.ts +192 -0
  34. package/src/editor/core/themes/migrations/2026-05-19-collapsiblesection-drop-frame-surface.ts +28 -0
  35. package/src/editor/core/themes/migrations/2026-05-19-sectiondivider-rich-gradient.ts +35 -0
  36. package/src/editor/core/themes/migrations/2026-05-20-sectiondivider-slim-variants.ts +82 -0
  37. package/src/editor/core/themes/migrations/2026-05-21-sectiondivider-spacing-to-padding.ts +24 -0
  38. package/src/editor/core/themes/migrations/2026-05-22-sectiondivider-intrinsics-to-css.ts +81 -0
  39. package/src/editor/core/themes/migrations/index.ts +10 -0
  40. package/src/editor/core/themes/slices/components.ts +18 -4
  41. package/src/editor/core/themes/slices/gradients.ts +88 -13
  42. package/src/editor/core/themes/themeInit.ts +2 -2
  43. package/src/editor/core/themes/themeTypes.ts +56 -1
  44. package/src/editor/overlay/ColumnsOverlay.svelte +0 -1
  45. package/src/editor/overlay/LiveEditorOverlay.svelte +1 -4
  46. package/src/editor/styles/ui-editor.css +1 -0
  47. package/src/editor/styles/ui-form-controls.css +19 -20
  48. package/src/editor/ui/BezierCurveEditor.svelte +114 -63
  49. package/src/editor/ui/EditorViewSwitcher.svelte +0 -1
  50. package/src/editor/ui/FileLoadList.svelte +22 -5
  51. package/src/editor/ui/FontStackEditor.svelte +214 -76
  52. package/src/editor/ui/GradientEditor.svelte +435 -215
  53. package/src/editor/ui/GradientStopPicker.svelte +11 -3
  54. package/src/editor/ui/ManifestFileManager.svelte +71 -4
  55. package/src/editor/ui/PaletteEditor.svelte +52 -79
  56. package/src/editor/ui/ProjectFontsSection.svelte +328 -293
  57. package/src/editor/ui/ThemeFileManager.svelte +0 -4
  58. package/src/editor/ui/UIFontFamilySelector.svelte +0 -1
  59. package/src/editor/ui/UIFontSizeSelector.svelte +3 -0
  60. package/src/editor/ui/UIInfoPopover.svelte +0 -1
  61. package/src/editor/ui/UILetterSpacingSelector.svelte +65 -0
  62. package/src/editor/ui/UIPaletteSelector.svelte +31 -4
  63. package/src/editor/ui/UIPillButton.svelte +33 -3
  64. package/src/editor/ui/UISegmentedControl.svelte +114 -0
  65. package/src/editor/ui/UITokenSelector.svelte +4 -1
  66. package/src/editor/ui/VariablesTab.svelte +41 -35
  67. package/src/editor/ui/palette/OverridesPanel.svelte +14 -37
  68. package/src/editor/ui/palette/PaletteBase.svelte +3 -3
  69. package/src/editor/ui/sections/ColumnsSection.svelte +1 -2
  70. package/src/editor/ui/sections/GradientsSection.svelte +1 -1
  71. package/src/editor/ui/sections/OverlaysSection.svelte +1 -1
  72. package/src/editor/ui/sections/ShadowsSection.svelte +1 -1
  73. package/src/system/components/Button.svelte +2 -2
  74. package/src/system/components/Card.svelte +29 -1
  75. package/src/system/components/CollapsibleSection.svelte +25 -2
  76. package/src/system/components/FloatingTokenTags.css +43 -24
  77. package/src/system/components/FloatingTokenTags.svelte +88 -137
  78. package/src/system/components/Notification.svelte +8 -1
  79. package/src/system/components/SectionDivider.svelte +456 -379
  80. package/src/system/styles/CONVENTIONS.md +1 -1
  81. package/src/system/styles/fonts.css +3 -16
  82. package/src/system/styles/tokens.css +356 -1199
  83. package/src/system/styles/tokens.generated.css +544 -0
  84. package/src/editor/component-editor/scaffolding/DividerEditor.svelte +0 -94
  85. package/src/editor/component-editor/scaffolding/GradientCard.svelte +0 -296
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { flip } from 'svelte/animate';
3
+ import { cubicOut } from 'svelte/easing';
2
4
  import type {
3
5
  FontFamily,
4
6
  FontSource,
@@ -12,7 +14,11 @@
12
14
  import { applyFontStacks, SYSTEM_CASCADES } from '../core/fonts/fontLoader';
13
15
 
14
16
  const SYSTEM_PRESETS: SystemCascadePreset[] = ['system-ui-sans', 'system-ui-serif', 'system-ui-mono'];
15
- const GENERIC_VALUES: GenericFamily[] = ['sans-serif', 'serif', 'monospace', 'cursive', 'fantasy'];
17
+ // `cursive` and `fantasy` are CSS-spec generics whose rendering varies wildly
18
+ // across OSes (cursive → Comic Sans / Snell Roundhand; fantasy → Impact /
19
+ // Papyrus). They're rarely what a designer means by "fallback," so we don't
20
+ // offer them in the editor.
21
+ const GENERIC_VALUES: GenericFamily[] = ['sans-serif', 'serif', 'monospace'];
16
22
 
17
23
  const STACK_VARIABLES: FontStackVariable[] = [
18
24
  '--font-display',
@@ -21,18 +27,55 @@
21
27
  '--font-mono',
22
28
  ];
23
29
 
30
+ // Each stack's terminal fallback (bottom slot). It's locked to system or
31
+ // generic so text always renders even if every project font 404s. The
32
+ // fallback maps by stack variable; chosen to match the stack's purpose.
33
+ const TERMINAL_FALLBACK_BY_VAR: Record<FontStackVariable, GenericFamily> = {
34
+ '--font-display': 'serif',
35
+ '--font-sans': 'sans-serif',
36
+ '--font-serif': 'serif',
37
+ '--font-mono': 'monospace',
38
+ };
39
+
40
+ // The single matching System UI preset paired with each stack — the only two
41
+ // options offered in the terminal slot's dropdown.
42
+ const TERMINAL_SYSTEM_BY_VAR: Record<FontStackVariable, SystemCascadePreset> = {
43
+ '--font-display': 'system-ui-serif',
44
+ '--font-sans': 'system-ui-sans',
45
+ '--font-serif': 'system-ui-serif',
46
+ '--font-mono': 'system-ui-mono',
47
+ };
48
+
24
49
  let fontSourcesList = $derived($editorState.fonts.sources);
25
50
  let fontStacksList = $derived($editorState.fonts.stacks);
26
51
  let allFamilies = $derived((fontSourcesList as FontSource[]).flatMap((s) => s.families.map((f) => ({ ...f, sourceLabel: s.label ?? s.kind }))));
27
52
  let familyById = $derived(new Map<string, FontFamily>(allFamilies.map((f) => [f.id, f])));
28
53
 
54
+ /** Ensure the slot list ends in a system/generic terminal. If it doesn't,
55
+ * append the variable's default generic so the stack is always renderable. */
56
+ function withTerminalFallback(variable: FontStackVariable, slots: FontStackSlot[]): FontStackSlot[] {
57
+ const fallback: FontStackSlot = { kind: 'generic', value: TERMINAL_FALLBACK_BY_VAR[variable] };
58
+ if (slots.length === 0) return [fallback];
59
+ const last = slots[slots.length - 1];
60
+ if (last.kind === 'project') return [...slots, fallback];
61
+ return slots;
62
+ }
63
+
29
64
  function ensureAllStacksPresent(current: FontStack[]): FontStack[] {
30
65
  const byVar = new Map(current.map((s) => [s.variable, s]));
31
- return STACK_VARIABLES.map((v) => byVar.get(v) ?? { variable: v, slots: [{ kind: 'generic', value: 'sans-serif' } as FontStackSlot] });
66
+ return STACK_VARIABLES.map((v) => {
67
+ const stack = byVar.get(v);
68
+ const slots = withTerminalFallback(v, stack?.slots ?? []);
69
+ return stack ? { ...stack, slots } : { variable: v, slots };
70
+ });
32
71
  }
33
72
 
34
73
  let stacks = $derived(ensureAllStacksPresent(fontStacksList));
35
74
 
75
+ function variableLabel(v: string): string {
76
+ return v.replace(/^--/, '').split('-').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
77
+ }
78
+
36
79
  function slotKey(slot: FontStackSlot): string {
37
80
  if (slot.kind === 'project') return `project:${slot.familyId}`;
38
81
  if (slot.kind === 'system') return `system:${slot.preset}`;
@@ -103,19 +146,37 @@
103
146
  newSlot = { kind: 'system', preset };
104
147
  }
105
148
  updateStack(variable, (slots) => {
106
- slots.push(newSlot);
149
+ // Insert above the terminal fallback (always the last slot) so the
150
+ // terminal stays at the bottom.
151
+ const insertAt = Math.max(0, slots.length - 1);
152
+ slots.splice(insertAt, 0, newSlot);
107
153
  return slots;
108
154
  });
109
155
  }
110
156
 
157
+ /* Drag UX: the source row lifts (opacity, shadow); a white insertion bar
158
+ sits in the gap between rows at the projected drop position. The array
159
+ is only mutated on drop. animate:flip then slides every row to its new
160
+ spot, so the commit doesn't snap.
161
+
162
+ Only the drag handle is `draggable="true"` — putting it on the whole row
163
+ swallowed clicks on the inner <button>/<select> in real browsers because
164
+ mousedown started a drag gesture before `click` could fire. The handle
165
+ starts the drag and calls setDragImage(rowEl, ...) so the visual drag
166
+ image is still the whole row. */
111
167
  let dragSource: { variable: FontStackVariable; index: number } | null = $state(null);
112
- let dragOver: { variable: FontStackVariable; index: number; position: 'before' | 'on' | 'after' } | null = $state(null);
168
+ let dragOver: { variable: FontStackVariable; index: number; position: 'before' | 'after' } | null = $state(null);
113
169
 
114
170
  function onDragStart(e: DragEvent, variable: FontStackVariable, index: number) {
115
171
  if (!e.dataTransfer) return;
116
172
  dragSource = { variable, index };
117
173
  e.dataTransfer.effectAllowed = 'move';
118
174
  e.dataTransfer.setData('application/x-font-slot', JSON.stringify({ variable, index }));
175
+ const rowEl = (e.currentTarget as HTMLElement).closest('.slot-row') as HTMLElement | null;
176
+ if (rowEl) {
177
+ const rect = rowEl.getBoundingClientRect();
178
+ e.dataTransfer.setDragImage(rowEl, e.clientX - rect.left, e.clientY - rect.top);
179
+ }
119
180
  }
120
181
 
121
182
  function onDragOver(e: DragEvent, variable: FontStackVariable, index: number) {
@@ -124,9 +185,12 @@
124
185
  e.preventDefault();
125
186
  if (e.dataTransfer) e.dataTransfer.dropEffect = 'move';
126
187
  const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
127
- const y = e.clientY - rect.top;
128
- const third = rect.height / 3;
129
- const position = y < third ? 'before' : y > rect.height - third ? 'after' : 'on';
188
+ let position: 'before' | 'after' = e.clientY - rect.top < rect.height / 2 ? 'before' : 'after';
189
+ // Terminal fallback stays at the bottom — never accept a drop after it.
190
+ const stack = stacks.find((s) => s.variable === variable);
191
+ if (stack && index === stack.slots.length - 1 && position === 'after') {
192
+ position = 'before';
193
+ }
130
194
  dragOver = { variable, index, position };
131
195
  }
132
196
 
@@ -137,12 +201,12 @@
137
201
  function onDrop(e: DragEvent, variable: FontStackVariable, index: number) {
138
202
  e.preventDefault();
139
203
  const slotPayload = e.dataTransfer?.getData('application/x-font-slot');
140
- const position = dragOver?.position ?? 'on';
204
+ const position = dragOver?.position ?? 'before';
141
205
  dragOver = null;
142
206
  if (!slotPayload) return;
143
207
  const src = JSON.parse(slotPayload) as { variable: FontStackVariable; index: number };
144
208
  if (src.variable !== variable) return;
145
- if (src.index === index && position === 'on') return;
209
+ if (src.index === index) return;
146
210
  updateStack(variable, (slots) => {
147
211
  const [moved] = slots.splice(src.index, 1);
148
212
  let target = index;
@@ -163,63 +227,82 @@
163
227
  {#each stacks as stack (stack.variable)}
164
228
  <div class="font-stack">
165
229
  <div class="stack-header">
166
- <span class="stack-variable">{stack.variable}</span>
230
+ <span class="stack-variable">{variableLabel(stack.variable)}</span>
167
231
  </div>
168
232
  <div class="font-stack-list">
169
- {#each stack.slots as slot, i (i + ':' + slotKey(slot))}
233
+ {#each stack.slots as slot, i (slotKey(slot))}
234
+ {@const isTerminal = i === stack.slots.length - 1}
170
235
  <!-- svelte-ignore a11y_no_static_element_interactions -->
171
236
  <div
172
237
  class="slot-row"
173
- class:drop-on={dragOver?.variable === stack.variable && dragOver?.index === i && dragOver?.position === 'on'}
238
+ class:terminal={isTerminal}
174
239
  class:drop-before={dragOver?.variable === stack.variable && dragOver?.index === i && dragOver?.position === 'before'}
175
240
  class:drop-after={dragOver?.variable === stack.variable && dragOver?.index === i && dragOver?.position === 'after'}
176
241
  class:dragging={dragSource?.variable === stack.variable && dragSource?.index === i}
177
- draggable="true"
178
- ondragstart={(e) => onDragStart(e, stack.variable, i)}
179
242
  ondragover={(e) => onDragOver(e, stack.variable, i)}
180
243
  ondragleave={onDragLeave}
181
244
  ondrop={(e) => onDrop(e, stack.variable, i)}
182
245
  ondragend={onDragEnd}
246
+ animate:flip={{ duration: 220, easing: cubicOut }}
183
247
  >
184
- <span class="drag-handle" aria-hidden="true">⋮⋮</span>
185
- <span class="slot-position">{i + 1}.</span>
186
- <div class="slot-main">
187
- <span
188
- class="slot-preview"
189
- style="font-family: {slotCssValue(slot)};{stack.variable === '--font-display' ? ' font-size: var(--ui-font-size-2xl);' : ''}"
190
- >The quick brown fox jumps over the lazy dog</span>
248
+ <div class="slot-controls">
249
+ {#if isTerminal}
250
+ <i class="fas fa-lock slot-locked-glyph" aria-hidden="true" title="Final fallback — can't be removed"></i>
251
+ {:else}
252
+ <span
253
+ class="drag-handle"
254
+ aria-hidden="true"
255
+ draggable="true"
256
+ ondragstart={(e) => onDragStart(e, stack.variable, i)}
257
+ >⋮⋮</span>
258
+ {/if}
259
+ <span class="slot-position">{i + 1}.</span>
191
260
  <select
192
261
  class="ui-form-select slot-select"
193
262
  value={slotKey(slot)}
194
263
  onchange={(e) => onSelectChange(e, stack.variable, i)}
195
264
  >
196
- {#if allFamilies.length > 0}
197
- <optgroup label="Project fonts">
198
- {#each allFamilies as fam}
199
- <option value={`project:${fam.id}`}>{fam.name}</option>
265
+ {#if isTerminal}
266
+ {@const sys = TERMINAL_SYSTEM_BY_VAR[stack.variable]}
267
+ {@const gen = TERMINAL_FALLBACK_BY_VAR[stack.variable]}
268
+ <option value={`system:${sys}`}>{sys === 'system-ui-sans' ? 'System UI (sans)' : sys === 'system-ui-serif' ? 'System UI (serif)' : 'System UI (mono)'}</option>
269
+ <option value={`generic:${gen}`}>{gen}</option>
270
+ {:else}
271
+ {#if allFamilies.length > 0}
272
+ <optgroup label="Project fonts">
273
+ {#each allFamilies as fam}
274
+ <option value={`project:${fam.id}`}>{fam.name}</option>
275
+ {/each}
276
+ </optgroup>
277
+ {/if}
278
+ <optgroup label="System cascade">
279
+ {#each SYSTEM_PRESETS as p}
280
+ <option value={`system:${p}`}>{p === 'system-ui-sans' ? 'System UI (sans)' : p === 'system-ui-serif' ? 'System UI (serif)' : 'System UI (mono)'}</option>
281
+ {/each}
282
+ </optgroup>
283
+ <optgroup label="Generic">
284
+ {#each GENERIC_VALUES as g}
285
+ <option value={`generic:${g}`}>{g}</option>
200
286
  {/each}
201
287
  </optgroup>
202
288
  {/if}
203
- <optgroup label="System cascade">
204
- {#each SYSTEM_PRESETS as p}
205
- <option value={`system:${p}`}>{p === 'system-ui-sans' ? 'System UI (sans)' : p === 'system-ui-serif' ? 'System UI (serif)' : 'System UI (mono)'}</option>
206
- {/each}
207
- </optgroup>
208
- <optgroup label="Generic">
209
- {#each GENERIC_VALUES as g}
210
- <option value={`generic:${g}`}>{g}</option>
211
- {/each}
212
- </optgroup>
213
289
  </select>
290
+ {#if isTerminal}
291
+ <span class="slot-remove-placeholder" aria-hidden="true"></span>
292
+ {:else}
293
+ <button
294
+ type="button"
295
+ class="slot-remove"
296
+ aria-label="Remove slot"
297
+ title="Remove"
298
+ onclick={() => removeSlot(stack.variable, i)}
299
+ >×</button>
300
+ {/if}
214
301
  </div>
215
- <button
216
- type="button"
217
- class="slot-remove"
218
- aria-label="Remove slot"
219
- title="Remove"
220
- onclick={() => removeSlot(stack.variable, i)}
221
- disabled={stack.slots.length <= 1}
222
- >×</button>
302
+ <span
303
+ class="slot-preview"
304
+ style="font-family: {slotCssValue(slot)};{stack.variable === '--font-display' ? ' font-size: var(--ui-font-size-2xl);' : ''}"
305
+ >The quick brown fox jumps over the lazy dog</span>
223
306
  </div>
224
307
  {/each}
225
308
  </div>
@@ -233,47 +316,104 @@
233
316
  <style>
234
317
  .font-stacks-columns {
235
318
  display: grid;
236
- grid-template-columns: repeat(auto-fit, minmax(min(14rem, 100%), 1fr));
237
- gap: var(--ui-space-8);
319
+ grid-template-columns: repeat(2, minmax(0, 1fr));
320
+ gap: var(--ui-space-24);
238
321
  }
239
322
 
323
+ @media (max-width: 720px) {
324
+ .font-stacks-columns {
325
+ grid-template-columns: 1fr;
326
+ }
327
+ }
328
+
329
+ /* Track: each font family is a bordered channel that the slot cards drop into.
330
+ The interior sits a step deeper than the page surface, so the slot cards
331
+ (surface-low) read as raised inside the depression. */
240
332
  .font-stack {
333
+ position: relative;
241
334
  display: flex;
242
335
  flex-direction: column;
243
- gap: var(--ui-space-6);
244
- padding: var(--ui-space-12);
245
- background: none;
336
+ gap: var(--ui-space-12);
337
+ padding: var(--ui-space-20) var(--ui-space-12) var(--ui-space-16);
338
+ background: var(--ui-surface-lowest);
246
339
  border: 1px solid var(--ui-border-low);
247
- border-radius: var(--ui-radius-md);
340
+ border-radius: var(--ui-radius-lg);
248
341
  }
249
342
 
343
+ /* Legend: knock through the top border like a native <fieldset>'s <legend>.
344
+ Editor content bg is solid black; the header paints over the border line
345
+ to cut it. */
250
346
  .stack-header {
347
+ position: absolute;
348
+ top: 0;
349
+ left: var(--ui-space-12);
350
+ transform: translateY(-50%);
251
351
  display: flex;
252
352
  align-items: center;
353
+ padding: 0 var(--ui-space-6);
354
+ background: black;
355
+ line-height: 1;
253
356
  }
254
357
 
255
358
  .stack-variable {
256
- font-family: var(--ui-font-mono);
257
- font-size: var(--ui-font-size-md);
359
+ font-size: var(--ui-font-size-lg);
360
+ font-weight: var(--ui-font-weight-bold);
258
361
  color: var(--ui-text-primary);
259
362
  }
260
363
 
261
364
  .font-stack-list {
262
365
  display: flex;
263
366
  flex-direction: column;
264
- gap: var(--ui-space-4);
367
+ gap: var(--ui-space-8);
265
368
  }
266
369
 
267
370
  .slot-row {
371
+ display: flex;
372
+ flex-direction: column;
373
+ gap: var(--ui-space-8);
374
+ padding: var(--ui-space-10);
375
+ border: 1px solid var(--ui-border-low);
376
+ border-radius: var(--ui-radius-md);
377
+ background: var(--ui-surface-low);
378
+ position: relative;
379
+ transition:
380
+ opacity var(--ui-transition-fast),
381
+ transform var(--ui-transition-fast),
382
+ border-color var(--ui-transition-fast);
383
+ }
384
+ .slot-row:hover { border-color: var(--ui-border); }
385
+
386
+ .slot-controls {
268
387
  display: grid;
269
388
  grid-template-columns: auto auto 1fr auto;
270
389
  align-items: center;
271
- gap: var(--ui-space-6);
272
- padding: var(--ui-space-4) 0;
273
- border-bottom: 1px solid var(--ui-border-low);
274
- position: relative;
390
+ gap: var(--ui-space-8);
391
+ }
392
+
393
+ /* Reorder UX: a thin white bar sits in the existing 8px gap between rows
394
+ as the insertion indicator. The bar is absolutely positioned and consumes
395
+ no layout space, so the parent track stays the same height. On drop, the
396
+ array commits and animate:flip slides each row to its new position. */
397
+ .slot-row.drop-before::before,
398
+ .slot-row.drop-after::after {
399
+ content: '';
400
+ position: absolute;
401
+ left: 0;
402
+ right: 0;
403
+ height: 2px;
404
+ background: var(--ui-text-primary);
405
+ border-radius: 1px;
406
+ box-shadow: 0 0 6px rgba(255, 255, 255, 0.45);
407
+ }
408
+ .slot-row.drop-before::before { top: -5px; }
409
+ .slot-row.drop-after::after { bottom: -5px; }
410
+
411
+ .slot-row.dragging {
412
+ opacity: 0.55;
413
+ z-index: 2;
414
+ border-color: var(--ui-border-high);
415
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.5);
275
416
  }
276
- .slot-row:last-child { border-bottom: none; }
277
417
 
278
418
  .drag-handle {
279
419
  cursor: grab;
@@ -281,22 +421,27 @@
281
421
  color: var(--ui-text-muted);
282
422
  font-size: var(--ui-font-size-md);
283
423
  line-height: 1;
284
- letter-spacing: -2px;
285
424
  }
286
425
  .slot-row.dragging .drag-handle { cursor: grabbing; }
287
- .slot-row.dragging { opacity: 0.55; }
288
426
 
289
- .slot-row.drop-on { outline: 2px solid var(--ui-focus, #5eb2ff); outline-offset: -2px; }
290
- .slot-row.drop-before::before,
291
- .slot-row.drop-after::after {
292
- content: '';
293
- position: absolute;
294
- left: 0; right: 0;
295
- height: 2px;
296
- background: var(--ui-focus, #5eb2ff);
427
+ /* Terminal-row lock glyph sits in the drag-handle's grid track; the row's
428
+ right-hand X column is left empty (see .slot-remove-placeholder) so the
429
+ lock is the only chrome and reads as "this row is fixed." */
430
+ .slot-locked-glyph {
431
+ display: inline-flex;
432
+ align-items: center;
433
+ justify-content: center;
434
+ color: var(--ui-text-muted);
435
+ font-size: var(--ui-font-size-sm);
436
+ opacity: 0.55;
437
+ }
438
+
439
+ /* Empty grid track on terminal rows where the X button sits on others,
440
+ so columns stay aligned. */
441
+ .slot-remove-placeholder {
442
+ width: 1.5rem;
443
+ height: 1.5rem;
297
444
  }
298
- .slot-row.drop-before::before { top: -1px; }
299
- .slot-row.drop-after::after { bottom: -1px; }
300
445
 
301
446
  .slot-position {
302
447
  font-size: var(--ui-font-size-md);
@@ -305,13 +450,6 @@
305
450
  text-align: right;
306
451
  }
307
452
 
308
- .slot-main {
309
- display: flex;
310
- flex-direction: column;
311
- gap: var(--ui-space-2);
312
- min-width: 0;
313
- }
314
-
315
453
  .slot-preview {
316
454
  font-size: var(--ui-font-size-md);
317
455
  color: var(--ui-text-primary);