@marianmeres/stuic 3.33.0 → 3.34.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.
@@ -1,63 +1,27 @@
1
1
  <script lang="ts" module>
2
- import {
3
- iconFile,
4
- iconFileBinary,
5
- iconFileCode,
6
- iconFileImage,
7
- iconFileMusic,
8
- iconFilePdf,
9
- iconFileRichtext,
10
- iconFileSlides,
11
- iconFileSpreadsheet,
12
- iconFileText,
13
- iconFileWord,
14
- iconFileZip,
15
- iconArrowLeft as iconPrevious,
16
- iconArrowRight as iconNext,
17
- iconDownload,
18
- iconPlus as iconAdd,
19
- iconTrash as iconDelete,
20
- iconZoomIn,
21
- iconZoomOut,
22
- } from "../../icons/index.js";
23
- import { getFileTypeLabel } from "../../utils/get-file-type-label.js";
24
2
  import { ModalDialog } from "../ModalDialog/index.js";
25
3
  import { createClog } from "@marianmeres/clog";
26
- import { isImage } from "../../utils/is-image.js";
27
- import { isPlainObject } from "../../utils/is-plain-object.js";
28
- import { replaceMap } from "../../utils/replace-map.js";
29
4
  import type { TranslateFn } from "../../types.js";
30
5
  import { twMerge } from "../../utils/tw-merge.js";
31
- import { forceDownload } from "../../utils/force-download.js";
32
- import { tooltip } from "../../actions/index.js";
33
- import { X } from "../X/index.js";
34
- import { preloadImgs } from "../../utils/preload-img.js";
35
- import { fade } from "svelte/transition";
6
+ import { preloadImgs, type PreloadImgOptions } from "../../utils/preload-img.js";
7
+ import { resolveUrl, resolveSrcset } from "../../utils/resolve-url.js";
36
8
 
37
- export type AssetPreviewUrlObj = {
38
- // o
39
- thumb: string | URL;
40
- // used in modal preview
41
- full: string | URL;
42
- // (potentially extra high res) used for download
43
- original: string | URL;
44
- };
9
+ export type {
10
+ AssetPreviewUrlObj,
11
+ AssetPreview,
12
+ AssetArea,
13
+ } from "./_internal/assets-preview-types.js";
14
+ export { getAssetIcon } from "./_internal/assets-preview-utils.js";
45
15
 
46
- export interface AssetPreview {
47
- url: AssetPreviewUrlObj;
48
- name?: string;
49
- type?: string;
50
- }
51
-
52
- interface AssetPreviewNormalized extends AssetPreview {
53
- name: string;
54
- type: string;
55
- ext: string;
56
- isImage: boolean;
57
- }
16
+ // re-import for local use
17
+ import type { AssetArea, AssetPreview } from "./_internal/assets-preview-types.js";
18
+ import type { AssetPreviewNormalized } from "./_internal/assets-preview-types.js";
19
+ import { normalizeInput, t_default } from "./_internal/assets-preview-utils.js";
58
20
 
59
21
  export interface Props {
60
22
  assets: string[] | AssetPreview[];
23
+ /** Fallback base URL for resolving relative asset URLs */
24
+ baseUrl?: string;
61
25
  classControls?: string;
62
26
  //
63
27
  modalClassDialog?: string;
@@ -77,358 +41,80 @@
77
41
  noName?: boolean;
78
42
  /** When true (default), panning is clamped to keep image within bounds */
79
43
  clampPan?: boolean;
80
- }
81
-
82
- export function getAssetIcon(ext?: string) {
83
- const map: Record<string, CallableFunction> = {
84
- archive: iconFileZip,
85
- audio: iconFileMusic,
86
- binary: iconFileBinary,
87
- code: iconFileCode,
88
- doc: iconFileWord,
89
- image: iconFileImage,
90
- pdf: iconFilePdf,
91
- presentation: iconFileSlides,
92
- richtext: iconFileRichtext,
93
- spreadsheet: iconFileSpreadsheet,
94
- text: iconFileText,
95
- unknown: iconFile,
96
- };
97
- return map[getFileTypeLabel(ext ?? "unknown")] ?? iconFile;
98
- }
99
-
100
- // i18n ready
101
- function t_default(
102
- k: string,
103
- values: false | null | undefined | Record<string, string | number> = null,
104
- fallback: string | boolean = "",
105
- i18nSpanWrap: boolean = true
106
- ) {
107
- const m: Record<string, string> = {
108
- unable_to_preview: "Unable to preview",
109
- download: "Download",
110
- close: "Close",
111
- zoom_in: "Zoom in",
112
- zoom_out: "Zoom out",
113
- delete: "Delete",
114
- };
115
- let out = m[k] ?? fallback ?? k;
116
-
117
- return isPlainObject(values) ? replaceMap(out, values as any) : out;
118
- }
119
-
120
- // naive best-effort
121
- function ext(name: string): string {
122
- const _ext = name.split(".").at(-1) ?? "";
123
- return _ext ? `.${_ext}` : "";
124
- }
125
-
126
- function normalizeInput(input: string | AssetPreview): AssetPreviewNormalized | null {
127
- const asset: AssetPreviewNormalized = {
128
- name: "",
129
- type: "",
130
- url: { full: "", thumb: "", original: "" },
131
- isImage: false,
132
- ext: "",
133
- };
134
- if (typeof input === "string") {
135
- asset.url.full = input;
136
- } else {
137
- // Handle AssetPreview object
138
- asset.url = { ...input.url };
139
- asset.name = input.name ?? "";
140
- asset.type = input.type ?? "";
141
- }
142
-
143
- if (!asset.url.full) {
144
- return null;
145
- }
146
-
147
- asset.url.full = new URL(
148
- asset.url.full,
149
- globalThis.location?.href ?? "http://placeholder"
150
- );
151
-
152
- // Use "full" also as "thumb" and "original", if not provided
153
- asset.url.thumb ||= asset.url.full;
154
- asset.url.original ||= asset.url.full;
155
-
156
- // best-effort..
157
- if (!asset.name) {
158
- asset.name = asset.url.full.pathname.split("/").at(-1) ?? "";
159
- }
160
-
161
- // best-effort..
162
- if (!asset.type) {
163
- asset.type = getFileTypeLabel(ext(asset.name) ?? "unknown");
164
- }
165
-
166
- asset.ext = ext(asset.name);
167
- asset.isImage = isImage(asset.ext);
168
-
169
- return asset;
44
+ /** Do not offer download if truthy (default false) */
45
+ noDownload?: boolean;
46
+ /** Hide prev/next arrow buttons */
47
+ noPrevNext?: boolean;
48
+ /** Disable all zooming (buttons + gestures) */
49
+ noZoom?: boolean;
50
+ /** Hide zoom buttons only (gestures still work) */
51
+ noZoomButtons?: boolean;
52
+ /** Never show dots (even if less than 10) */
53
+ noDots?: boolean;
54
+ /** Never show "x / y" meta */
55
+ noCurrentOfTotal?: boolean;
56
+ /** Callback when a clickable area on an image is clicked */
57
+ onAreaClick?: (data: { area: AssetArea; asset: AssetPreviewNormalized }) => void;
170
58
  }
171
59
  </script>
172
60
 
173
61
  <script lang="ts">
174
- import Button from "../Button/Button.svelte";
62
+ import AssetsPreviewContent from "./_internal/AssetsPreviewContent.svelte";
175
63
  const clog = createClog("AssetsPreview", { color: "auto" });
176
64
 
177
65
  let {
178
66
  modalClassDialog = "",
179
67
  modalClass = "",
180
68
  assets: _assets,
69
+ baseUrl,
181
70
  t = t_default,
182
71
  classControls = "",
183
72
  onDelete,
73
+ onAreaClick,
184
74
  noName,
185
75
  clampPan = false,
76
+ noDownload = false,
77
+ noPrevNext = false,
78
+ noZoom = false,
79
+ noZoomButtons = false,
80
+ noDots = false,
81
+ noCurrentOfTotal = false,
186
82
  }: Props = $props();
187
83
 
188
84
  let assets: AssetPreviewNormalized[] = $derived(
189
85
  (_assets ?? []).map(normalizeInput).filter(Boolean) as AssetPreviewNormalized[]
190
86
  );
191
87
  let previewIdx = $state<number>(0);
88
+ let _openIdx: number | undefined = $state();
192
89
  let modal: ModalDialog | undefined = $state();
193
- let dotTooltip: string | undefined = $state();
194
-
195
- // Zoom state
196
- const ZOOM_LEVELS = [1, 1.5, 2, 3, 4] as const;
197
- const MIN_ZOOM = ZOOM_LEVELS[0];
198
- const MAX_ZOOM = ZOOM_LEVELS[ZOOM_LEVELS.length - 1];
199
- let zoomLevelIdx = $state(0);
200
-
201
- // Pinch zoom state
202
- let isPinching = $state(false);
203
- let initialPinchDistance = 0;
204
- let initialPinchZoom = 1;
205
- let continuousZoom = $state(1);
206
-
207
- // Use continuous zoom during pinch, discrete levels otherwise
208
- let zoomLevel = $derived(isPinching ? continuousZoom : ZOOM_LEVELS[zoomLevelIdx]);
209
-
210
- // Pan state
211
- let isPanning = $state(false);
212
- let panX = $state(0);
213
- let panY = $state(0);
214
- let startPanX = 0;
215
- let startPanY = 0;
216
- let startMouseX = 0;
217
- let startMouseY = 0;
218
-
219
- // Image and container dimensions for pan clamping
220
- let imgEl: HTMLImageElement | null = null;
221
- let containerEl: HTMLDivElement | null = $state(null);
222
-
223
- const BUTTON_CLS = "stuic-assets-preview-control pointer-events-auto p-0!";
224
-
225
- const BUTTON_PROPS = {
226
- aspect1: true,
227
- variant: "soft",
228
- roundedFull: true,
229
- };
90
+ let content: AssetsPreviewContent | undefined = $state();
230
91
 
231
92
  $effect(() => {
232
93
  const visible = modal?.visibility().visible;
233
94
  if (visible) {
234
- // Reset preview index on modal open
235
- previewIdx = 0;
95
+ // Use the index from open(index) if provided, otherwise reset to 0
96
+ previewIdx = _openIdx ?? 0;
97
+ _openIdx = undefined;
236
98
 
237
99
  // perhaps we should have some upper limit here...
238
- const toPreload = (assets ?? [])
239
- .map((asset) => (asset.isImage ? String(asset.url.full) : ""))
240
- .filter(Boolean);
100
+ const toPreload: PreloadImgOptions[] = (assets ?? [])
101
+ .filter((asset) => asset.isImage)
102
+ .map((asset) => ({
103
+ src: resolveUrl(String(asset.url.full), baseUrl),
104
+ srcset: resolveSrcset(asset.srcset ?? "", baseUrl) || undefined,
105
+ sizes: asset.sizes,
106
+ }));
241
107
 
242
108
  clog.debug("going to (maybe) preload", toPreload);
243
- preloadImgs(toPreload.map((src) => ({ src })));
109
+ if (toPreload.length) preloadImgs(toPreload);
244
110
  } else {
245
111
  // Reset zoom when modal closes
246
- resetZoom();
112
+ content?.resetZoom();
247
113
  }
248
114
  });
249
115
 
250
- // Wheel zoom handler
251
- function handleWheel(e: WheelEvent) {
252
- e.preventDefault();
253
- if (e.deltaY > 0) {
254
- zoomOut();
255
- } else {
256
- zoomIn();
257
- }
258
- }
259
-
260
- // Svelte action for pan event listeners - guaranteed to run when element is created
261
- function pannable(node: HTMLImageElement) {
262
- imgEl = node;
263
- node.addEventListener("mousedown", panStart);
264
- node.addEventListener("touchstart", panStart, { passive: false });
265
- node.addEventListener("wheel", handleWheel, { passive: false });
266
-
267
- document.addEventListener("mousemove", panMove);
268
- document.addEventListener("mouseup", panEnd);
269
- document.addEventListener("touchmove", panMove, { passive: false });
270
- document.addEventListener("touchend", panEnd);
271
- document.addEventListener("touchcancel", panEnd);
272
-
273
- return {
274
- destroy() {
275
- imgEl = null;
276
- node.removeEventListener("mousedown", panStart);
277
- node.removeEventListener("touchstart", panStart);
278
- node.removeEventListener("wheel", handleWheel);
279
- document.removeEventListener("mousemove", panMove);
280
- document.removeEventListener("mouseup", panEnd);
281
- document.removeEventListener("touchmove", panMove);
282
- document.removeEventListener("touchend", panEnd);
283
- document.removeEventListener("touchcancel", panEnd);
284
- },
285
- };
286
- }
287
-
288
- // Clamp pan values to keep image within bounds
289
- function getClampedPan(newPanX: number, newPanY: number): { x: number; y: number } {
290
- if (!imgEl || !containerEl) return { x: newPanX, y: newPanY };
291
-
292
- const imgRect = imgEl.getBoundingClientRect();
293
- const containerRect = containerEl.getBoundingClientRect();
294
-
295
- // Calculate the scaled image dimensions
296
- const scaledWidth = imgRect.width; // already includes transform scale
297
- const scaledHeight = imgRect.height;
298
-
299
- // Calculate max pan distance (how much the scaled image exceeds the container)
300
- // Divide by zoomLevel because translate values are applied before scale in our transform
301
- const maxPanX = Math.max(0, (scaledWidth - containerRect.width) / 2 / zoomLevel);
302
- const maxPanY = Math.max(0, (scaledHeight - containerRect.height) / 2 / zoomLevel);
303
-
304
- return {
305
- x: Math.max(-maxPanX, Math.min(maxPanX, newPanX)),
306
- y: Math.max(-maxPanY, Math.min(maxPanY, newPanY)),
307
- };
308
- }
309
-
310
- // Zoom functions
311
- function zoomIn() {
312
- if (zoomLevelIdx < ZOOM_LEVELS.length - 1) {
313
- zoomLevelIdx++;
314
- }
315
- }
316
-
317
- function zoomOut() {
318
- if (zoomLevelIdx > 0) {
319
- zoomLevelIdx--;
320
- // Reset pan when zooming out to 1x
321
- if (zoomLevelIdx === 0) {
322
- panX = 0;
323
- panY = 0;
324
- }
325
- }
326
- }
327
-
328
- function resetZoom() {
329
- zoomLevelIdx = 0;
330
- continuousZoom = 1;
331
- panX = 0;
332
- panY = 0;
333
- isPinching = false;
334
- }
335
-
336
- // Pinch zoom helpers
337
- function getDistance(touch1: Touch, touch2: Touch): number {
338
- const dx = touch1.clientX - touch2.clientX;
339
- const dy = touch1.clientY - touch2.clientY;
340
- return Math.hypot(dx, dy);
341
- }
342
-
343
- function findNearestZoomLevelIdx(zoom: number): number {
344
- let nearestIdx = 0;
345
- let minDiff = Math.abs(ZOOM_LEVELS[0] - zoom);
346
- for (let i = 1; i < ZOOM_LEVELS.length; i++) {
347
- const diff = Math.abs(ZOOM_LEVELS[i] - zoom);
348
- if (diff < minDiff) {
349
- minDiff = diff;
350
- nearestIdx = i;
351
- }
352
- }
353
- return nearestIdx;
354
- }
355
-
356
- // Pan/drag handlers
357
- function panStart(e: MouseEvent | TouchEvent) {
358
- // Detect two-finger pinch gesture
359
- if ("touches" in e && e.touches.length === 2) {
360
- e.preventDefault();
361
- isPinching = true;
362
- isPanning = false;
363
- initialPinchDistance = getDistance(e.touches[0], e.touches[1]);
364
- initialPinchZoom = continuousZoom;
365
- return;
366
- }
367
-
368
- // Single-finger pan (only when zoomed in)
369
- if (zoomLevel <= 1) return;
370
- e.preventDefault();
371
- isPanning = true;
372
-
373
- const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
374
- const clientY = "touches" in e ? e.touches[0].clientY : e.clientY;
375
-
376
- startMouseX = clientX;
377
- startMouseY = clientY;
378
- startPanX = panX;
379
- startPanY = panY;
380
- }
381
-
382
- function panMove(e: MouseEvent | TouchEvent) {
383
- // Handle pinch zoom
384
- if ("touches" in e && e.touches.length === 2 && isPinching) {
385
- e.preventDefault();
386
- const currentDistance = getDistance(e.touches[0], e.touches[1]);
387
- const scale = currentDistance / initialPinchDistance;
388
- continuousZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, initialPinchZoom * scale));
389
- return;
390
- }
391
-
392
- // Handle single-finger pan
393
- if (!isPanning) return;
394
- e.preventDefault();
395
-
396
- const clientX = "touches" in e ? e.touches[0].clientX : e.clientX;
397
- const clientY = "touches" in e ? e.touches[0].clientY : e.clientY;
398
-
399
- const newPanX = startPanX + (clientX - startMouseX);
400
- const newPanY = startPanY + (clientY - startMouseY);
401
-
402
- if (clampPan) {
403
- const clamped = getClampedPan(newPanX, newPanY);
404
- panX = clamped.x;
405
- panY = clamped.y;
406
- } else {
407
- panX = newPanX;
408
- panY = newPanY;
409
- }
410
- }
411
-
412
- function panEnd() {
413
- // Handle pinch end - snap to nearest discrete level
414
- if (isPinching) {
415
- isPinching = false;
416
- zoomLevelIdx = findNearestZoomLevelIdx(continuousZoom);
417
- continuousZoom = ZOOM_LEVELS[zoomLevelIdx];
418
- // Reset pan when zoomed out to 1x
419
- if (zoomLevelIdx === 0) {
420
- panX = 0;
421
- panY = 0;
422
- }
423
- return;
424
- }
425
- isPanning = false;
426
- }
427
-
428
116
  export function open(index?: number) {
429
- if (typeof index === "number") {
430
- previewIdx = index;
431
- }
117
+ _openIdx = typeof index === "number" ? index : undefined;
432
118
  modal?.open();
433
119
  }
434
120
 
@@ -449,33 +135,16 @@
449
135
  export function setOpener(opener?: null | HTMLElement) {
450
136
  modal?.setOpener(opener);
451
137
  }
452
-
453
- function preview_previous() {
454
- previewIdx = (previewIdx - 1 + assets.length) % assets.length;
455
- resetZoom();
456
- }
457
-
458
- function preview_next() {
459
- previewIdx = (previewIdx + 1) % assets.length;
460
- resetZoom();
461
- }
462
-
463
- function preview(idx: number) {
464
- previewIdx = idx % assets.length;
465
- }
466
-
467
- const ICON_SIZE = 24;
468
- // $inspect(assets).with(clog);
469
138
  </script>
470
139
 
471
140
  <!-- this must be on window as we're catching any typing anywhere -->
472
141
  <svelte:window
473
142
  onkeydown={(e) => {
474
- if (modal?.visibility().visible) {
143
+ if (modal?.visibility().visible && !noPrevNext) {
475
144
  if (["ArrowRight"].includes(e.key)) {
476
- preview_next();
145
+ content?.next();
477
146
  } else if (["ArrowLeft"].includes(e.key)) {
478
- preview_previous();
147
+ content?.previous();
479
148
  }
480
149
  }
481
150
  }}
@@ -492,180 +161,24 @@
492
161
  modalClass
493
162
  )}
494
163
  >
495
- {@const previewAsset = assets?.[previewIdx]}
496
- {#if previewAsset}
497
- <!-- <pre>{JSON.stringify(previewAsset)}</pre> -->
498
- {#if previewAsset.isImage}
499
- <div
500
- bind:this={containerEl}
501
- class="w-full h-full overflow-hidden flex items-center justify-center"
502
- >
503
- <img
504
- use:pannable
505
- src={String(previewAsset.url.full)}
506
- class="max-w-full max-h-full object-scale-down select-none"
507
- class:cursor-grab={zoomLevel > 1 && !isPanning}
508
- class:cursor-grabbing={isPanning}
509
- alt={previewAsset?.name}
510
- style:transform="scale({zoomLevel}) translate({panX / zoomLevel}px, {panY /
511
- zoomLevel}px)"
512
- style:transform-origin="center center"
513
- draggable="false"
514
- />
515
- </div>
516
- {:else}
517
- <div>
518
- <div>
519
- {@html getAssetIcon(previewAsset.ext)({
520
- size: 32,
521
- class: "mx-auto",
522
- })}
523
- </div>
524
- <div class="text-(--stuic-color-muted-foreground) mt-4">
525
- {t("unable_to_preview")}
526
- </div>
527
- </div>
528
- {/if}
529
-
530
- {#if assets?.length > 1}
531
- <div
532
- class="absolute inset-0 flex items-center justify-between pointer-events-none"
533
- >
534
- <!-- class={twMerge("p-4 aspect-square pointer-events-auto", classControls)} -->
535
- <Button
536
- class={twMerge(BUTTON_CLS, "ml-4", classControls)}
537
- onclick={preview_previous}
538
- type="button"
539
- {...BUTTON_PROPS}
540
- >
541
- <!-- <span class="stuic-assets-preview-control-nav p-3 block"> -->
542
- {@html iconPrevious({ size: ICON_SIZE })}
543
- <!-- </span> -->
544
- </Button>
545
-
546
- <!-- class={twMerge("p-4 aspect-square pointer-events-auto", classControls)} -->
547
- <Button
548
- class={twMerge(BUTTON_CLS, "mr-4", classControls)}
549
- onclick={preview_next}
550
- type="button"
551
- {...BUTTON_PROPS}
552
- >
553
- <!-- <span class="stuic-assets-preview-control-nav p-3 block"> -->
554
- {@html iconNext({ size: ICON_SIZE })}
555
- <!-- </span> -->
556
- </Button>
557
- </div>
558
- {/if}
559
-
560
- <div class="absolute top-4 left-4 right-4 flex items-center justify-between gap-3">
561
- {#if !noName && previewAsset?.name}
562
- <span class="stuic-assets-preview-label truncate px-1">
563
- {previewAsset?.name}
564
- </span>
565
- {:else}
566
- <span></span>
567
- {/if}
568
- <div class="flex items-center space-x-3 shrink-0">
569
- {#if previewAsset.isImage}
570
- <Button
571
- class={twMerge(BUTTON_CLS, classControls)}
572
- type="button"
573
- onclick={zoomOut}
574
- disabled={zoomLevelIdx === 0}
575
- aria-label={t("zoom_out")}
576
- tooltip={t("zoom_out")}
577
- {...BUTTON_PROPS}
578
- >
579
- {@html iconZoomOut({ size: ICON_SIZE })}
580
- </Button>
581
-
582
- <Button
583
- class={twMerge(BUTTON_CLS, classControls)}
584
- type="button"
585
- onclick={zoomIn}
586
- disabled={zoomLevelIdx === ZOOM_LEVELS.length - 1}
587
- aria-label={t("zoom_in")}
588
- tooltip={t("zoom_in")}
589
- {...BUTTON_PROPS}
590
- >
591
- {@html iconZoomIn({ size: ICON_SIZE })}
592
- </Button>
593
- {/if}
594
-
595
- {#if typeof onDelete === "function"}
596
- <Button
597
- class={twMerge(BUTTON_CLS, classControls)}
598
- type="button"
599
- onclick={() => onDelete(previewAsset, previewIdx, { close })}
600
- aria-label={t("delete")}
601
- tooltip={t("delete")}
602
- {...BUTTON_PROPS}
603
- >
604
- {@html iconDelete({ size: ICON_SIZE })}
605
- </Button>
606
- {/if}
607
-
608
- <Button
609
- class={twMerge(BUTTON_CLS, classControls)}
610
- type="button"
611
- onclick={(e) => {
612
- e.preventDefault();
613
- forceDownload(String(previewAsset.url.original), previewAsset?.name || "");
614
- }}
615
- aria-label={t("download")}
616
- tooltip={t("download")}
617
- {...BUTTON_PROPS}
618
- >
619
- {@html iconDownload({ size: ICON_SIZE })}
620
- </Button>
621
-
622
- <Button
623
- class={twMerge(BUTTON_CLS, classControls)}
624
- onclick={modal?.close}
625
- aria-label={t("close")}
626
- type="button"
627
- tooltip={t("close")}
628
- {...BUTTON_PROPS}
629
- x
630
- />
631
- </div>
632
- </div>
633
-
634
- {#if assets.length > 1}
635
- {#if !noName && dotTooltip}
636
- <div
637
- class="absolute bottom-10 left-0 right-0 text-center"
638
- transition:fade={{ duration: 100 }}
639
- >
640
- <span class="stuic-assets-preview-label p-1">
641
- {dotTooltip}
642
- </span>
643
- </div>
644
- {/if}
645
- <div class="absolute bottom-4 left-1/2 -translate-x-1/2 flex items-center gap-3">
646
- {#each assets as _, i}
647
- <!-- svelte-ignore a11y_mouse_events_have_key_events -->
648
- <button
649
- type="button"
650
- class={twMerge(
651
- "stuic-assets-preview-dot",
652
- i === previewIdx ? "active" : ""
653
- )}
654
- onclick={() => {
655
- previewIdx = i;
656
- resetZoom();
657
- }}
658
- aria-label={assets[i]?.name}
659
- onmouseover={() => {
660
- dotTooltip = assets[i]?.name;
661
- }}
662
- onmouseout={() => {
663
- dotTooltip = undefined;
664
- }}
665
- ></button>
666
- {/each}
667
- </div>
668
- {/if}
669
- {/if}
164
+ <AssetsPreviewContent
165
+ bind:this={content}
166
+ bind:previewIdx
167
+ {assets}
168
+ {baseUrl}
169
+ {classControls}
170
+ {t}
171
+ {onDelete}
172
+ {noName}
173
+ {clampPan}
174
+ {noDownload}
175
+ {noPrevNext}
176
+ {noZoom}
177
+ {noZoomButtons}
178
+ {noDots}
179
+ {noCurrentOfTotal}
180
+ {onAreaClick}
181
+ onClose={() => modal?.close()}
182
+ />
670
183
  </ModalDialog>
671
184
  {/if}