@motion-proto/live-tokens 0.72.1 → 0.74.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 (81) hide show
  1. package/.claude/skills/live-tokens-build-page/SKILL.md +37 -2
  2. package/.claude/skills/live-tokens-build-page/references/layout-sources.md +48 -0
  3. package/.claude/skills/live-tokens-check-compliance/SKILL.md +2 -2
  4. package/.claude/skills/live-tokens-create-component/SKILL.md +4 -4
  5. package/.claude/skills/live-tokens-create-theme/SKILL.md +80 -0
  6. package/.claude/skills/live-tokens-create-theme/references/design-directions.md +94 -0
  7. package/.claude/skills/live-tokens-pick-component/SKILL.md +1 -1
  8. package/.claude/skills/live-tokens-set-colors/SKILL.md +140 -0
  9. package/.claude/skills/live-tokens-set-colors/references/color-anchors.md +80 -0
  10. package/.claude/skills/{live-tokens-adjust-geometry → live-tokens-set-geometry}/SKILL.md +12 -7
  11. package/.claude/skills/live-tokens-set-geometry/references/geometry-anchors.md +61 -0
  12. package/.claude/skills/{live-tokens-pair-fonts → live-tokens-set-type}/SKILL.md +18 -15
  13. package/.claude/skills/live-tokens-set-type/references/type-anchors.md +60 -0
  14. package/CHANGELOG.md +141 -0
  15. package/README.md +24 -15
  16. package/bin/check-page.mjs +3 -3
  17. package/bin/cli.mjs +92 -55
  18. package/bin/lib/liveState.mjs +110 -0
  19. package/bin/save-theme.mjs +177 -0
  20. package/bin/set-colors.mjs +191 -0
  21. package/bin/{adjust.mjs → set-geometry.mjs} +18 -50
  22. package/bin/{set-fonts.mjs → set-type.mjs} +21 -55
  23. package/dist-plugin/{chunk-RIXO2E55.js → chunk-7VRTBGJT.js} +1 -1
  24. package/dist-plugin/{chunk-YLCOIGQC.js → chunk-V3YF6CGT.js} +56 -2
  25. package/dist-plugin/index.cjs +58 -3
  26. package/dist-plugin/index.js +11 -11
  27. package/dist-plugin/migrateData/index.cjs +56 -1
  28. package/dist-plugin/migrateData/index.js +2 -2
  29. package/dist-plugin/{generateColorsAndType → setColors}/index.cjs +1109 -1071
  30. package/dist-plugin/{generateColorsAndType → setColors}/index.d.cts +33 -24
  31. package/dist-plugin/{generateColorsAndType → setColors}/index.d.ts +33 -24
  32. package/dist-plugin/{generateColorsAndType → setColors}/index.js +45 -63
  33. package/dist-plugin/{adjust → setGeometry}/index.cjs +60 -5
  34. package/dist-plugin/{adjust → setGeometry}/index.d.cts +1 -1
  35. package/dist-plugin/{adjust → setGeometry}/index.d.ts +1 -1
  36. package/dist-plugin/{adjust → setGeometry}/index.js +1 -1
  37. package/dist-plugin/{fontPairing → setType}/index.cjs +4 -4
  38. package/dist-plugin/{fontPairing → setType}/index.d.cts +1 -1
  39. package/dist-plugin/{fontPairing → setType}/index.d.ts +1 -1
  40. package/dist-plugin/{themeTypes-DSV3Zisf.d.cts → themeTypes-BxRtuN5V.d.cts} +1 -1
  41. package/dist-plugin/{themeTypes-DSV3Zisf.d.ts → themeTypes-BxRtuN5V.d.ts} +1 -1
  42. package/package.json +10 -2
  43. package/src/editor/core/themes/{generateColorsAndType.ts → buildColors.ts} +82 -96
  44. package/src/editor/core/themes/migrations/2026-09-03-drop-legacy-component-keys.ts +62 -0
  45. package/src/editor/core/themes/migrations/index.ts +2 -0
  46. package/src/editor/docs/content/creating-components.md +13 -0
  47. package/src/editor/docs/content/themes-workflow.md +2 -2
  48. package/src/editor/docs/content.generated.ts +2 -2
  49. package/src/editor/overlay/LiveEditorOverlay.svelte +519 -28
  50. package/src/editor/skill-atlas/SkillAtlas.svelte +836 -0
  51. package/src/editor/skill-atlas/SkillAtlas.svelte.d.ts +4 -0
  52. package/src/editor/skill-atlas/SourcePane.svelte +364 -0
  53. package/src/editor/skill-atlas/TreeNodeCard.svelte +206 -0
  54. package/src/editor/skill-atlas/skillSources.generated.ts +45 -0
  55. package/src/editor/skill-atlas/skillSources.ts +1 -0
  56. package/src/editor/skill-atlas/skillTrees.ts +3844 -0
  57. package/src/editor/skill-atlas/types.ts +65 -0
  58. package/src/live-tokens/data/colors-and-type/autumn.json +1 -37
  59. package/src/live-tokens/data/colors-and-type/default.json +1 -37
  60. package/src/live-tokens/data/colors-and-type/halloween.json +1 -37
  61. package/src/live-tokens/data/colors-and-type/midnight-study.json +1 -37
  62. package/src/live-tokens/data/colors-and-type/ocean.json +1 -37
  63. package/src/live-tokens/data/colors-and-type/royal-velvet.json +1 -37
  64. package/src/live-tokens/data/colors-and-type/sketchy.json +1 -37
  65. package/src/live-tokens/data/colors-and-type/spring-meadow.json +1 -37
  66. package/src/live-tokens/data/colors-and-type/sunset.json +1 -37
  67. package/src/live-tokens/data/themes/autumn.json +1 -37
  68. package/src/live-tokens/data/themes/halloween.json +1 -37
  69. package/src/live-tokens/data/themes/midnight-study.json +1 -37
  70. package/src/live-tokens/data/themes/ocean.json +1 -37
  71. package/src/live-tokens/data/themes/royal-velvet.json +1 -37
  72. package/src/live-tokens/data/themes/sketchy.json +1 -37
  73. package/src/live-tokens/data/themes/spring-meadow.json +1 -37
  74. package/src/live-tokens/data/themes/sunset.json +1 -37
  75. package/src/live-tokens/data/tokens.generated.css +0 -36
  76. package/.claude/skills/live-tokens-generate-theme/SKILL.md +0 -156
  77. package/.claude/skills/live-tokens-generate-theme/references/mood-vocabulary.md +0 -43
  78. package/.claude/skills/live-tokens-generate-theme/references/named-themes.md +0 -18
  79. package/.claude/skills/live-tokens-generate-theme/references/style-vocabulary.md +0 -35
  80. package/bin/generate-theme.mjs +0 -260
  81. /package/dist-plugin/{fontPairing → setType}/index.js +0 -0
@@ -105,10 +105,28 @@
105
105
  });
106
106
 
107
107
  type Mode = 'docked' | 'floating';
108
+ type Side = 'left' | 'right';
108
109
 
109
110
  const STORAGE_KEY = storageKey('overlay-state');
110
111
  const MIN_WIDTH = 360;
111
112
  const MIN_HEIGHT = 480;
113
+
114
+ // Collapsed-pill size; slight overshoot is fine (overflow:hidden).
115
+ const COLLAPSED_WIDTH = 252;
116
+ const COLLAPSED_HEIGHT = 44;
117
+ const COLLAPSED_DEFAULT = { right: 12, top: 12 };
118
+
119
+ // Drop the pill this close to an edge and it docks there.
120
+ const DOCK_EDGES = ['left', 'right', 'top', 'bottom'] as const;
121
+ const DOCK_SNAP = 20;
122
+ // Leaving a dock is resisted across twice that distance, so the edge holds
123
+ // unless the drag is deliberate.
124
+ const DOCK_PULL = DOCK_SNAP * 2;
125
+ // A press that travels this far is a drag, and must not also fire a click.
126
+ const DRAG_SLOP = 16;
127
+ // Mirror the pill motion timings in CSS below.
128
+ const SNAP_DUR = 200;
129
+ const SETTLE_DUR = 300;
112
130
  const DEFAULT_DOCKED_WIDTH = Math.min(960, Math.floor(window.innerWidth * 0.55));
113
131
  const DEFAULT_FLOATING = {
114
132
  x: Math.max(16, window.innerWidth - 960 - 32),
@@ -117,10 +135,44 @@
117
135
  height: Math.min(880, window.innerHeight - 96),
118
136
  };
119
137
 
138
+ // The collapsed pill is placed from the right and top edges whichever side it
139
+ // docks to, so that every dock is a value change on one of those two axes and
140
+ // stays animatable. A left dock is right: <the far end>, not left: 0.
141
+ type DockEdge = 'none' | (typeof DOCK_EDGES)[number];
142
+
143
+ interface CollapsedState {
144
+ right: number;
145
+ top: number;
146
+ edge: DockEdge;
147
+ }
148
+
149
+ function parseEdge(value: unknown): DockEdge {
150
+ return DOCK_EDGES.find((e) => e === value) ?? 'none';
151
+ }
152
+
153
+ // A fixed element's offsets resolve against the viewport without its
154
+ // scrollbars, which is what documentElement.clientWidth/Height report.
155
+ // window.innerWidth counts the scrollbar, and pinning a dock with it hangs
156
+ // the pill off the edge by the scrollbar's width.
157
+ let viewport = $state({
158
+ width: document.documentElement.clientWidth,
159
+ height: document.documentElement.clientHeight,
160
+ });
161
+
162
+ function maxRight() {
163
+ return Math.max(0, viewport.width - COLLAPSED_WIDTH);
164
+ }
165
+
166
+ function maxTop() {
167
+ return Math.max(0, viewport.height - COLLAPSED_HEIGHT);
168
+ }
169
+
120
170
  interface OverlayState {
121
171
  mode: Mode;
172
+ dockSide: Side;
122
173
  dockedWidth: number;
123
174
  floating: { x: number; y: number; width: number; height: number };
175
+ collapsed: CollapsedState;
124
176
  }
125
177
 
126
178
  function loadState(): OverlayState {
@@ -128,6 +180,7 @@
128
180
  if (parsed && typeof parsed === 'object') {
129
181
  return {
130
182
  mode: parsed.mode === 'floating' ? 'floating' : 'docked',
183
+ dockSide: parsed.dockSide === 'left' ? 'left' : 'right',
131
184
  dockedWidth: typeof parsed.dockedWidth === 'number' ? parsed.dockedWidth : DEFAULT_DOCKED_WIDTH,
132
185
  floating: {
133
186
  x: parsed.floating?.x ?? DEFAULT_FLOATING.x,
@@ -135,27 +188,41 @@
135
188
  width: parsed.floating?.width ?? DEFAULT_FLOATING.width,
136
189
  height: parsed.floating?.height ?? DEFAULT_FLOATING.height,
137
190
  },
191
+ collapsed: {
192
+ right: parsed.collapsed?.right ?? COLLAPSED_DEFAULT.right,
193
+ top: parsed.collapsed?.top ?? COLLAPSED_DEFAULT.top,
194
+ edge: parseEdge(parsed.collapsed?.edge),
195
+ },
138
196
  };
139
197
  }
140
198
  return {
141
199
  mode: 'docked',
200
+ dockSide: 'right',
142
201
  dockedWidth: DEFAULT_DOCKED_WIDTH,
143
202
  floating: { ...DEFAULT_FLOATING },
203
+ collapsed: { ...COLLAPSED_DEFAULT, edge: 'none' },
144
204
  };
145
205
  }
146
206
 
147
207
  function persist() {
148
- quietSet(STORAGE_KEY, JSON.stringify({ mode, dockedWidth, floating }));
208
+ quietSet(STORAGE_KEY, JSON.stringify({ mode, dockSide, dockedWidth, floating, collapsed }));
149
209
  }
150
210
 
151
211
  const initial = loadState();
152
212
  let mode: Mode = $state(initial.mode);
213
+ let dockSide: Side = $state(initial.dockSide);
153
214
  let dockedWidth: number = $state(Math.max(MIN_WIDTH, initial.dockedWidth));
154
215
  let floating = $state({ ...initial.floating });
216
+ let collapsed: CollapsedState = $state({ ...initial.collapsed });
155
217
 
156
- // Collapsed-pill size; slight overshoot is fine (overflow:hidden).
157
- const COLLAPSED_WIDTH = 232;
158
- const COLLAPSED_HEIGHT = 44;
218
+ // A docked pill tucks off the edge; its tab pulls it back out.
219
+ let dockExpanded = $state(false);
220
+ // Set once a press passes DRAG_SLOP, and consumed by the click it precedes.
221
+ let dragged = false;
222
+ let snapping = $state(false);
223
+ let settling = $state(false);
224
+ let snapTimer: ReturnType<typeof setTimeout> | undefined;
225
+ let settleTimer: ReturnType<typeof setTimeout> | undefined;
159
226
 
160
227
  // Fade for open-only buttons (bar timing lives in CSS vars below).
161
228
  const BTN_FADE = { duration: 130, easing: cubicInOut };
@@ -184,6 +251,8 @@
184
251
  if (open) {
185
252
  closing = false;
186
253
  opening = true;
254
+ dockExpanded = false;
255
+ if (collapsed.edge === 'left' || collapsed.edge === 'right') dockSide = collapsed.edge;
187
256
  if (gesturing || suppressTransition) {
188
257
  // .no-transition path: no transitionend will fire, so reveal next frame.
189
258
  requestAnimationFrame(() => { opening = false; });
@@ -210,7 +279,7 @@
210
279
  }
211
280
 
212
281
  // Scrim catches pointer events during gestures so they hit the panel, not the iframe.
213
- let gesturing: 'drag' | 'resize-left' | 'resize-se' | null = $state(null);
282
+ let gesturing: 'drag' | 'resize-dock' | 'resize-se' | 'collapsed-drag' | null = $state(null);
214
283
 
215
284
  function startDrag(e: PointerEvent) {
216
285
  if (!open || mode !== 'floating') return;
@@ -238,15 +307,140 @@
238
307
  window.addEventListener('pointerup', up);
239
308
  }
240
309
 
310
+ // The whole collapsed pill is the drag surface, so a press anywhere on it can
311
+ // become a drag; the buttons under it stay clickable via dragSafe().
312
+ function startCollapsedDrag(e: PointerEvent) {
313
+ const surface = e.currentTarget as HTMLElement;
314
+ const pointerId = e.pointerId;
315
+ const orig = { ...collapsed };
316
+ // The pointer only moves the pill once it has travelled DRAG_SLOP, and it
317
+ // moves from that point rather than from the press, so a click on a button
318
+ // in the pill neither nudges it nor gets retargeted by pointer capture.
319
+ let anchorX = e.clientX;
320
+ let anchorY = e.clientY;
321
+
322
+ function move(ev: PointerEvent) {
323
+ if (!dragged) {
324
+ if (Math.hypot(ev.clientX - anchorX, ev.clientY - anchorY) <= DRAG_SLOP) return;
325
+ dragged = true;
326
+ gesturing = 'collapsed-drag';
327
+ anchorX = ev.clientX;
328
+ anchorY = ev.clientY;
329
+ surface.setPointerCapture(pointerId);
330
+ }
331
+ const dx = ev.clientX - anchorX;
332
+ const dy = ev.clientY - anchorY;
333
+
334
+ let right = clamp(orig.right - dx, 0, maxRight());
335
+ let top = clamp(orig.top + dy, 0, maxTop());
336
+
337
+ // Sticky edge: while docked, blend the pill back toward the edge it is on,
338
+ // so pulling off takes a deliberate move rather than a twitch.
339
+ if (collapsed.edge === 'right') right = resist(right, 0);
340
+ else if (collapsed.edge === 'left') right = resist(right, maxRight());
341
+ else if (collapsed.edge === 'top') top = resist(top, 0);
342
+ else if (collapsed.edge === 'bottom') top = resist(top, maxTop());
343
+
344
+ const edge = nearestEdge(right, top);
345
+ if (edge === 'right') right = 0;
346
+ else if (edge === 'left') right = maxRight();
347
+ else if (edge === 'top') top = 0;
348
+ else if (edge === 'bottom') top = maxTop();
349
+
350
+ if (edge !== 'none' && edge !== collapsed.edge) {
351
+ // Animate the last pixels to the edge instead of jumping them.
352
+ dockExpanded = false;
353
+ snapping = true;
354
+ clearTimeout(snapTimer);
355
+ snapTimer = setTimeout(() => { snapping = false; }, SNAP_DUR);
356
+ }
357
+ collapsed = { right, top, edge };
358
+ }
359
+
360
+ function up() {
361
+ window.removeEventListener('pointermove', move);
362
+ window.removeEventListener('pointerup', up);
363
+ if (!dragged) return;
364
+ gesturing = null;
365
+ snapping = false;
366
+ clearTimeout(snapTimer);
367
+ settling = true;
368
+ clearTimeout(settleTimer);
369
+ settleTimer = setTimeout(() => { settling = false; }, SETTLE_DUR + 40);
370
+ persist();
371
+ }
372
+
373
+ window.addEventListener('pointermove', move);
374
+ window.addEventListener('pointerup', up);
375
+ }
376
+
377
+ function onHeaderPointerDown(e: PointerEvent) {
378
+ dragged = false;
379
+ if (open) startDrag(e);
380
+ else startCollapsedDrag(e);
381
+ }
382
+
383
+ // Mirrors applySnapResistance in the source toolbar: the closer the pill is to
384
+ // the edge it is docked on, the harder it pulls back to it.
385
+ function resist(value: number, anchor: number) {
386
+ const delta = Math.abs(value - anchor);
387
+ if (delta >= DOCK_PULL) return value;
388
+ const factor = 1 - delta / DOCK_PULL;
389
+ return value * (1 - factor) + anchor * factor;
390
+ }
391
+
392
+ function nearestEdge(right: number, top: number): DockEdge {
393
+ const distances = [
394
+ { edge: 'right' as const, distance: right },
395
+ { edge: 'left' as const, distance: maxRight() - right },
396
+ { edge: 'top' as const, distance: top },
397
+ { edge: 'bottom' as const, distance: maxTop() - top },
398
+ ];
399
+ const nearest = distances.reduce((a, b) => (b.distance < a.distance ? b : a));
400
+ return nearest.distance < DOCK_SNAP ? nearest.edge : 'none';
401
+ }
402
+
403
+ // A press that turned into a drag must not also fire the button it landed on.
404
+ function dragSafe(fn: () => void) {
405
+ return () => {
406
+ if (dragged) return;
407
+ fn();
408
+ };
409
+ }
410
+
411
+ function toggleDock() {
412
+ if (collapsed.edge === 'none') return;
413
+ dockExpanded = !dockExpanded;
414
+ }
415
+
416
+ function onViewportResize() {
417
+ viewport = {
418
+ width: document.documentElement.clientWidth,
419
+ height: document.documentElement.clientHeight,
420
+ };
421
+ clampCollapsed();
422
+ }
423
+
424
+ function clampCollapsed() {
425
+ const { edge } = collapsed;
426
+ collapsed = {
427
+ edge,
428
+ right: edge === 'left' ? maxRight() : edge === 'right' ? 0 : clamp(collapsed.right, 0, maxRight()),
429
+ top: edge === 'bottom' ? maxTop() : edge === 'top' ? 0 : clamp(collapsed.top, 0, maxTop()),
430
+ };
431
+ }
432
+
241
433
  function startDockedResize(e: PointerEvent) {
242
434
  if (!open || mode !== 'docked') return;
243
- gesturing = 'resize-left';
435
+ gesturing = 'resize-dock';
244
436
  (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
245
437
  const startX = e.clientX;
246
438
  const origWidth = dockedWidth;
247
439
  function move(ev: PointerEvent) {
248
- // Panel anchored right — drag left grows width.
249
- dockedWidth = clamp(origWidth + (startX - ev.clientX), MIN_WIDTH, window.innerWidth - 120);
440
+ // The panel is pinned to its docked side, so dragging away from that side
441
+ // is what grows it.
442
+ const grow = dockSide === 'left' ? ev.clientX - startX : startX - ev.clientX;
443
+ dockedWidth = clamp(origWidth + grow, MIN_WIDTH, viewport.width - 120);
250
444
  }
251
445
  function up() {
252
446
  gesturing = null;
@@ -307,9 +501,16 @@
307
501
  open = !open;
308
502
  }
309
503
 
504
+ function flipDockSide() {
505
+ dockSide = dockSide === 'right' ? 'left' : 'right';
506
+ persist();
507
+ }
508
+
310
509
  function handleHeaderDblClick(e: MouseEvent) {
311
- // Skip dblclick on buttons so their handlers don't double-fire.
510
+ // Skip dblclick on buttons so their handlers don't double-fire, and on the
511
+ // grip so a quick reposition doesn't open the editor.
312
512
  if ((e.target as HTMLElement).closest('button')) return;
513
+ if (dragged) return;
313
514
  toggleOpen();
314
515
  }
315
516
 
@@ -319,52 +520,100 @@
319
520
 
320
521
  onMount(() => {
321
522
  window.addEventListener('lt-overlay-toggle', handleToggleRequest);
523
+ window.addEventListener('resize', onViewportResize);
524
+ onViewportResize();
322
525
  });
323
526
  onDestroy(() => {
324
527
  window.removeEventListener('lt-overlay-toggle', handleToggleRequest);
528
+ window.removeEventListener('resize', onViewportResize);
325
529
  clearTimeout(maskTimer);
530
+ clearTimeout(snapTimer);
531
+ clearTimeout(settleTimer);
326
532
  });
327
533
 
534
+ // A docked pill tucks off its edge the moment it snaps, mid-drag included, so
535
+ // that reaching an edge gets it out of the way instead of parking it against
536
+ // the side. It waits out the collapse animation, so the shrink and the tuck
537
+ // read as one move.
538
+ let tucked = $derived(!open && !closing && !dockExpanded && collapsed.edge !== 'none');
539
+ let dockClass = $derived(!open && collapsed.edge !== 'none' ? `dock-${collapsed.edge}` : '');
540
+ let tabIsHorizontal = $derived(collapsed.edge === 'top' || collapsed.edge === 'bottom');
541
+
328
542
  let panelStyle = $derived(!open
329
- ? `position: fixed; top: 12px; right: 12px; width: ${COLLAPSED_WIDTH}px; height: ${COLLAPSED_HEIGHT}px;`
543
+ ? `position: fixed; top: ${collapsed.top}px; right: ${collapsed.right}px; width: ${COLLAPSED_WIDTH}px; height: ${COLLAPSED_HEIGHT}px;`
330
544
  : mode === 'docked'
331
- ? `position: fixed; top: 0; right: 0; width: ${dockedWidth}px; height: 100vh;`
545
+ // Both sides are pinned with `right`, so flipping side and collapsing to
546
+ // the pill are value changes on one axis, and animate.
547
+ ? `position: fixed; top: 0; right: ${dockSide === 'left' ? Math.max(0, viewport.width - dockedWidth) : 0}px; width: ${dockedWidth}px; height: 100vh;`
332
548
  : `position: fixed; top: ${floating.y}px; left: ${floating.x}px; width: ${floating.width}px; height: ${floating.height}px;`);
333
549
  </script>
334
550
 
335
551
  {#if enabled && !onEditorPath}
336
552
  <!-- svelte-ignore a11y_no_static_element_interactions -->
337
553
  <div
338
- class="lt-overlay"
554
+ class="lt-overlay {dockClass}"
339
555
  style={panelStyle}
340
556
  class:shown={open}
341
557
  class:hidden={!open}
342
558
  class:docked={open && mode === 'docked'}
559
+ class:side-left={open && mode === 'docked' && dockSide === 'left'}
343
560
  class:floating={open && mode === 'floating'}
561
+ class:tucked
562
+ class:dragging={gesturing === 'collapsed-drag'}
563
+ class:snapping
564
+ class:settling
565
+ class:at-edge={gesturing === 'collapsed-drag' && collapsed.edge !== 'none'}
344
566
  class:opening={open && opening}
345
567
  class:closing={!open && closing}
346
- class:no-transition={!!gesturing || suppressTransition}
568
+ class:no-transition={(!!gesturing && gesturing !== 'collapsed-drag') || suppressTransition}
347
569
  ontransitionend={onPanelTransitionEnd}
348
570
  >
349
571
  <div
350
572
  class="header"
351
- onpointerdown={startDrag}
573
+ onpointerdown={onHeaderPointerDown}
352
574
  ondblclick={handleHeaderDblClick}
353
- title={open ? 'Double-click to hide' : 'Double-click to show'}
575
+ title={open ? 'Double-click to collapse' : 'Double-click to expand'}
354
576
  >
577
+ {#if !open && collapsed.edge !== 'none'}
578
+ <button
579
+ class="tab"
580
+ inert={!tucked}
581
+ onclick={dragSafe(toggleDock)}
582
+ title="Show the editor bar. Drag to undock."
583
+ >
584
+ <i class="fas {tabIsHorizontal ? 'fa-grip' : 'fa-grip-vertical'}"></i>
585
+ </button>
586
+ {/if}
587
+
588
+ {#if !open}
589
+ <button
590
+ class="grip"
591
+ inert={tucked}
592
+ onclick={dragSafe(toggleDock)}
593
+ title={collapsed.edge === 'none'
594
+ ? 'Drag to move. Drop at an edge to dock.'
595
+ : 'Tuck to the edge'}
596
+ transition:fade={BTN_FADE}
597
+ >
598
+ <i class="fas fa-grip-vertical"></i>
599
+ </button>
600
+ {/if}
601
+
355
602
  <button
356
603
  class="hdr-btn text title"
357
- onclick={toggleOpen}
358
- title={open ? 'Hide Editor' : 'Show Editor'}
604
+ inert={tucked}
605
+ onclick={dragSafe(toggleOpen)}
606
+ title={open ? 'Collapse Editor' : 'Expand Editor'}
359
607
  >
360
- <i class="fas {open ? 'fa-chevron-right' : 'fa-chevron-left'}"></i>
608
+ <i class="fas {open ? 'fa-compress' : 'fa-expand'}"></i>
361
609
  <span>Editor</span>
362
610
  </button>
363
611
 
364
612
  <button
365
613
  class="hdr-btn icon"
366
614
  class:active={$columnsVisible}
367
- onclick={toggleColumns}
615
+ inert={tucked}
616
+ onclick={dragSafe(toggleColumns)}
368
617
  title="{$columnsVisible ? 'Hide' : 'Show'} columns"
369
618
  >
370
619
  <i class="fas fa-grip-lines-vertical"></i>
@@ -373,7 +622,7 @@
373
622
  {#if open}
374
623
  <button
375
624
  class="hdr-btn icon"
376
- title={mode === 'docked' ? 'Float' : 'Dock to right'}
625
+ title={mode === 'docked' ? 'Float' : `Dock to the ${dockSide}`}
377
626
  onclick={toggleMode}
378
627
  transition:fade={BTN_FADE}
379
628
  >
@@ -381,6 +630,17 @@
381
630
  </button>
382
631
  {/if}
383
632
 
633
+ {#if open && mode === 'docked'}
634
+ <button
635
+ class="hdr-btn icon"
636
+ title="Dock to the {dockSide === 'right' ? 'left' : 'right'}"
637
+ onclick={flipDockSide}
638
+ transition:fade={BTN_FADE}
639
+ >
640
+ <i class="fas {dockSide === 'right' ? 'fa-angles-left' : 'fa-angles-right'}"></i>
641
+ </button>
642
+ {/if}
643
+
384
644
  {#if APP_VERSION}
385
645
  <span class="version" title="live-tokens version">v{APP_VERSION}</span>
386
646
  {/if}
@@ -437,7 +697,10 @@
437
697
  </div>
438
698
 
439
699
  {#if mode === 'docked'}
440
- <div class="resize-left" onpointerdown={startDockedResize}></div>
700
+ <div
701
+ class="resize-edge {dockSide === 'left' ? 'at-right' : 'at-left'}"
702
+ onpointerdown={startDockedResize}
703
+ ></div>
441
704
  {:else}
442
705
  <div class="resize-se" onpointerdown={startFloatingResize}></div>
443
706
  {/if}
@@ -454,6 +717,17 @@
454
717
  --bar-close-dur: 240ms;
455
718
  --bar-close-ease: cubic-bezier(0, 0, 0.2, 1); /* ease-out */
456
719
  --bar-close-delay: 120ms; /* let the iframe fade out before the panel shrinks */
720
+ /* Collapsed-pill motion: snap = the last pixels to the edge, settle = the
721
+ drop, tuck = sliding off the edge and back. Mirrored by SNAP_DUR and
722
+ SETTLE_DUR in the script. */
723
+ --snap-dur: 200ms;
724
+ --snap-ease: cubic-bezier(0.755, 0.05, 0.855, 0.06);
725
+ --settle-dur: 300ms;
726
+ --settle-ease: cubic-bezier(0.22, 1, 0.36, 1);
727
+ --tuck-dur: 220ms;
728
+ --tuck-ease: cubic-bezier(0.22, 1, 0.36, 1);
729
+ --tab-width: 20px; /* the sliver of pill that stays on screen when docked */
730
+ --tab-length: 56px; /* the handle's reach along a horizontal edge */
457
731
 
458
732
  display: flex;
459
733
  flex-direction: column;
@@ -469,7 +743,8 @@
469
743
  height var(--bar-open-dur) var(--bar-open-ease) var(--bar-open-delay),
470
744
  top var(--bar-open-dur) var(--bar-open-ease) var(--bar-open-delay),
471
745
  right var(--bar-open-dur) var(--bar-open-ease) var(--bar-open-delay),
472
- border-radius var(--bar-open-dur) var(--bar-open-ease) var(--bar-open-delay);
746
+ border-radius var(--bar-open-dur) var(--bar-open-ease) var(--bar-open-delay),
747
+ transform var(--tuck-dur) var(--tuck-ease);
473
748
  }
474
749
 
475
750
  /* Sketch mode scopes itself to the host document root, and this bar lives
@@ -492,11 +767,17 @@
492
767
  border-radius: 0;
493
768
  }
494
769
 
770
+ .lt-overlay.docked.side-left {
771
+ border-right: 1px solid rgba(255, 255, 255, 0.12);
772
+ border-left: none;
773
+ }
774
+
495
775
  .lt-overlay.floating {
496
776
  border-radius: var(--ui-radius-xl, 8px);
497
777
  }
498
778
 
499
- /* Collapsed state: pinned top-right; iframe stays mounted, clipped by overflow:hidden. */
779
+ /* Collapsed state: draggable pill, placed from the right edge; iframe stays
780
+ mounted, clipped by overflow:hidden. */
500
781
  .lt-overlay.hidden {
501
782
  border-radius: var(--ui-radius-lg, 6px);
502
783
  border-color: rgba(255, 255, 255, 0.32);
@@ -505,10 +786,99 @@
505
786
  height var(--bar-close-dur) var(--bar-close-ease) var(--bar-close-delay),
506
787
  top var(--bar-close-dur) var(--bar-close-ease) var(--bar-close-delay),
507
788
  right var(--bar-close-dur) var(--bar-close-ease) var(--bar-close-delay),
508
- border-radius var(--bar-close-dur) var(--bar-close-ease) var(--bar-close-delay);
789
+ border-radius var(--bar-close-dur) var(--bar-close-ease) var(--bar-close-delay),
790
+ transform var(--tuck-dur) var(--tuck-ease),
791
+ background-color var(--tuck-dur) var(--tuck-ease),
792
+ border-color var(--tuck-dur) var(--tuck-ease),
793
+ box-shadow var(--tuck-dur) ease;
794
+ }
795
+
796
+ /* Docked: slide all but a tab's width off the edge, whichever edge it is. */
797
+ .lt-overlay.tucked.dock-right {
798
+ transform: translateX(calc(100% - var(--tab-width)));
799
+ }
800
+
801
+ .lt-overlay.tucked.dock-left {
802
+ transform: translateX(calc(-100% + var(--tab-width)));
803
+ }
804
+
805
+ .lt-overlay.tucked.dock-top {
806
+ transform: translateY(calc(-100% + var(--tab-width)));
807
+ }
808
+
809
+ .lt-overlay.tucked.dock-bottom {
810
+ transform: translateY(calc(100% - var(--tab-width)));
811
+ }
812
+
813
+ /* Flatten the corners the pill is resting against. */
814
+ .lt-overlay.hidden.dock-right {
815
+ border-top-right-radius: 0;
816
+ border-bottom-right-radius: 0;
509
817
  }
510
818
 
511
- .lt-overlay.hidden .resize-left,
819
+ .lt-overlay.hidden.dock-left {
820
+ border-top-left-radius: 0;
821
+ border-bottom-left-radius: 0;
822
+ }
823
+
824
+ .lt-overlay.hidden.dock-top {
825
+ border-top-left-radius: 0;
826
+ border-top-right-radius: 0;
827
+ }
828
+
829
+ .lt-overlay.hidden.dock-bottom {
830
+ border-bottom-left-radius: 0;
831
+ border-bottom-right-radius: 0;
832
+ }
833
+
834
+ /* Following the pointer, so position does not animate. The tuck still does,
835
+ so pulling a docked pill off the edge unfurls it. */
836
+ .lt-overlay.dragging {
837
+ transition: transform var(--tuck-dur) var(--tuck-ease);
838
+ }
839
+
840
+ /* The last pixels to the edge are a jump, and worth animating. */
841
+ .lt-overlay.dragging.snapping {
842
+ transition:
843
+ right var(--snap-dur) var(--snap-ease),
844
+ top var(--snap-dur) var(--snap-ease),
845
+ transform var(--tuck-dur) var(--tuck-ease);
846
+ }
847
+
848
+ /* Released: ease into the resting spot rather than stopping dead. */
849
+ .lt-overlay.settling {
850
+ transition:
851
+ top var(--settle-dur) var(--settle-ease),
852
+ right var(--settle-dur) var(--settle-ease),
853
+ transform var(--tuck-dur) var(--tuck-ease),
854
+ box-shadow var(--tuck-dur) ease;
855
+ }
856
+
857
+ /* Docked to a horizontal edge, the pill hides its depth rather than its
858
+ length, which would leave its whole width parked on the page. Its chrome
859
+ gives way to the tab, so what stays on screen is a handle either way. */
860
+ .lt-overlay.tucked.dock-top,
861
+ .lt-overlay.tucked.dock-bottom,
862
+ .lt-overlay.tucked.dock-top .header,
863
+ .lt-overlay.tucked.dock-bottom .header {
864
+ background: transparent;
865
+ border-color: transparent;
866
+ box-shadow: none;
867
+ }
868
+
869
+ .lt-overlay.tucked.dock-top .frame-wrap,
870
+ .lt-overlay.tucked.dock-bottom .frame-wrap {
871
+ opacity: 0;
872
+ }
873
+
874
+ /* The edge has the pill: say so while it is still under the pointer. */
875
+ .lt-overlay.at-edge {
876
+ box-shadow:
877
+ 0 0 0 2px rgba(255, 255, 255, 0.45),
878
+ 0 18px 60px rgba(0, 0, 0, 0.6);
879
+ }
880
+
881
+ .lt-overlay.hidden .resize-edge,
512
882
  .lt-overlay.hidden .resize-se {
513
883
  display: none;
514
884
  }
@@ -519,6 +889,7 @@
519
889
  }
520
890
 
521
891
  .header {
892
+ position: relative;
522
893
  display: flex;
523
894
  align-items: center;
524
895
  gap: var(--ui-space-6, 6px);
@@ -530,15 +901,127 @@
530
901
  user-select: none;
531
902
  }
532
903
 
904
+ /* The whole collapsed pill is the drag surface. */
533
905
  .lt-overlay.hidden .header {
534
906
  border-bottom: none;
535
907
  padding: 5px var(--ui-space-8, 8px);
908
+ cursor: grab;
909
+ transition: background-color var(--tuck-dur) var(--tuck-ease);
910
+ }
911
+
912
+ .grip {
913
+ display: inline-flex;
914
+ align-self: stretch;
915
+ align-items: center;
916
+ justify-content: center;
917
+ width: var(--tab-width);
918
+ /* Cancel the header's left padding so the grip is the pill's left edge,
919
+ which is what stays on screen when docked. */
920
+ margin-left: calc(-1 * var(--ui-space-8, 8px));
921
+ padding: 0;
922
+ background: transparent;
923
+ border: 0;
924
+ color: rgba(255, 255, 255, 0.45);
925
+ font-size: var(--ui-font-size-sm, 12px);
926
+ cursor: grab;
927
+ touch-action: none;
928
+ transition: background var(--ui-transition-fast, 120ms ease), color var(--ui-transition-fast, 120ms ease);
929
+ }
930
+
931
+ .grip:hover {
932
+ background: rgba(255, 255, 255, 0.1);
933
+ color: rgba(255, 255, 255, 0.9);
934
+ }
935
+
936
+ /* The tab hugs the edge of the pill that stays on screen, so it is the whole
937
+ of what a tucked pill shows. It sits over the bar, which fades out under it. */
938
+ .tab {
939
+ position: absolute;
940
+ display: flex;
941
+ align-items: center;
942
+ justify-content: center;
943
+ padding: 0;
944
+ background: rgba(255, 255, 255, 0.08);
945
+ border: 0;
946
+ color: rgba(255, 255, 255, 0.75);
947
+ font-size: var(--ui-font-size-sm, 12px);
948
+ cursor: grab;
949
+ opacity: 0;
950
+ touch-action: none;
951
+ transition: opacity var(--tuck-dur) var(--tuck-ease), background var(--ui-transition-fast, 120ms ease);
952
+ }
953
+
954
+ .tab:hover {
955
+ background: rgba(255, 255, 255, 0.14);
956
+ color: var(--ui-text-primary, #fff);
957
+ }
958
+
959
+ .dock-right .tab {
960
+ top: 0;
961
+ bottom: 0;
962
+ left: 0;
963
+ width: var(--tab-width);
964
+ }
965
+
966
+ .dock-left .tab {
967
+ top: 0;
968
+ bottom: 0;
969
+ right: 0;
970
+ width: var(--tab-width);
971
+ }
972
+
973
+ /* On a horizontal edge the tab is the whole of the visible chrome, so it
974
+ carries the pill's surface and border and reaches only --tab-length. */
975
+ .dock-top .tab,
976
+ .dock-bottom .tab {
977
+ left: 50%;
978
+ width: var(--tab-length);
979
+ height: var(--tab-width);
980
+ margin-left: calc(var(--tab-length) / -2);
981
+ background: var(--ui-surface-low, #111);
982
+ border: 1px solid rgba(255, 255, 255, 0.32);
983
+ }
984
+
985
+ .dock-top .tab {
986
+ bottom: 0;
987
+ border-top: 0;
988
+ border-radius: 0 0 var(--ui-radius-lg, 6px) var(--ui-radius-lg, 6px);
989
+ }
990
+
991
+ .dock-bottom .tab {
992
+ top: 0;
993
+ border-bottom: 0;
994
+ border-radius: var(--ui-radius-lg, 6px) var(--ui-radius-lg, 6px) 0 0;
995
+ }
996
+
997
+ .dock-top .tab:hover,
998
+ .dock-bottom .tab:hover {
999
+ background: var(--ui-surface-lower, #0a0a0a);
1000
+ }
1001
+
1002
+ .lt-overlay.tucked .tab {
1003
+ opacity: 1;
1004
+ }
1005
+
1006
+ /* Tucked, the bar itself is off-screen; fade it so nothing of it shows
1007
+ through the tab on the way out. */
1008
+ .lt-overlay.tucked .hdr-btn,
1009
+ .lt-overlay.tucked .grip,
1010
+ .lt-overlay.tucked .version {
1011
+ opacity: 0;
1012
+ transition: opacity var(--tuck-dur) var(--tuck-ease);
536
1013
  }
537
1014
 
538
1015
  .lt-overlay.floating .header {
539
1016
  cursor: move;
540
1017
  }
541
1018
 
1019
+ .lt-overlay.dragging .header,
1020
+ .lt-overlay.dragging .grip,
1021
+ .lt-overlay.dragging .tab {
1022
+ cursor: grabbing;
1023
+ }
1024
+
542
1025
  .hdr-btn.title {
543
1026
  gap: 7px;
544
1027
  font-size: var(--ui-font-size-md, 16px);
@@ -716,17 +1199,25 @@
716
1199
  cursor: inherit;
717
1200
  }
718
1201
 
719
- .resize-left {
1202
+ /* Sits on the panel's inner edge, whichever side it is docked to. */
1203
+ .resize-edge {
720
1204
  position: absolute;
721
1205
  top: 0;
722
- left: 0;
723
1206
  bottom: 0;
724
1207
  width: 6px;
725
1208
  cursor: ew-resize;
726
1209
  background: transparent;
727
1210
  }
728
1211
 
729
- .resize-left:hover {
1212
+ .resize-edge.at-left {
1213
+ left: 0;
1214
+ }
1215
+
1216
+ .resize-edge.at-right {
1217
+ right: 0;
1218
+ }
1219
+
1220
+ .resize-edge:hover {
730
1221
  background: rgba(255, 255, 255, 0.08);
731
1222
  }
732
1223