@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.
- package/.claude/skills/live-tokens-add-component/SKILL.md +488 -0
- package/README.md +84 -29
- package/dist-plugin/index.cjs +177 -125
- package/dist-plugin/index.d.cts +3 -2
- package/dist-plugin/index.d.ts +3 -2
- package/dist-plugin/index.js +177 -125
- package/package.json +8 -2
- package/src/editor/component-editor/BadgeEditor.svelte +44 -42
- package/src/editor/component-editor/ButtonEditor.svelte +224 -0
- package/src/editor/component-editor/CollapsibleSectionEditor.svelte +1 -7
- package/src/editor/component-editor/CornerBadgeEditor.svelte +44 -34
- package/src/editor/component-editor/ImageLightboxEditor.svelte +58 -0
- package/src/editor/component-editor/InputEditor.svelte +272 -0
- package/src/editor/component-editor/NotificationEditor.svelte +44 -65
- package/src/editor/component-editor/ProgressBarEditor.svelte +71 -87
- package/src/editor/component-editor/SegmentedControlEditor.svelte +98 -37
- package/src/editor/component-editor/SideNavigationEditor.svelte +342 -0
- package/src/editor/component-editor/index.ts +16 -1
- package/src/editor/component-editor/registry.ts +138 -28
- package/src/editor/component-editor/scaffolding/ComponentFileManager.svelte +3 -2
- package/src/editor/component-editor/scaffolding/ComponentsTab.svelte +2 -2
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +9 -10
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +60 -36
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +38 -1
- package/src/editor/component-editor/scaffolding/buildTypeGroupTokens.ts +1 -1
- package/src/editor/component-editor/scaffolding/componentSources.ts +3 -3
- package/src/editor/component-editor/scaffolding/defaultSections.ts +15 -10
- package/src/editor/component-editor/scaffolding/siblings.ts +2 -2
- package/src/editor/component-editor/scaffolding/types.ts +2 -1
- package/src/editor/core/components/componentConfigKeys.ts +14 -3
- package/src/editor/core/components/componentConfigService.ts +7 -6
- package/src/editor/core/manifests/manifestService.ts +5 -4
- package/src/editor/core/storage/apiBase.ts +15 -0
- package/src/editor/core/storage/files/versionedFileResourceClient.ts +1 -1
- package/src/editor/core/themes/migrations/2026-05-24-collapsiblesection-drop-active-state.ts +28 -0
- package/src/editor/core/themes/migrations/2026-05-24-progressbar-collapse-variants.ts +41 -0
- package/src/editor/core/themes/migrations/2026-05-24-promote-state-shared-tokens.ts +59 -0
- package/src/editor/core/themes/migrations/2026-05-24-segmentedcontrol-divider-inset.ts +29 -0
- package/src/editor/core/themes/migrations/2026-05-25-cornerbadge-flatten-variants.ts +46 -0
- package/src/editor/core/themes/migrations/index.ts +10 -0
- package/src/editor/core/themes/slices/components.ts +9 -0
- package/src/editor/core/themes/themeInit.ts +3 -2
- package/src/editor/core/themes/themeService.ts +3 -2
- package/src/editor/index.ts +10 -1
- package/src/editor/pages/ComponentEditorPage.svelte +53 -3
- package/src/editor/pages/EditorShell.svelte +53 -3
- package/src/editor/ui/UIEasingSelector.svelte +240 -0
- package/src/editor/ui/variantScales.ts +34 -0
- package/src/system/components/Button.svelte +34 -85
- package/src/system/components/CollapsibleSection.svelte +1 -48
- package/src/system/components/CornerBadge.svelte +72 -138
- package/src/system/components/Dialog.svelte +24 -4
- package/src/system/components/ImageLightbox.svelte +578 -0
- package/src/system/components/Input.svelte +387 -0
- package/src/system/components/ProgressBar.svelte +62 -258
- package/src/system/components/SectionDivider.svelte +117 -43
- package/src/system/components/SegmentedControl.svelte +81 -15
- package/src/system/components/SideNavigation.svelte +777 -0
- package/src/system/styles/tokens.css +43 -0
- package/src/system/styles/tokens.generated.css +4 -183
- 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
|
|
43
|
-
let
|
|
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>
|