@motion-proto/live-tokens 0.8.0 → 0.10.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 (61) hide show
  1. package/.claude/skills/live-tokens-add-component/SKILL.md +488 -0
  2. package/README.md +84 -29
  3. package/dist-plugin/index.cjs +177 -125
  4. package/dist-plugin/index.d.cts +3 -2
  5. package/dist-plugin/index.d.ts +3 -2
  6. package/dist-plugin/index.js +177 -125
  7. package/package.json +8 -2
  8. package/src/editor/component-editor/BadgeEditor.svelte +44 -42
  9. package/src/editor/component-editor/ButtonEditor.svelte +224 -0
  10. package/src/editor/component-editor/CollapsibleSectionEditor.svelte +1 -7
  11. package/src/editor/component-editor/CornerBadgeEditor.svelte +44 -34
  12. package/src/editor/component-editor/ImageLightboxEditor.svelte +58 -0
  13. package/src/editor/component-editor/InputEditor.svelte +272 -0
  14. package/src/editor/component-editor/NotificationEditor.svelte +44 -65
  15. package/src/editor/component-editor/ProgressBarEditor.svelte +71 -87
  16. package/src/editor/component-editor/SegmentedControlEditor.svelte +98 -37
  17. package/src/editor/component-editor/SideNavigationEditor.svelte +342 -0
  18. package/src/editor/component-editor/index.ts +16 -1
  19. package/src/editor/component-editor/registry.ts +138 -28
  20. package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +3 -2
  21. package/src/editor/component-editor/scaffolding/ComponentsTab.svelte +2 -2
  22. package/src/editor/component-editor/scaffolding/StateBlock.svelte +9 -10
  23. package/src/editor/component-editor/scaffolding/TokenLayout.svelte +60 -36
  24. package/src/editor/component-editor/scaffolding/VariantGroup.svelte +38 -1
  25. package/src/editor/component-editor/scaffolding/buildTypeGroupTokens.ts +1 -1
  26. package/src/editor/component-editor/scaffolding/componentSources.ts +3 -3
  27. package/src/editor/component-editor/scaffolding/defaultSections.ts +15 -10
  28. package/src/editor/component-editor/scaffolding/siblings.ts +2 -2
  29. package/src/editor/component-editor/scaffolding/types.ts +2 -1
  30. package/src/editor/core/components/componentConfigKeys.ts +14 -3
  31. package/src/editor/core/components/componentConfigService.ts +7 -6
  32. package/src/editor/core/manifests/manifestService.ts +5 -4
  33. package/src/editor/core/storage/apiBase.ts +15 -0
  34. package/src/editor/core/storage/files/versionedFileResourceClient.ts +1 -1
  35. package/src/editor/core/themes/migrations/2026-05-24-collapsiblesection-drop-active-state.ts +28 -0
  36. package/src/editor/core/themes/migrations/2026-05-24-progressbar-collapse-variants.ts +41 -0
  37. package/src/editor/core/themes/migrations/2026-05-24-promote-state-shared-tokens.ts +59 -0
  38. package/src/editor/core/themes/migrations/2026-05-24-segmentedcontrol-divider-inset.ts +29 -0
  39. package/src/editor/core/themes/migrations/2026-05-25-cornerbadge-flatten-variants.ts +46 -0
  40. package/src/editor/core/themes/migrations/index.ts +10 -0
  41. package/src/editor/core/themes/slices/components.ts +9 -0
  42. package/src/editor/core/themes/themeInit.ts +3 -2
  43. package/src/editor/core/themes/themeService.ts +3 -2
  44. package/src/editor/index.ts +10 -1
  45. package/src/editor/pages/ComponentEditorPage.svelte +53 -3
  46. package/src/editor/pages/EditorShell.svelte +53 -3
  47. package/src/editor/ui/UIEasingSelector.svelte +240 -0
  48. package/src/editor/ui/variantScales.ts +34 -0
  49. package/src/system/components/Button.svelte +34 -85
  50. package/src/system/components/CollapsibleSection.svelte +1 -48
  51. package/src/system/components/CornerBadge.svelte +72 -138
  52. package/src/system/components/Dialog.svelte +24 -4
  53. package/src/system/components/ImageLightbox.svelte +578 -0
  54. package/src/system/components/Input.svelte +387 -0
  55. package/src/system/components/ProgressBar.svelte +62 -258
  56. package/src/system/components/SectionDivider.svelte +117 -43
  57. package/src/system/components/SegmentedControl.svelte +81 -15
  58. package/src/system/components/SideNavigation.svelte +777 -0
  59. package/src/system/styles/tokens.css +43 -0
  60. package/src/system/styles/tokens.generated.css +4 -183
  61. package/src/editor/component-editor/StandardButtonsEditor.svelte +0 -190
@@ -2,7 +2,6 @@
2
2
  import { createEventDispatcher, tick } from 'svelte';
3
3
  import type { Snippet } from 'svelte';
4
4
  import Button from './Button.svelte';
5
- import { editorState } from '../../editor/core/store/editorStore';
6
5
  import type { ButtonVariant, DialogButtonSpec } from './types';
7
6
 
8
7
  const BUTTON_VARIANTS: readonly ButtonVariant[] = ['primary', 'secondary', 'outline', 'success', 'danger', 'warning'];
@@ -10,6 +9,18 @@
10
9
  return v && (BUTTON_VARIANTS as readonly string[]).includes(v) ? (v as ButtonVariant) : fallback;
11
10
  }
12
11
 
12
+ // Read the configured Button variants from :root. The editor mutates these
13
+ // inline on documentElement via cssVarSync; a MutationObserver picks the
14
+ // changes up without coupling this component to the editor module graph.
15
+ // In production the var lives in the generated stylesheet and never
16
+ // changes, so the observer registers but never fires.
17
+ function readCssVar(name: string): string {
18
+ if (typeof document === 'undefined') return '';
19
+ return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
20
+ }
21
+ let confirmVarValue = $state(readCssVar('--dialog-confirm-variant'));
22
+ let cancelVarValue = $state(readCssVar('--dialog-cancel-variant'));
23
+
13
24
  interface Props {
14
25
  show?: boolean;
15
26
  title?: string;
@@ -39,9 +50,8 @@
39
50
  children,
40
51
  }: Props = $props();
41
52
 
42
- let configuredConfig = $derived($editorState.components.dialog?.config ?? {});
43
- let effectiveConfirmVariant = $derived(confirm?.variant ?? asVariant(configuredConfig['--dialog-confirm-variant'] as string | undefined, 'primary'));
44
- let effectiveCancelVariant = $derived(cancel?.variant ?? asVariant(configuredConfig['--dialog-cancel-variant'] as string | undefined, 'outline'));
53
+ let effectiveConfirmVariant = $derived(confirm?.variant ?? asVariant(confirmVarValue, 'primary'));
54
+ let effectiveCancelVariant = $derived(cancel?.variant ?? asVariant(cancelVarValue, 'outline'));
45
55
 
46
56
  // Dual-fire bridge — see Button.svelte for the deprecation timeline.
47
57
  const dispatch = createEventDispatcher<{
@@ -52,6 +62,16 @@
52
62
  let cancelButtonRef: HTMLButtonElement = $state()!;
53
63
  let closeButtonRef: HTMLButtonElement = $state()!;
54
64
 
65
+ $effect(() => {
66
+ if (typeof document === 'undefined') return;
67
+ const obs = new MutationObserver(() => {
68
+ confirmVarValue = readCssVar('--dialog-confirm-variant');
69
+ cancelVarValue = readCssVar('--dialog-cancel-variant');
70
+ });
71
+ obs.observe(document.documentElement, { attributes: true, attributeFilter: ['style'] });
72
+ return () => obs.disconnect();
73
+ });
74
+
55
75
  // Focus the primary button when dialog opens (skip in inline mode so the editor doesn't steal focus).
56
76
  $effect(() => {
57
77
  if (show && !inline) {
@@ -0,0 +1,578 @@
1
+ <script lang="ts">
2
+ import { onMount, tick } from 'svelte';
3
+
4
+ interface Props {
5
+ src: string;
6
+ alt: string;
7
+ width?: number | undefined;
8
+ height?: number | undefined;
9
+ maxWidth?: number | string | undefined;
10
+ /** When true, shows a bottom toolbar (zoom in/out + percent) and a top-right close button, and enables wheel/drag zoom inside the open modal. When false, click anywhere closes. */
11
+ extended?: boolean;
12
+ }
13
+
14
+ let {
15
+ src,
16
+ alt,
17
+ width = undefined,
18
+ height = undefined,
19
+ maxWidth = undefined,
20
+ extended = false,
21
+ }: Props = $props();
22
+
23
+ const MIN_SCALE = 1;
24
+ const MAX_SCALE = 5;
25
+ const ZOOM_STEP = 1.5;
26
+ const TRANSITION_MS = 350;
27
+ const TRANSITION_EASE = 'cubic-bezier(0.65, 0, 0.35, 1)';
28
+
29
+ let wrapperEl: HTMLDivElement;
30
+ let tileEl: HTMLDivElement;
31
+ let transformEl: HTMLDivElement;
32
+ let overlayEl: HTMLDivElement;
33
+ let toolbarEl: HTMLDivElement | undefined = $state();
34
+ let closeBtnEl: HTMLButtonElement | undefined = $state();
35
+
36
+ let open = $state(false);
37
+ let scale = $state(1);
38
+ let offset = { x: 0, y: 0 };
39
+
40
+ // Pointer drag for pan (extended + zoomed only).
41
+ let dragState: { startX: number; startY: number; baseX: number; baseY: number; pointerId: number } | null = null;
42
+ let didDrag = false;
43
+
44
+ const aspect = $derived(width && height ? width / height : undefined);
45
+
46
+ function viewportTarget() {
47
+ const vw = window.innerWidth;
48
+ const vh = window.innerHeight;
49
+ const capW = vw * 0.94;
50
+ const capH = vh * 0.92;
51
+ if (!aspect) {
52
+ return { top: vh * 0.04, left: vw * 0.03, width: capW, height: capH };
53
+ }
54
+ const tileW = Math.min(capW, capH * aspect);
55
+ const tileH = tileW / aspect;
56
+ return {
57
+ top: (vh - tileH) / 2,
58
+ left: (vw - tileW) / 2,
59
+ width: tileW,
60
+ height: tileH,
61
+ };
62
+ }
63
+
64
+ function clampOffset(x: number, y: number, s: number) {
65
+ if (!tileEl) return { x: 0, y: 0 };
66
+ const r = tileEl.getBoundingClientRect();
67
+ const maxX = Math.max(0, (r.width * s - r.width) / 2);
68
+ const maxY = Math.max(0, (r.height * s - r.height) / 2);
69
+ return {
70
+ x: Math.max(-maxX, Math.min(maxX, x)),
71
+ y: Math.max(-maxY, Math.min(maxY, y)),
72
+ };
73
+ }
74
+
75
+ function applyTransform(s: number, o: { x: number; y: number }) {
76
+ if (!transformEl) return;
77
+ transformEl.style.transform = `translate(${o.x}px, ${o.y}px) scale(${s})`;
78
+ }
79
+
80
+ // The dev editor overlay (`.lt-overlay`) sits at z-index 2000, above the
81
+ // lightbox modal (`--z-modal` = 1100). When the lightbox is open we hide the
82
+ // overlay so its iframe can't intercept clicks on the lightbox close button
83
+ // or backdrop. Restored on close. No-op in production where the overlay
84
+ // isn't mounted.
85
+ let prevOverlayVisibility: string | null = null;
86
+
87
+ function hideEditorOverlay() {
88
+ const overlay = document.querySelector<HTMLElement>('.lt-overlay');
89
+ if (!overlay) return;
90
+ prevOverlayVisibility = overlay.style.visibility;
91
+ overlay.style.visibility = 'hidden';
92
+ }
93
+
94
+ function restoreEditorOverlay() {
95
+ const overlay = document.querySelector<HTMLElement>('.lt-overlay');
96
+ if (!overlay) return;
97
+ overlay.style.visibility = prevOverlayVisibility ?? '';
98
+ prevOverlayVisibility = null;
99
+ }
100
+
101
+ function cancelAnimations() {
102
+ // Commit each animation's current value to inline styles before cancelling,
103
+ // so a mid-flight cancel doesn't visually snap the element back to its
104
+ // pre-animation state. After cancel, the WAAPI effect is gone and the
105
+ // committed inline values are what we'll either animate from next or
106
+ // clear via cssText = ''.
107
+ for (const el of [tileEl, overlayEl, toolbarEl, closeBtnEl]) {
108
+ if (!el) continue;
109
+ for (const a of el.getAnimations()) {
110
+ try { a.commitStyles(); } catch {}
111
+ a.cancel();
112
+ }
113
+ }
114
+ }
115
+
116
+ async function openLightbox() {
117
+ if (open || !tileEl) return;
118
+ cancelAnimations();
119
+ const start = tileEl.getBoundingClientRect();
120
+ const target = viewportTarget();
121
+
122
+ open = true;
123
+ document.body.style.overflow = 'hidden';
124
+ hideEditorOverlay();
125
+ await tick();
126
+
127
+ Object.assign(tileEl.style, {
128
+ position: 'fixed',
129
+ top: `${start.top}px`,
130
+ left: `${start.left}px`,
131
+ width: `${start.width}px`,
132
+ height: `${start.height}px`,
133
+ zIndex: 'var(--z-modal)',
134
+ });
135
+
136
+ tileEl.animate(
137
+ [
138
+ { top: `${start.top}px`, left: `${start.left}px`, width: `${start.width}px`, height: `${start.height}px` },
139
+ { top: `${target.top}px`, left: `${target.left}px`, width: `${target.width}px`, height: `${target.height}px` },
140
+ ],
141
+ { duration: TRANSITION_MS, easing: TRANSITION_EASE, fill: 'forwards' },
142
+ );
143
+
144
+ overlayEl.animate([{ opacity: 0 }, { opacity: 1 }], {
145
+ duration: TRANSITION_MS,
146
+ easing: TRANSITION_EASE,
147
+ fill: 'forwards',
148
+ });
149
+
150
+ if (extended) {
151
+ toolbarEl?.animate([{ opacity: 0, transform: 'translate(-50%, 16px)' }, { opacity: 1, transform: 'translate(-50%, 0)' }], {
152
+ duration: TRANSITION_MS,
153
+ easing: TRANSITION_EASE,
154
+ fill: 'forwards',
155
+ delay: 80,
156
+ });
157
+ closeBtnEl?.animate([{ opacity: 0 }, { opacity: 1 }], {
158
+ duration: TRANSITION_MS,
159
+ easing: TRANSITION_EASE,
160
+ fill: 'forwards',
161
+ delay: 80,
162
+ });
163
+ }
164
+ }
165
+
166
+ function closeLightbox() {
167
+ if (!open || !tileEl || !wrapperEl) return;
168
+ cancelAnimations();
169
+ const target = wrapperEl.getBoundingClientRect();
170
+ const current = tileEl.getBoundingClientRect();
171
+
172
+ const anim = tileEl.animate(
173
+ [
174
+ { top: `${current.top}px`, left: `${current.left}px`, width: `${current.width}px`, height: `${current.height}px` },
175
+ { top: `${target.top}px`, left: `${target.left}px`, width: `${target.width}px`, height: `${target.height}px` },
176
+ ],
177
+ { duration: TRANSITION_MS, easing: TRANSITION_EASE, fill: 'forwards' },
178
+ );
179
+
180
+ overlayEl.animate([{ opacity: 1 }, { opacity: 0 }], {
181
+ duration: TRANSITION_MS,
182
+ easing: TRANSITION_EASE,
183
+ fill: 'forwards',
184
+ });
185
+
186
+ if (extended) {
187
+ toolbarEl?.animate([{ opacity: 1 }, { opacity: 0 }], {
188
+ duration: TRANSITION_MS,
189
+ easing: TRANSITION_EASE,
190
+ fill: 'forwards',
191
+ });
192
+ closeBtnEl?.animate([{ opacity: 1 }, { opacity: 0 }], {
193
+ duration: TRANSITION_MS,
194
+ easing: TRANSITION_EASE,
195
+ fill: 'forwards',
196
+ });
197
+ }
198
+
199
+ anim.onfinish = () => {
200
+ cancelAnimations();
201
+ tileEl.style.cssText = '';
202
+ transformEl.style.transform = '';
203
+ scale = 1;
204
+ offset = { x: 0, y: 0 };
205
+ open = false;
206
+ document.body.style.overflow = '';
207
+ restoreEditorOverlay();
208
+ };
209
+ }
210
+
211
+ function zoomTo(nextScale: number, anchor?: { x: number; y: number }) {
212
+ const s = Math.max(MIN_SCALE, Math.min(MAX_SCALE, nextScale));
213
+ if (s <= MIN_SCALE) {
214
+ scale = MIN_SCALE;
215
+ offset = { x: 0, y: 0 };
216
+ applyTransform(scale, offset);
217
+ return;
218
+ }
219
+ let next: { x: number; y: number };
220
+ if (anchor && tileEl) {
221
+ const r = tileEl.getBoundingClientRect();
222
+ const dx = anchor.x - (r.left + r.width / 2);
223
+ const dy = anchor.y - (r.top + r.height / 2);
224
+ const ratio = scale > 0 ? s / scale : 1;
225
+ next = clampOffset(dx * (1 - ratio) + offset.x * ratio, dy * (1 - ratio) + offset.y * ratio, s);
226
+ } else {
227
+ next = clampOffset(offset.x, offset.y, s);
228
+ }
229
+ scale = s;
230
+ offset = next;
231
+ applyTransform(scale, offset);
232
+ }
233
+
234
+ function onWheel(e: WheelEvent) {
235
+ if (!extended || !open) return;
236
+ e.preventDefault();
237
+ const factor = Math.exp(-e.deltaY * 0.002);
238
+ zoomTo(scale * factor, { x: e.clientX, y: e.clientY });
239
+ }
240
+
241
+ function onPointerDown(e: PointerEvent) {
242
+ if (!extended || !open || scale <= MIN_SCALE) return;
243
+ didDrag = false;
244
+ (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
245
+ dragState = {
246
+ startX: e.clientX,
247
+ startY: e.clientY,
248
+ baseX: offset.x,
249
+ baseY: offset.y,
250
+ pointerId: e.pointerId,
251
+ };
252
+ }
253
+
254
+ function onPointerMove(e: PointerEvent) {
255
+ if (!dragState) return;
256
+ const dx = e.clientX - dragState.startX;
257
+ const dy = e.clientY - dragState.startY;
258
+ if (!didDrag && Math.hypot(dx, dy) > 4) didDrag = true;
259
+ offset = clampOffset(dragState.baseX + dx, dragState.baseY + dy, scale);
260
+ applyTransform(scale, offset);
261
+ }
262
+
263
+ function onPointerUp(e: PointerEvent) {
264
+ if (dragState) {
265
+ (e.currentTarget as HTMLElement).releasePointerCapture(dragState.pointerId);
266
+ dragState = null;
267
+ }
268
+ }
269
+
270
+ function onTileClick() {
271
+ if (didDrag) {
272
+ didDrag = false;
273
+ return;
274
+ }
275
+ if (!open) {
276
+ openLightbox();
277
+ } else if (!extended) {
278
+ closeLightbox();
279
+ }
280
+ }
281
+
282
+ function onTileKeyDown(e: KeyboardEvent) {
283
+ if (e.key === 'Enter' || e.code === 'Space') {
284
+ e.preventDefault();
285
+ onTileClick();
286
+ }
287
+ }
288
+
289
+ onMount(() => {
290
+ const onKey = (e: KeyboardEvent) => {
291
+ if (e.key === 'Escape' && open) closeLightbox();
292
+ };
293
+ const onResize = () => {
294
+ if (!open || !tileEl) return;
295
+ const target = viewportTarget();
296
+ Object.assign(tileEl.style, {
297
+ top: `${target.top}px`,
298
+ left: `${target.left}px`,
299
+ width: `${target.width}px`,
300
+ height: `${target.height}px`,
301
+ });
302
+ offset = clampOffset(offset.x, offset.y, scale);
303
+ applyTransform(scale, offset);
304
+ };
305
+ document.addEventListener('keydown', onKey);
306
+ window.addEventListener('resize', onResize);
307
+ return () => {
308
+ document.removeEventListener('keydown', onKey);
309
+ window.removeEventListener('resize', onResize);
310
+ document.body.style.overflow = '';
311
+ restoreEditorOverlay();
312
+ };
313
+ });
314
+
315
+ let percentLabel = $derived(`${Math.round(scale * 100)}%`);
316
+ </script>
317
+
318
+ <div
319
+ bind:this={wrapperEl}
320
+ class="image-lightbox-wrapper"
321
+ style:aspect-ratio={aspect ? `${width} / ${height}` : undefined}
322
+ style:max-width={typeof maxWidth === 'number' ? `${maxWidth}px` : maxWidth}
323
+ >
324
+ <div
325
+ bind:this={tileEl}
326
+ class="image-lightbox-tile"
327
+ class:open
328
+ role="button"
329
+ tabindex="0"
330
+ aria-label={open ? `Close image: ${alt}` : `Expand image: ${alt}`}
331
+ onclick={onTileClick}
332
+ onkeydown={onTileKeyDown}
333
+ onpointerdown={onPointerDown}
334
+ onpointermove={onPointerMove}
335
+ onpointerup={onPointerUp}
336
+ onpointercancel={onPointerUp}
337
+ onwheel={onWheel}
338
+ >
339
+ <div class="image-lightbox-clip">
340
+ <div bind:this={transformEl} class="image-lightbox-transform">
341
+ <img {src} {alt} draggable="false" />
342
+ </div>
343
+ </div>
344
+ </div>
345
+ </div>
346
+
347
+ <div
348
+ bind:this={overlayEl}
349
+ class="image-lightbox-overlay"
350
+ class:active={open}
351
+ aria-hidden="true"
352
+ onclick={closeLightbox}
353
+ role="presentation"
354
+ ></div>
355
+
356
+ {#if extended}
357
+ <button
358
+ bind:this={closeBtnEl}
359
+ class="image-lightbox-chrome image-lightbox-close"
360
+ class:active={open}
361
+ type="button"
362
+ aria-label="Close"
363
+ onclick={closeLightbox}
364
+ >
365
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
366
+ <path d="M18 6L6 18" />
367
+ <path d="M6 6l12 12" />
368
+ </svg>
369
+ </button>
370
+
371
+ <div
372
+ bind:this={toolbarEl}
373
+ class="image-lightbox-toolbar"
374
+ class:active={open}
375
+ >
376
+ <button
377
+ class="image-lightbox-chrome-button"
378
+ type="button"
379
+ aria-label="Zoom out"
380
+ disabled={scale <= MIN_SCALE + 0.001}
381
+ onclick={() => zoomTo(scale / ZOOM_STEP)}
382
+ >
383
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
384
+ <path d="M5 12h14" />
385
+ </svg>
386
+ </button>
387
+ <span class="image-lightbox-toolbar-label">{percentLabel}</span>
388
+ <button
389
+ class="image-lightbox-chrome-button"
390
+ type="button"
391
+ aria-label="Zoom in"
392
+ disabled={scale >= MAX_SCALE - 0.001}
393
+ onclick={() => zoomTo(scale * ZOOM_STEP)}
394
+ >
395
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
396
+ <path d="M12 5v14" />
397
+ <path d="M5 12h14" />
398
+ </svg>
399
+ </button>
400
+ </div>
401
+ {/if}
402
+
403
+ <style>
404
+ :global(:root) {
405
+ /* tile (closed inline + animated modal surface) */
406
+ --imagelightbox-tile-radius: var(--radius-2xl);
407
+ --imagelightbox-tile-border: var(--color-transparent);
408
+ --imagelightbox-tile-border-width: var(--border-width-0);
409
+ --imagelightbox-tile-shadow: var(--shadow-md);
410
+
411
+ /* overlay */
412
+ --imagelightbox-overlay-surface: var(--overlay-high);
413
+
414
+ /* chrome (toolbar + close button) */
415
+ --imagelightbox-chrome-surface: var(--overlay-higher);
416
+ --imagelightbox-chrome-border: var(--border-brand);
417
+ --imagelightbox-chrome-border-width: var(--border-width-1);
418
+ --imagelightbox-chrome-radius: var(--radius-full);
419
+ --imagelightbox-chrome-icon: var(--text-primary);
420
+ --imagelightbox-chrome-hover-surface: var(--surface-brand-high);
421
+ }
422
+
423
+ .image-lightbox-wrapper {
424
+ position: relative;
425
+ width: 100%;
426
+ }
427
+
428
+ .image-lightbox-tile {
429
+ position: absolute;
430
+ inset: 0;
431
+ cursor: zoom-in;
432
+ border: var(--imagelightbox-tile-border-width) solid var(--imagelightbox-tile-border);
433
+ border-radius: var(--imagelightbox-tile-radius);
434
+ box-shadow: var(--imagelightbox-tile-shadow);
435
+ background: transparent;
436
+ overflow: visible;
437
+ transition: transform 250ms ease;
438
+ }
439
+
440
+ .image-lightbox-tile:hover:not(.open) {
441
+ transform: scale(1.02);
442
+ }
443
+
444
+ .image-lightbox-tile.open {
445
+ cursor: zoom-out;
446
+ }
447
+
448
+ .image-lightbox-tile:focus-visible {
449
+ outline: 2px solid var(--border-brand-medium);
450
+ outline-offset: 2px;
451
+ }
452
+
453
+ .image-lightbox-clip {
454
+ position: absolute;
455
+ inset: 0;
456
+ border-radius: inherit;
457
+ overflow: hidden;
458
+ }
459
+
460
+ .image-lightbox-transform {
461
+ position: absolute;
462
+ inset: 0;
463
+ will-change: transform;
464
+ transform-origin: center center;
465
+ }
466
+
467
+ .image-lightbox-transform img {
468
+ width: 100%;
469
+ height: 100%;
470
+ object-fit: contain;
471
+ object-position: center;
472
+ user-select: none;
473
+ pointer-events: none;
474
+ display: block;
475
+ }
476
+
477
+ .image-lightbox-overlay {
478
+ position: fixed;
479
+ inset: 0;
480
+ background: var(--imagelightbox-overlay-surface);
481
+ backdrop-filter: blur(var(--blur-md));
482
+ z-index: var(--z-overlay);
483
+ opacity: 0;
484
+ pointer-events: none;
485
+ }
486
+
487
+ .image-lightbox-overlay.active {
488
+ pointer-events: auto;
489
+ cursor: zoom-out;
490
+ }
491
+
492
+ .image-lightbox-chrome {
493
+ position: fixed;
494
+ z-index: var(--z-modal);
495
+ background: var(--imagelightbox-chrome-surface);
496
+ border: var(--imagelightbox-chrome-border-width) solid var(--imagelightbox-chrome-border);
497
+ border-radius: var(--imagelightbox-chrome-radius);
498
+ color: var(--imagelightbox-chrome-icon);
499
+ backdrop-filter: blur(var(--blur-md));
500
+ opacity: 0;
501
+ pointer-events: none;
502
+ transition: background var(--duration-200) ease;
503
+ }
504
+
505
+ .image-lightbox-chrome.active {
506
+ pointer-events: auto;
507
+ }
508
+
509
+ .image-lightbox-close {
510
+ top: var(--space-24);
511
+ right: var(--space-24);
512
+ width: 2.75rem;
513
+ height: 2.75rem;
514
+ display: flex;
515
+ align-items: center;
516
+ justify-content: center;
517
+ cursor: pointer;
518
+ }
519
+
520
+ .image-lightbox-close:hover {
521
+ background: var(--imagelightbox-chrome-hover-surface);
522
+ }
523
+
524
+ .image-lightbox-toolbar {
525
+ position: fixed;
526
+ left: 50%;
527
+ bottom: var(--space-32);
528
+ transform: translateX(-50%);
529
+ z-index: var(--z-modal);
530
+ display: flex;
531
+ align-items: center;
532
+ gap: var(--space-4);
533
+ padding: var(--space-4);
534
+ background: var(--imagelightbox-chrome-surface);
535
+ border: var(--imagelightbox-chrome-border-width) solid var(--imagelightbox-chrome-border);
536
+ border-radius: var(--imagelightbox-chrome-radius);
537
+ color: var(--imagelightbox-chrome-icon);
538
+ backdrop-filter: blur(var(--blur-md));
539
+ opacity: 0;
540
+ pointer-events: none;
541
+ }
542
+
543
+ .image-lightbox-toolbar.active {
544
+ pointer-events: auto;
545
+ }
546
+
547
+ .image-lightbox-toolbar-label {
548
+ min-width: 3.5ch;
549
+ text-align: center;
550
+ font-family: var(--font-mono);
551
+ font-size: var(--font-size-xs);
552
+ letter-spacing: var(--letter-spacing-wider);
553
+ padding: 0 var(--space-4);
554
+ }
555
+
556
+ .image-lightbox-chrome-button {
557
+ background: none;
558
+ border: none;
559
+ color: inherit;
560
+ width: 2.5rem;
561
+ height: 2.5rem;
562
+ display: flex;
563
+ align-items: center;
564
+ justify-content: center;
565
+ border-radius: var(--radius-full);
566
+ cursor: pointer;
567
+ transition: background var(--duration-200) ease;
568
+ }
569
+
570
+ .image-lightbox-chrome-button:hover:not(:disabled) {
571
+ background: var(--imagelightbox-chrome-hover-surface);
572
+ }
573
+
574
+ .image-lightbox-chrome-button:disabled {
575
+ opacity: 0.4;
576
+ cursor: not-allowed;
577
+ }
578
+ </style>