@leavepulse/ui 0.15.4 → 0.16.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/dist/chunks/{LpConfirmDialog-D6Rhge21.js → LpConfirmDialog-C-iOXJIM.js} +1 -1
- package/dist/chunks/LpLightbox-CWdIsthW.js +424 -0
- package/dist/chunks/{LpModal-d6SZpfBH.js → LpModal-DeZlvK1B.js} +2 -2
- package/dist/component-names.d.ts +1 -1
- package/dist/components/LpConfirmDialog.vue.js +1 -1
- package/dist/components/LpLightbox.vue.d.ts +62 -0
- package/dist/components/LpLightbox.vue.js +4 -0
- package/dist/components/LpModal.vue.js +1 -1
- package/dist/components/lightbox.d.ts +14 -0
- package/dist/components/lightbox.js +13 -0
- package/dist/composables/useZoomPan.d.ts +34 -0
- package/dist/composables/useZoomPan.js +133 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +73 -67
- package/package.json +1 -1
- package/src/component-names.ts +1 -0
- package/src/components/LpLightbox.vue +439 -0
- package/src/components/LpModal.vue +8 -3
- package/src/components/lightbox.ts +32 -0
- package/src/composables/useZoomPan.ts +211 -0
- package/src/index.ts +5 -0
- package/src/tokens/tokens.css +36 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineComponent, openBlock, createBlock, withCtx, renderSlot, createVNode, createTextVNode, toDisplayString } from "vue";
|
|
2
2
|
import { _ as _sfc_main$2 } from "./LpButton-5VXXCjwF.js";
|
|
3
|
-
import { _ as _sfc_main$1 } from "./LpModal-
|
|
3
|
+
import { _ as _sfc_main$1 } from "./LpModal-DeZlvK1B.js";
|
|
4
4
|
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
5
5
|
__name: "LpConfirmDialog",
|
|
6
6
|
props: {
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
import { defineComponent, computed, ref, watch, nextTick, openBlock, createBlock, unref, withCtx, createVNode, createTextVNode, toDisplayString, createElementVNode, createElementBlock, createCommentVNode, renderSlot, normalizeClass, withModifiers, normalizeStyle, Fragment, renderList } from "vue";
|
|
2
|
+
import { DialogRoot, DialogPortal, DialogOverlay, DialogContent, DialogTitle, DialogDescription } from "reka-ui";
|
|
3
|
+
import { useZoomPan } from "../composables/useZoomPan.js";
|
|
4
|
+
import { fileNameOf } from "../components/lightbox.js";
|
|
5
|
+
import { _ as _sfc_main$1 } from "./LpIcon-CCnX5_2j.js";
|
|
6
|
+
const _hoisted_1 = { class: "flex shrink-0 items-center gap-1 p-3 text-sm text-white/70 [--chrome-from:-8px] animate-[lightbox-chrome-in_var(--duration-medium)_var(--ease-emphasized)_60ms_both] motion-reduce:animate-none" };
|
|
7
|
+
const _hoisted_2 = {
|
|
8
|
+
key: 0,
|
|
9
|
+
class: "px-2 tabular-nums"
|
|
10
|
+
};
|
|
11
|
+
const _hoisted_3 = {
|
|
12
|
+
key: 1,
|
|
13
|
+
class: "px-2 tabular-nums"
|
|
14
|
+
};
|
|
15
|
+
const _hoisted_4 = { class: "ml-auto flex items-center gap-1" };
|
|
16
|
+
const _hoisted_5 = ["disabled"];
|
|
17
|
+
const _hoisted_6 = {
|
|
18
|
+
key: 0,
|
|
19
|
+
class: "flex flex-col items-center gap-3 text-white/60"
|
|
20
|
+
};
|
|
21
|
+
const _hoisted_7 = ["src", "alt"];
|
|
22
|
+
const _hoisted_8 = ["disabled"];
|
|
23
|
+
const _hoisted_9 = { class: "shrink-0 p-3 pt-2 animate-[lightbox-chrome-in_var(--duration-medium)_var(--ease-emphasized)_60ms_both] motion-reduce:animate-none" };
|
|
24
|
+
const _hoisted_10 = {
|
|
25
|
+
key: 0,
|
|
26
|
+
class: "mb-2 text-center"
|
|
27
|
+
};
|
|
28
|
+
const _hoisted_11 = {
|
|
29
|
+
key: 0,
|
|
30
|
+
class: "truncate text-sm font-medium text-white"
|
|
31
|
+
};
|
|
32
|
+
const _hoisted_12 = {
|
|
33
|
+
key: 1,
|
|
34
|
+
class: "truncate text-xs text-white/55"
|
|
35
|
+
};
|
|
36
|
+
const _hoisted_13 = ["data-index", "aria-label", "onClick"];
|
|
37
|
+
const _hoisted_14 = ["src", "alt"];
|
|
38
|
+
const BTN = "flex size-9 items-center justify-center rounded-control text-white/80 outline-none transition-colors duration-[var(--duration-fast)] hover:bg-white/15 hover:text-white focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-40";
|
|
39
|
+
const ARROW = "absolute top-1/2 z-10 flex size-11 -translate-y-1/2 items-center justify-center rounded-pill bg-black/45 text-white/90 outline-none backdrop-blur-sm transition-[background-color,scale] duration-[var(--duration-fast)] hover:bg-black/65 hover:scale-105 focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-0";
|
|
40
|
+
const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
41
|
+
__name: "LpLightbox",
|
|
42
|
+
props: {
|
|
43
|
+
open: { type: Boolean, default: false },
|
|
44
|
+
items: {},
|
|
45
|
+
index: { default: 0 },
|
|
46
|
+
filmstrip: { type: Boolean },
|
|
47
|
+
rotatable: { type: Boolean, default: true },
|
|
48
|
+
downloadable: { type: Boolean, default: true },
|
|
49
|
+
copyable: { type: Boolean, default: false },
|
|
50
|
+
loop: { type: Boolean, default: true },
|
|
51
|
+
maxZoom: { default: 8 }
|
|
52
|
+
},
|
|
53
|
+
emits: ["update:open", "update:index", "change", "download", "copy"],
|
|
54
|
+
setup(__props, { emit: __emit }) {
|
|
55
|
+
const props = __props;
|
|
56
|
+
const emit = __emit;
|
|
57
|
+
const zoom = useZoomPan({ max: props.maxZoom });
|
|
58
|
+
const scale = computed(() => zoom.scale.value);
|
|
59
|
+
const isZoomed = computed(() => zoom.zoomed.value);
|
|
60
|
+
const isPanning = computed(() => zoom.panning.value);
|
|
61
|
+
const current = computed(() => Math.min(Math.max(props.index, 0), Math.max(props.items.length - 1, 0)));
|
|
62
|
+
const item = computed(() => props.items[current.value]);
|
|
63
|
+
const total = computed(() => props.items.length);
|
|
64
|
+
const showStrip = computed(() => props.filmstrip ?? total.value > 1);
|
|
65
|
+
const rotation = ref(0);
|
|
66
|
+
const loading = ref(false);
|
|
67
|
+
const failed = ref(false);
|
|
68
|
+
const viewport = ref(null);
|
|
69
|
+
const stripEl = ref(null);
|
|
70
|
+
watch(viewport, (el) => zoom.setViewport(el));
|
|
71
|
+
const direction = ref(1);
|
|
72
|
+
function go(next) {
|
|
73
|
+
if (!total.value) return;
|
|
74
|
+
const wrapped = props.loop ? (next + total.value) % total.value : Math.min(Math.max(next, 0), total.value - 1);
|
|
75
|
+
if (wrapped === current.value) return;
|
|
76
|
+
direction.value = next > current.value ? 1 : -1;
|
|
77
|
+
emit("update:index", wrapped);
|
|
78
|
+
emit("change", wrapped, props.items[wrapped]);
|
|
79
|
+
}
|
|
80
|
+
const canPrev = computed(() => total.value > 1 && (props.loop || current.value > 0));
|
|
81
|
+
const canNext = computed(() => total.value > 1 && (props.loop || current.value < total.value - 1));
|
|
82
|
+
watch(
|
|
83
|
+
() => [current.value, props.open],
|
|
84
|
+
() => {
|
|
85
|
+
zoom.reset();
|
|
86
|
+
rotation.value = 0;
|
|
87
|
+
failed.value = false;
|
|
88
|
+
loading.value = true;
|
|
89
|
+
if (props.open) nextTick(scrollThumbIntoView);
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
function scrollThumbIntoView() {
|
|
93
|
+
const el = stripEl.value?.querySelector(`[data-index="${current.value}"]`);
|
|
94
|
+
el?.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" });
|
|
95
|
+
}
|
|
96
|
+
function close() {
|
|
97
|
+
emit("update:open", false);
|
|
98
|
+
}
|
|
99
|
+
function onKeydown(e) {
|
|
100
|
+
switch (e.key) {
|
|
101
|
+
case "ArrowRight":
|
|
102
|
+
e.preventDefault();
|
|
103
|
+
go(current.value + 1);
|
|
104
|
+
break;
|
|
105
|
+
case "ArrowLeft":
|
|
106
|
+
e.preventDefault();
|
|
107
|
+
go(current.value - 1);
|
|
108
|
+
break;
|
|
109
|
+
case "Home":
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
go(0);
|
|
112
|
+
break;
|
|
113
|
+
case "End":
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
go(total.value - 1);
|
|
116
|
+
break;
|
|
117
|
+
case "+":
|
|
118
|
+
case "=":
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
zoom.zoomBy(1.3);
|
|
121
|
+
break;
|
|
122
|
+
case "-":
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
zoom.zoomBy(1 / 1.3);
|
|
125
|
+
break;
|
|
126
|
+
case "0":
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
zoom.reset();
|
|
129
|
+
break;
|
|
130
|
+
case "r":
|
|
131
|
+
if (props.rotatable) {
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
rotate();
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function rotate() {
|
|
139
|
+
rotation.value = (rotation.value + 90) % 360;
|
|
140
|
+
zoom.reset();
|
|
141
|
+
}
|
|
142
|
+
let swipeStart = null;
|
|
143
|
+
function onPointerDown(e) {
|
|
144
|
+
zoom.onPointerDown(e);
|
|
145
|
+
swipeStart = zoom.zoomed.value ? null : { x: e.clientX, y: e.clientY };
|
|
146
|
+
}
|
|
147
|
+
function onPointerUp(e) {
|
|
148
|
+
zoom.onPointerUp(e);
|
|
149
|
+
if (!swipeStart) return;
|
|
150
|
+
const dx = e.clientX - swipeStart.x;
|
|
151
|
+
const dy = e.clientY - swipeStart.y;
|
|
152
|
+
swipeStart = null;
|
|
153
|
+
if (Math.abs(dx) > 60 && Math.abs(dx) > Math.abs(dy) * 1.5) {
|
|
154
|
+
go(current.value + (dx < 0 ? 1 : -1));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function onDoubleClick(e) {
|
|
158
|
+
zoom.toggleZoom({ x: e.clientX, y: e.clientY });
|
|
159
|
+
}
|
|
160
|
+
function onViewportClick(e) {
|
|
161
|
+
if (zoom.zoomed.value) return;
|
|
162
|
+
if (e.target.closest("[data-lightbox-image]")) return;
|
|
163
|
+
close();
|
|
164
|
+
}
|
|
165
|
+
async function download() {
|
|
166
|
+
const target = item.value;
|
|
167
|
+
if (!target) return;
|
|
168
|
+
emit("download", target);
|
|
169
|
+
const a = document.createElement("a");
|
|
170
|
+
a.href = target.src;
|
|
171
|
+
a.download = fileNameOf(target);
|
|
172
|
+
a.rel = "noopener";
|
|
173
|
+
document.body.appendChild(a);
|
|
174
|
+
a.click();
|
|
175
|
+
a.remove();
|
|
176
|
+
}
|
|
177
|
+
async function copy() {
|
|
178
|
+
const target = item.value;
|
|
179
|
+
if (!target) return;
|
|
180
|
+
try {
|
|
181
|
+
const response = await fetch(target.src);
|
|
182
|
+
const blob = await response.blob();
|
|
183
|
+
const png = blob.type === "image/png" ? blob : await reencodeToPng(await createImageBitmap(blob));
|
|
184
|
+
await navigator.clipboard.write([new ClipboardItem({ "image/png": png })]);
|
|
185
|
+
emit("copy", target, true);
|
|
186
|
+
} catch {
|
|
187
|
+
emit("copy", target, false);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async function reencodeToPng(bitmap) {
|
|
191
|
+
const canvas = document.createElement("canvas");
|
|
192
|
+
canvas.width = bitmap.width;
|
|
193
|
+
canvas.height = bitmap.height;
|
|
194
|
+
canvas.getContext("2d")?.drawImage(bitmap, 0, 0);
|
|
195
|
+
return new Promise(
|
|
196
|
+
(resolve, reject) => canvas.toBlob((b) => b ? resolve(b) : reject(new Error("encode failed")), "image/png")
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
const imageStyle = computed(() => ({
|
|
200
|
+
...zoom.style.value,
|
|
201
|
+
rotate: `${rotation.value}deg`,
|
|
202
|
+
// Consumed by the lightbox-frame-in keyframe; negative when paging backwards.
|
|
203
|
+
"--frame-from": `${direction.value * 24}px`
|
|
204
|
+
}));
|
|
205
|
+
return (_ctx, _cache) => {
|
|
206
|
+
return openBlock(), createBlock(unref(DialogRoot), {
|
|
207
|
+
open: __props.open,
|
|
208
|
+
"onUpdate:open": _cache[9] || (_cache[9] = (v) => emit("update:open", v))
|
|
209
|
+
}, {
|
|
210
|
+
default: withCtx(() => [
|
|
211
|
+
createVNode(unref(DialogPortal), null, {
|
|
212
|
+
default: withCtx(() => [
|
|
213
|
+
createVNode(unref(DialogOverlay), { class: "fixed inset-0 z-(--z-overlay) bg-black/85 backdrop-blur-sm data-[state=open]:animate-[fade-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[fade-out_120ms_ease]" }),
|
|
214
|
+
createVNode(unref(DialogContent), {
|
|
215
|
+
class: "fixed inset-0 z-(--z-modal) flex flex-col outline-none data-[state=open]:animate-[lightbox-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[lightbox-out_140ms_cubic-bezier(0.4,0,1,1)] motion-reduce:animate-none",
|
|
216
|
+
onKeydown
|
|
217
|
+
}, {
|
|
218
|
+
default: withCtx(() => [
|
|
219
|
+
createVNode(unref(DialogTitle), { class: "sr-only" }, {
|
|
220
|
+
default: withCtx(() => [
|
|
221
|
+
createTextVNode(toDisplayString(item.value?.title ?? "Image viewer"), 1)
|
|
222
|
+
]),
|
|
223
|
+
_: 1
|
|
224
|
+
}),
|
|
225
|
+
createVNode(unref(DialogDescription), { class: "sr-only" }, {
|
|
226
|
+
default: withCtx(() => [
|
|
227
|
+
createTextVNode(toDisplayString(item.value?.description ?? `Image ${current.value + 1} of ${total.value}`), 1)
|
|
228
|
+
]),
|
|
229
|
+
_: 1
|
|
230
|
+
}),
|
|
231
|
+
createElementVNode("div", _hoisted_1, [
|
|
232
|
+
total.value > 1 ? (openBlock(), createElementBlock("span", _hoisted_2, toDisplayString(current.value + 1) + " / " + toDisplayString(total.value), 1)) : createCommentVNode("", true),
|
|
233
|
+
isZoomed.value ? (openBlock(), createElementBlock("span", _hoisted_3, toDisplayString(Math.round(scale.value * 100)) + "% ", 1)) : createCommentVNode("", true),
|
|
234
|
+
createElementVNode("div", _hoisted_4, [
|
|
235
|
+
renderSlot(_ctx.$slots, "actions", {
|
|
236
|
+
item: item.value,
|
|
237
|
+
index: current.value
|
|
238
|
+
}),
|
|
239
|
+
createElementVNode("button", {
|
|
240
|
+
class: normalizeClass(BTN),
|
|
241
|
+
type: "button",
|
|
242
|
+
title: "Zoom out (−)",
|
|
243
|
+
onClick: _cache[0] || (_cache[0] = ($event) => unref(zoom).zoomBy(1 / 1.3))
|
|
244
|
+
}, [
|
|
245
|
+
createVNode(_sfc_main$1, {
|
|
246
|
+
name: "lucide:zoom-out",
|
|
247
|
+
size: 18
|
|
248
|
+
})
|
|
249
|
+
]),
|
|
250
|
+
createElementVNode("button", {
|
|
251
|
+
class: normalizeClass(BTN),
|
|
252
|
+
type: "button",
|
|
253
|
+
title: "Zoom in (+)",
|
|
254
|
+
onClick: _cache[1] || (_cache[1] = ($event) => unref(zoom).zoomBy(1.3))
|
|
255
|
+
}, [
|
|
256
|
+
createVNode(_sfc_main$1, {
|
|
257
|
+
name: "lucide:zoom-in",
|
|
258
|
+
size: 18
|
|
259
|
+
})
|
|
260
|
+
]),
|
|
261
|
+
__props.rotatable ? (openBlock(), createElementBlock("button", {
|
|
262
|
+
key: 0,
|
|
263
|
+
class: normalizeClass(BTN),
|
|
264
|
+
type: "button",
|
|
265
|
+
title: "Rotate (R)",
|
|
266
|
+
onClick: rotate
|
|
267
|
+
}, [
|
|
268
|
+
createVNode(_sfc_main$1, {
|
|
269
|
+
name: "lucide:rotate-cw",
|
|
270
|
+
size: 18
|
|
271
|
+
})
|
|
272
|
+
])) : createCommentVNode("", true),
|
|
273
|
+
__props.copyable ? (openBlock(), createElementBlock("button", {
|
|
274
|
+
key: 1,
|
|
275
|
+
class: normalizeClass(BTN),
|
|
276
|
+
type: "button",
|
|
277
|
+
title: "Copy image",
|
|
278
|
+
onClick: copy
|
|
279
|
+
}, [
|
|
280
|
+
createVNode(_sfc_main$1, {
|
|
281
|
+
name: "lucide:copy",
|
|
282
|
+
size: 18
|
|
283
|
+
})
|
|
284
|
+
])) : createCommentVNode("", true),
|
|
285
|
+
__props.downloadable ? (openBlock(), createElementBlock("button", {
|
|
286
|
+
key: 2,
|
|
287
|
+
class: normalizeClass(BTN),
|
|
288
|
+
type: "button",
|
|
289
|
+
title: "Download",
|
|
290
|
+
onClick: download
|
|
291
|
+
}, [
|
|
292
|
+
createVNode(_sfc_main$1, {
|
|
293
|
+
name: "lucide:download",
|
|
294
|
+
size: 18
|
|
295
|
+
})
|
|
296
|
+
])) : createCommentVNode("", true),
|
|
297
|
+
createElementVNode("button", {
|
|
298
|
+
class: normalizeClass(BTN),
|
|
299
|
+
type: "button",
|
|
300
|
+
title: "Close (Esc)",
|
|
301
|
+
onClick: close
|
|
302
|
+
}, [
|
|
303
|
+
createVNode(_sfc_main$1, {
|
|
304
|
+
name: "lucide:x",
|
|
305
|
+
size: 20
|
|
306
|
+
})
|
|
307
|
+
])
|
|
308
|
+
])
|
|
309
|
+
]),
|
|
310
|
+
createElementVNode("div", {
|
|
311
|
+
ref_key: "viewport",
|
|
312
|
+
ref: viewport,
|
|
313
|
+
class: normalizeClass(["relative flex min-h-0 flex-1 items-center justify-center overflow-hidden", isPanning.value ? "cursor-grabbing" : isZoomed.value ? "cursor-grab" : ""]),
|
|
314
|
+
onWheel: _cache[6] || (_cache[6] = //@ts-ignore
|
|
315
|
+
(...args) => unref(zoom).onWheel && unref(zoom).onWheel(...args)),
|
|
316
|
+
onPointerdown: onPointerDown,
|
|
317
|
+
onPointermove: _cache[7] || (_cache[7] = //@ts-ignore
|
|
318
|
+
(...args) => unref(zoom).onPointerMove && unref(zoom).onPointerMove(...args)),
|
|
319
|
+
onPointerup: onPointerUp,
|
|
320
|
+
onPointercancel: _cache[8] || (_cache[8] = //@ts-ignore
|
|
321
|
+
(...args) => unref(zoom).onPointerUp && unref(zoom).onPointerUp(...args)),
|
|
322
|
+
onDblclick: onDoubleClick,
|
|
323
|
+
onClick: onViewportClick
|
|
324
|
+
}, [
|
|
325
|
+
createElementVNode("button", {
|
|
326
|
+
class: normalizeClass([ARROW, "left-4"]),
|
|
327
|
+
type: "button",
|
|
328
|
+
disabled: !canPrev.value,
|
|
329
|
+
"aria-label": "Previous image",
|
|
330
|
+
onClick: _cache[2] || (_cache[2] = withModifiers(($event) => go(current.value - 1), ["stop"]))
|
|
331
|
+
}, [
|
|
332
|
+
createVNode(_sfc_main$1, {
|
|
333
|
+
name: "lucide:chevron-left",
|
|
334
|
+
size: 24
|
|
335
|
+
})
|
|
336
|
+
], 10, _hoisted_5),
|
|
337
|
+
failed.value ? (openBlock(), createElementBlock("div", _hoisted_6, [
|
|
338
|
+
createVNode(_sfc_main$1, {
|
|
339
|
+
name: "lucide:image-off",
|
|
340
|
+
size: 40
|
|
341
|
+
}),
|
|
342
|
+
_cache[10] || (_cache[10] = createElementVNode("p", { class: "text-sm" }, "This image couldn't be loaded.", -1))
|
|
343
|
+
])) : item.value ? (openBlock(), createElementBlock("img", {
|
|
344
|
+
key: item.value.src,
|
|
345
|
+
"data-lightbox-image": "",
|
|
346
|
+
src: item.value.src,
|
|
347
|
+
alt: item.value.title ?? "",
|
|
348
|
+
class: "max-h-full max-w-full select-none object-contain transition-transform duration-[var(--duration-medium)] ease-[var(--ease-emphasized)] animate-[lightbox-frame-in_var(--duration-medium)_var(--ease-emphasized)] motion-reduce:animate-none motion-reduce:transition-none",
|
|
349
|
+
style: normalizeStyle(imageStyle.value),
|
|
350
|
+
draggable: "false",
|
|
351
|
+
onLoad: _cache[3] || (_cache[3] = ($event) => loading.value = false),
|
|
352
|
+
onError: _cache[4] || (_cache[4] = ($event) => (failed.value = true, loading.value = false))
|
|
353
|
+
}, null, 44, _hoisted_7)) : createCommentVNode("", true),
|
|
354
|
+
loading.value && !failed.value ? (openBlock(), createBlock(_sfc_main$1, {
|
|
355
|
+
key: 2,
|
|
356
|
+
name: "lucide:loader-circle",
|
|
357
|
+
size: 28,
|
|
358
|
+
class: "absolute animate-spin text-white/50"
|
|
359
|
+
})) : createCommentVNode("", true),
|
|
360
|
+
createElementVNode("button", {
|
|
361
|
+
class: normalizeClass([ARROW, "right-4"]),
|
|
362
|
+
type: "button",
|
|
363
|
+
disabled: !canNext.value,
|
|
364
|
+
"aria-label": "Next image",
|
|
365
|
+
onClick: _cache[5] || (_cache[5] = withModifiers(($event) => go(current.value + 1), ["stop"]))
|
|
366
|
+
}, [
|
|
367
|
+
createVNode(_sfc_main$1, {
|
|
368
|
+
name: "lucide:chevron-right",
|
|
369
|
+
size: 24
|
|
370
|
+
})
|
|
371
|
+
], 10, _hoisted_8)
|
|
372
|
+
], 34),
|
|
373
|
+
createElementVNode("div", _hoisted_9, [
|
|
374
|
+
renderSlot(_ctx.$slots, "caption", {
|
|
375
|
+
item: item.value,
|
|
376
|
+
index: current.value,
|
|
377
|
+
total: total.value
|
|
378
|
+
}, () => [
|
|
379
|
+
item.value?.title || item.value?.description ? (openBlock(), createElementBlock("div", _hoisted_10, [
|
|
380
|
+
item.value.title ? (openBlock(), createElementBlock("p", _hoisted_11, toDisplayString(item.value.title), 1)) : createCommentVNode("", true),
|
|
381
|
+
item.value.description ? (openBlock(), createElementBlock("p", _hoisted_12, toDisplayString(item.value.description), 1)) : createCommentVNode("", true)
|
|
382
|
+
])) : createCommentVNode("", true)
|
|
383
|
+
]),
|
|
384
|
+
showStrip.value && total.value > 1 ? (openBlock(), createElementBlock("div", {
|
|
385
|
+
key: 0,
|
|
386
|
+
ref_key: "stripEl",
|
|
387
|
+
ref: stripEl,
|
|
388
|
+
class: "lp-scrollbar-none flex justify-start gap-2 overflow-x-auto sm:justify-center"
|
|
389
|
+
}, [
|
|
390
|
+
(openBlock(true), createElementBlock(Fragment, null, renderList(__props.items, (entry, i) => {
|
|
391
|
+
return openBlock(), createElementBlock("button", {
|
|
392
|
+
key: entry.src + i,
|
|
393
|
+
"data-index": i,
|
|
394
|
+
type: "button",
|
|
395
|
+
style: normalizeStyle({ "--thumb-delay": `${Math.min(i * 30, 240)}ms` }),
|
|
396
|
+
class: normalizeClass(["size-14 shrink-0 overflow-hidden rounded-control outline-none ring-2 transition-[opacity,box-shadow,scale] duration-[var(--duration-fast)] hover:scale-105 focus-visible:ring-ring animate-[lightbox-chrome-in_var(--duration-medium)_var(--ease-emphasized)_both] [animation-delay:var(--thumb-delay)] motion-reduce:animate-none", i === current.value ? "opacity-100 ring-brand" : "opacity-45 ring-transparent hover:opacity-80"]),
|
|
397
|
+
"aria-label": entry.title ?? `Image ${i + 1}`,
|
|
398
|
+
onClick: ($event) => go(i)
|
|
399
|
+
}, [
|
|
400
|
+
createElementVNode("img", {
|
|
401
|
+
src: entry.thumb ?? entry.src,
|
|
402
|
+
alt: entry.title ?? "",
|
|
403
|
+
class: "size-full object-cover",
|
|
404
|
+
draggable: "false"
|
|
405
|
+
}, null, 8, _hoisted_14)
|
|
406
|
+
], 14, _hoisted_13);
|
|
407
|
+
}), 128))
|
|
408
|
+
], 512)) : createCommentVNode("", true)
|
|
409
|
+
])
|
|
410
|
+
]),
|
|
411
|
+
_: 3
|
|
412
|
+
})
|
|
413
|
+
]),
|
|
414
|
+
_: 3
|
|
415
|
+
})
|
|
416
|
+
]),
|
|
417
|
+
_: 3
|
|
418
|
+
}, 8, ["open"]);
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
export {
|
|
423
|
+
_sfc_main as _
|
|
424
|
+
};
|
|
@@ -3,7 +3,7 @@ import { DialogRoot, DialogPortal, DialogOverlay, DialogContent, DialogTitle, Di
|
|
|
3
3
|
import { CLOSE_ICON } from "../components/dropdown.js";
|
|
4
4
|
import { _ as _sfc_main$1 } from "./LpIcon-CCnX5_2j.js";
|
|
5
5
|
import { _ as _sfc_main$2 } from "./LpScrollArea-De9DLpa1.js";
|
|
6
|
-
const _hoisted_1 = { class: "fixed inset-0 z-(--z-modal) flex items-center justify-center
|
|
6
|
+
const _hoisted_1 = { class: "fixed inset-0 z-(--z-modal) flex items-center justify-center pointer-events-none" };
|
|
7
7
|
const _hoisted_2 = {
|
|
8
8
|
key: 0,
|
|
9
9
|
class: "flex shrink-0 items-start justify-between gap-4 p-5 pb-3"
|
|
@@ -57,7 +57,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
|
|
|
57
57
|
createVNode(unref(DialogOverlay), { class: "fixed inset-0 z-(--z-overlay) bg-black/50 backdrop-blur-sm data-[state=open]:animate-[fade-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[fade-out_120ms_ease]" }),
|
|
58
58
|
createElementVNode("div", _hoisted_1, [
|
|
59
59
|
createVNode(unref(DialogContent), {
|
|
60
|
-
class: normalizeClass(["pointer-events-auto flex max-h-
|
|
60
|
+
class: normalizeClass(["pointer-events-auto flex max-h-[min(90vh,calc(100dvh-2rem))] min-h-0 flex-col rounded-card border border-line bg-surface-raised shadow-panel outline-none data-[state=open]:animate-[rise-in_var(--duration-medium)_var(--ease-emphasized)] data-[state=closed]:animate-[rise-out_120ms_cubic-bezier(0.4,0,1,1)]", widthClass.value]),
|
|
61
61
|
style: normalizeStyle(__props.width ? { width: __props.width } : void 0)
|
|
62
62
|
}, {
|
|
63
63
|
default: withCtx(() => [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const COMPONENT_NAMES: readonly ["LayoutCanvas", "LayoutNode", "LpAlert", "LpAppShell", "LpAutocomplete", "LpAvatar", "LpBadge", "LpBreadcrumbs", "LpButton", "LpCalendar", "LpCard", "LpCheckbox", "LpCodeBlock", "LpCommandPalette", "LpConfirmDialog", "LpContextMenu", "LpDatePicker", "LpDisclosure", "LpDivider", "LpDrawer", "LpDropdownMenu", "LpEmptyState", "LpFileTree", "LpFileTreeNode", "LpFormField", "LpIcon", "LpInfraNode", "LpInput", "LpLaneNode", "LpLink", "LpLogViewer", "LpModal", "LpNotificationBell", "LpNumberField", "LpOtpInput", "LpPagination", "LpPasswordInput", "LpPhoneInput", "LpPopover", "LpProgress", "LpRadio", "LpRadioGroup", "LpScrollArea", "LpSegmented", "LpSelect", "LpServiceNode", "LpSidebar", "LpSidebarNav", "LpSkeleton", "LpSlider", "LpStat", "LpStepper", "LpSwitch", "LpTable", "LpTableOfContents", "LpTabs", "LpTextarea", "LpThemeSwitcher", "LpTilt", "LpToaster", "LpTooltip", "LpTopologyCanvas", "LpUptimeBar"];
|
|
1
|
+
export declare const COMPONENT_NAMES: readonly ["LayoutCanvas", "LayoutNode", "LpAlert", "LpAppShell", "LpAutocomplete", "LpAvatar", "LpBadge", "LpBreadcrumbs", "LpButton", "LpCalendar", "LpCard", "LpCheckbox", "LpCodeBlock", "LpCommandPalette", "LpConfirmDialog", "LpContextMenu", "LpDatePicker", "LpDisclosure", "LpDivider", "LpDrawer", "LpDropdownMenu", "LpEmptyState", "LpFileTree", "LpFileTreeNode", "LpFormField", "LpIcon", "LpInfraNode", "LpInput", "LpLaneNode", "LpLightbox", "LpLink", "LpLogViewer", "LpModal", "LpNotificationBell", "LpNumberField", "LpOtpInput", "LpPagination", "LpPasswordInput", "LpPhoneInput", "LpPopover", "LpProgress", "LpRadio", "LpRadioGroup", "LpScrollArea", "LpSegmented", "LpSelect", "LpServiceNode", "LpSidebar", "LpSidebarNav", "LpSkeleton", "LpSlider", "LpStat", "LpStepper", "LpSwitch", "LpTable", "LpTableOfContents", "LpTabs", "LpTextarea", "LpThemeSwitcher", "LpTilt", "LpToaster", "LpTooltip", "LpTopologyCanvas", "LpUptimeBar"];
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { LightboxItem } from './lightbox';
|
|
2
|
+
type __VLS_Props = {
|
|
3
|
+
open?: boolean;
|
|
4
|
+
/** Images to page through. */
|
|
5
|
+
items: LightboxItem[];
|
|
6
|
+
/** Index of the visible image (v-model:index). */
|
|
7
|
+
index?: number;
|
|
8
|
+
/** Thumbnail strip along the bottom. Defaults on when there's more than one. */
|
|
9
|
+
filmstrip?: boolean;
|
|
10
|
+
/** Show the rotate control. */
|
|
11
|
+
rotatable?: boolean;
|
|
12
|
+
/** Show the download control. */
|
|
13
|
+
downloadable?: boolean;
|
|
14
|
+
/** Show "copy image" — needs a same-origin or CORS-readable source. */
|
|
15
|
+
copyable?: boolean;
|
|
16
|
+
/** Wrap from the last image back to the first. */
|
|
17
|
+
loop?: boolean;
|
|
18
|
+
/** Largest zoom level. */
|
|
19
|
+
maxZoom?: number;
|
|
20
|
+
};
|
|
21
|
+
type __VLS_Slots = {
|
|
22
|
+
/** Replace the caption block under the image. */
|
|
23
|
+
caption(props: {
|
|
24
|
+
item: LightboxItem;
|
|
25
|
+
index: number;
|
|
26
|
+
total: number;
|
|
27
|
+
}): unknown;
|
|
28
|
+
/** Extra controls in the toolbar, before the close button. */
|
|
29
|
+
actions(props: {
|
|
30
|
+
item: LightboxItem;
|
|
31
|
+
index: number;
|
|
32
|
+
}): unknown;
|
|
33
|
+
};
|
|
34
|
+
declare const __VLS_base: import('vue').DefineComponent<__VLS_Props, {}, {}, {}, {}, import('vue').ComponentOptionsMixin, import('vue').ComponentOptionsMixin, {} & {
|
|
35
|
+
copy: (item: LightboxItem, ok: boolean) => any;
|
|
36
|
+
download: (item: LightboxItem) => any;
|
|
37
|
+
change: (index: number, item: LightboxItem) => any;
|
|
38
|
+
"update:open": (value: boolean) => any;
|
|
39
|
+
"update:index": (value: number) => any;
|
|
40
|
+
}, string, import('vue').PublicProps, Readonly<__VLS_Props> & Readonly<{
|
|
41
|
+
onCopy?: ((item: LightboxItem, ok: boolean) => any) | undefined;
|
|
42
|
+
onDownload?: ((item: LightboxItem) => any) | undefined;
|
|
43
|
+
onChange?: ((index: number, item: LightboxItem) => any) | undefined;
|
|
44
|
+
"onUpdate:open"?: ((value: boolean) => any) | undefined;
|
|
45
|
+
"onUpdate:index"?: ((value: number) => any) | undefined;
|
|
46
|
+
}>, {
|
|
47
|
+
loop: boolean;
|
|
48
|
+
open: boolean;
|
|
49
|
+
copyable: boolean;
|
|
50
|
+
index: number;
|
|
51
|
+
rotatable: boolean;
|
|
52
|
+
downloadable: boolean;
|
|
53
|
+
maxZoom: number;
|
|
54
|
+
}, {}, {}, {}, string, import('vue').ComponentProvideOptions, false, {}, any>;
|
|
55
|
+
declare const __VLS_export: __VLS_WithSlots<typeof __VLS_base, __VLS_Slots>;
|
|
56
|
+
declare const _default: typeof __VLS_export;
|
|
57
|
+
export default _default;
|
|
58
|
+
type __VLS_WithSlots<T, S> = T & {
|
|
59
|
+
new (): {
|
|
60
|
+
$slots: S;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface LightboxItem {
|
|
2
|
+
/** Full-size source. */
|
|
3
|
+
src: string;
|
|
4
|
+
/** Smaller source for the filmstrip; falls back to `src`. */
|
|
5
|
+
thumb?: string;
|
|
6
|
+
/** Caption under the image, and the img alt text. */
|
|
7
|
+
title?: string;
|
|
8
|
+
/** Secondary line under the title (size, date, resolution…). */
|
|
9
|
+
description?: string;
|
|
10
|
+
/** Filename used when downloading; derived from `src` when absent. */
|
|
11
|
+
filename?: string;
|
|
12
|
+
}
|
|
13
|
+
/** Last path segment of a URL, or a fallback — used to name a download. */
|
|
14
|
+
export declare function fileNameOf(item: LightboxItem, fallback?: string): string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
function fileNameOf(item, fallback = "image") {
|
|
2
|
+
if (item.filename) return item.filename;
|
|
3
|
+
try {
|
|
4
|
+
const path = new URL(item.src, "http://x").pathname;
|
|
5
|
+
const tail = decodeURIComponent(path.split("/").pop() ?? "");
|
|
6
|
+
if (tail) return tail;
|
|
7
|
+
} catch {
|
|
8
|
+
}
|
|
9
|
+
return fallback;
|
|
10
|
+
}
|
|
11
|
+
export {
|
|
12
|
+
fileNameOf
|
|
13
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ComputedRef, CSSProperties, Ref } from 'vue';
|
|
2
|
+
export interface UseZoomPanOptions {
|
|
3
|
+
/** Smallest allowed scale. Default 1 (fit — never shrink past the viewport). */
|
|
4
|
+
min?: number;
|
|
5
|
+
/** Largest allowed scale. Default 8. */
|
|
6
|
+
max?: number;
|
|
7
|
+
/** Multiplier per wheel notch. Default 1.15. */
|
|
8
|
+
step?: number;
|
|
9
|
+
/** Scale that a double-click/tap toggles to, from `min`. Default 2. */
|
|
10
|
+
zoomedScale?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface UseZoomPan {
|
|
13
|
+
scale: Ref<number>;
|
|
14
|
+
/** True once zoomed past the minimum — the point at which panning matters. */
|
|
15
|
+
zoomed: ComputedRef<boolean>;
|
|
16
|
+
/** True while a drag is in flight, so the caller can swap the cursor. */
|
|
17
|
+
panning: Ref<boolean>;
|
|
18
|
+
style: ComputedRef<CSSProperties>;
|
|
19
|
+
onWheel: (e: WheelEvent) => void;
|
|
20
|
+
onPointerDown: (e: PointerEvent) => void;
|
|
21
|
+
onPointerMove: (e: PointerEvent) => void;
|
|
22
|
+
onPointerUp: (e: PointerEvent) => void;
|
|
23
|
+
/** Zoom about the viewport centre (buttons, keyboard). */
|
|
24
|
+
zoomBy: (factor: number) => void;
|
|
25
|
+
/** Toggle between fit and `zoomedScale`, optionally about a point. */
|
|
26
|
+
toggleZoom: (origin?: {
|
|
27
|
+
x: number;
|
|
28
|
+
y: number;
|
|
29
|
+
}) => void;
|
|
30
|
+
reset: () => void;
|
|
31
|
+
/** Call with the viewport element so pointer coords can be made relative. */
|
|
32
|
+
setViewport: (el: HTMLElement | null) => void;
|
|
33
|
+
}
|
|
34
|
+
export declare function useZoomPan(options?: UseZoomPanOptions): UseZoomPan;
|