@marianmeres/stuic 2.43.0 → 2.45.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.
|
@@ -75,6 +75,8 @@
|
|
|
75
75
|
) => void;
|
|
76
76
|
/** optional "do not display file name" switch flag */
|
|
77
77
|
noName?: boolean;
|
|
78
|
+
/** When true (default), panning is clamped to keep image within bounds */
|
|
79
|
+
clampPan?: boolean;
|
|
78
80
|
}
|
|
79
81
|
|
|
80
82
|
export function getAssetIcon(ext?: string) {
|
|
@@ -181,6 +183,7 @@
|
|
|
181
183
|
classControls = "",
|
|
182
184
|
onDelete,
|
|
183
185
|
noName,
|
|
186
|
+
clampPan = false,
|
|
184
187
|
}: Props = $props();
|
|
185
188
|
|
|
186
189
|
let assets: AssetPreviewNormalized[] = $derived(
|
|
@@ -192,8 +195,18 @@
|
|
|
192
195
|
|
|
193
196
|
// Zoom state
|
|
194
197
|
const ZOOM_LEVELS = [1, 1.5, 2, 3, 4] as const;
|
|
198
|
+
const MIN_ZOOM = ZOOM_LEVELS[0];
|
|
199
|
+
const MAX_ZOOM = ZOOM_LEVELS[ZOOM_LEVELS.length - 1];
|
|
195
200
|
let zoomLevelIdx = $state(0);
|
|
196
|
-
|
|
201
|
+
|
|
202
|
+
// Pinch zoom state
|
|
203
|
+
let isPinching = $state(false);
|
|
204
|
+
let initialPinchDistance = 0;
|
|
205
|
+
let initialPinchZoom = 1;
|
|
206
|
+
let continuousZoom = $state(1);
|
|
207
|
+
|
|
208
|
+
// Use continuous zoom during pinch, discrete levels otherwise
|
|
209
|
+
let zoomLevel = $derived(isPinching ? continuousZoom : ZOOM_LEVELS[zoomLevelIdx]);
|
|
197
210
|
|
|
198
211
|
// Pan state
|
|
199
212
|
let isPanning = $state(false);
|
|
@@ -229,11 +242,22 @@
|
|
|
229
242
|
}
|
|
230
243
|
});
|
|
231
244
|
|
|
245
|
+
// Wheel zoom handler
|
|
246
|
+
function handleWheel(e: WheelEvent) {
|
|
247
|
+
e.preventDefault();
|
|
248
|
+
if (e.deltaY > 0) {
|
|
249
|
+
zoomOut();
|
|
250
|
+
} else {
|
|
251
|
+
zoomIn();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
232
255
|
// Svelte action for pan event listeners - guaranteed to run when element is created
|
|
233
256
|
function pannable(node: HTMLImageElement) {
|
|
234
257
|
imgEl = node;
|
|
235
258
|
node.addEventListener("mousedown", panStart);
|
|
236
259
|
node.addEventListener("touchstart", panStart, { passive: false });
|
|
260
|
+
node.addEventListener("wheel", handleWheel, { passive: false });
|
|
237
261
|
|
|
238
262
|
document.addEventListener("mousemove", panMove);
|
|
239
263
|
document.addEventListener("mouseup", panEnd);
|
|
@@ -246,6 +270,7 @@
|
|
|
246
270
|
imgEl = null;
|
|
247
271
|
node.removeEventListener("mousedown", panStart);
|
|
248
272
|
node.removeEventListener("touchstart", panStart);
|
|
273
|
+
node.removeEventListener("wheel", handleWheel);
|
|
249
274
|
document.removeEventListener("mousemove", panMove);
|
|
250
275
|
document.removeEventListener("mouseup", panEnd);
|
|
251
276
|
document.removeEventListener("touchmove", panMove);
|
|
@@ -256,7 +281,7 @@
|
|
|
256
281
|
}
|
|
257
282
|
|
|
258
283
|
// Clamp pan values to keep image within bounds
|
|
259
|
-
function
|
|
284
|
+
function getClampedPan(newPanX: number, newPanY: number): { x: number; y: number } {
|
|
260
285
|
if (!imgEl || !containerEl) return { x: newPanX, y: newPanY };
|
|
261
286
|
|
|
262
287
|
const imgRect = imgEl.getBoundingClientRect();
|
|
@@ -297,12 +322,45 @@
|
|
|
297
322
|
|
|
298
323
|
function resetZoom() {
|
|
299
324
|
zoomLevelIdx = 0;
|
|
325
|
+
continuousZoom = 1;
|
|
300
326
|
panX = 0;
|
|
301
327
|
panY = 0;
|
|
328
|
+
isPinching = false;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Pinch zoom helpers
|
|
332
|
+
function getDistance(touch1: Touch, touch2: Touch): number {
|
|
333
|
+
const dx = touch1.clientX - touch2.clientX;
|
|
334
|
+
const dy = touch1.clientY - touch2.clientY;
|
|
335
|
+
return Math.hypot(dx, dy);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function findNearestZoomLevelIdx(zoom: number): number {
|
|
339
|
+
let nearestIdx = 0;
|
|
340
|
+
let minDiff = Math.abs(ZOOM_LEVELS[0] - zoom);
|
|
341
|
+
for (let i = 1; i < ZOOM_LEVELS.length; i++) {
|
|
342
|
+
const diff = Math.abs(ZOOM_LEVELS[i] - zoom);
|
|
343
|
+
if (diff < minDiff) {
|
|
344
|
+
minDiff = diff;
|
|
345
|
+
nearestIdx = i;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return nearestIdx;
|
|
302
349
|
}
|
|
303
350
|
|
|
304
351
|
// Pan/drag handlers
|
|
305
352
|
function panStart(e: MouseEvent | TouchEvent) {
|
|
353
|
+
// Detect two-finger pinch gesture
|
|
354
|
+
if ("touches" in e && e.touches.length === 2) {
|
|
355
|
+
e.preventDefault();
|
|
356
|
+
isPinching = true;
|
|
357
|
+
isPanning = false;
|
|
358
|
+
initialPinchDistance = getDistance(e.touches[0], e.touches[1]);
|
|
359
|
+
initialPinchZoom = continuousZoom;
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Single-finger pan (only when zoomed in)
|
|
306
364
|
if (zoomLevel <= 1) return;
|
|
307
365
|
e.preventDefault();
|
|
308
366
|
isPanning = true;
|
|
@@ -317,6 +375,16 @@
|
|
|
317
375
|
}
|
|
318
376
|
|
|
319
377
|
function panMove(e: MouseEvent | TouchEvent) {
|
|
378
|
+
// Handle pinch zoom
|
|
379
|
+
if ("touches" in e && e.touches.length === 2 && isPinching) {
|
|
380
|
+
e.preventDefault();
|
|
381
|
+
const currentDistance = getDistance(e.touches[0], e.touches[1]);
|
|
382
|
+
const scale = currentDistance / initialPinchDistance;
|
|
383
|
+
continuousZoom = Math.max(MIN_ZOOM, Math.min(MAX_ZOOM, initialPinchZoom * scale));
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Handle single-finger pan
|
|
320
388
|
if (!isPanning) return;
|
|
321
389
|
e.preventDefault();
|
|
322
390
|
|
|
@@ -326,12 +394,29 @@
|
|
|
326
394
|
const newPanX = startPanX + (clientX - startMouseX);
|
|
327
395
|
const newPanY = startPanY + (clientY - startMouseY);
|
|
328
396
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
397
|
+
if (clampPan) {
|
|
398
|
+
const clamped = getClampedPan(newPanX, newPanY);
|
|
399
|
+
panX = clamped.x;
|
|
400
|
+
panY = clamped.y;
|
|
401
|
+
} else {
|
|
402
|
+
panX = newPanX;
|
|
403
|
+
panY = newPanY;
|
|
404
|
+
}
|
|
332
405
|
}
|
|
333
406
|
|
|
334
407
|
function panEnd() {
|
|
408
|
+
// Handle pinch end - snap to nearest discrete level
|
|
409
|
+
if (isPinching) {
|
|
410
|
+
isPinching = false;
|
|
411
|
+
zoomLevelIdx = findNearestZoomLevelIdx(continuousZoom);
|
|
412
|
+
continuousZoom = ZOOM_LEVELS[zoomLevelIdx];
|
|
413
|
+
// Reset pan when zoomed out to 1x
|
|
414
|
+
if (zoomLevelIdx === 0) {
|
|
415
|
+
panX = 0;
|
|
416
|
+
panY = 0;
|
|
417
|
+
}
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
335
420
|
isPanning = false;
|
|
336
421
|
}
|
|
337
422
|
|
|
@@ -394,7 +479,7 @@
|
|
|
394
479
|
<Modal
|
|
395
480
|
bind:this={modal}
|
|
396
481
|
onEscape={modal?.close}
|
|
397
|
-
classBackdrop="p-
|
|
482
|
+
classBackdrop="p-2 md:p-2 {modalClassBackdrop}"
|
|
398
483
|
classInner="max-w-full h-full {modalClassInner}"
|
|
399
484
|
class="max-h-full md:max-h-full rounded-lg {modalClass}"
|
|
400
485
|
classMain="flex items-center justify-center relative stuic-assets-preview stuic-assets-preview-open {modalClassMain}"
|
|
@@ -24,6 +24,8 @@ export interface Props {
|
|
|
24
24
|
}) => void;
|
|
25
25
|
/** optional "do not display file name" switch flag */
|
|
26
26
|
noName?: boolean;
|
|
27
|
+
/** When true (default), panning is clamped to keep image within bounds */
|
|
28
|
+
clampPan?: boolean;
|
|
27
29
|
}
|
|
28
30
|
export declare function getAssetIcon(ext?: string): CallableFunction;
|
|
29
31
|
declare const AssetsPreview: import("svelte").Component<Props, {
|