@for-the-people-initiative/design-system 1.3.3 → 1.3.4
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/css/tokens.css +19 -0
- package/dist/scss/_tokens.scss +21 -0
- package/dist/ts/tokens.ts +20 -0
- package/package.json +1 -1
- package/src/components/Galleria/Galleria.scss +133 -1
- package/src/components/Galleria/Galleria.vue +228 -16
- package/src/components/Lightbox/Lightbox.scss +264 -0
- package/src/components/Lightbox/Lightbox.vue +274 -0
- package/src/types/index.ts +32 -0
- package/tokens/components/lightbox.json +119 -0
package/dist/css/tokens.css
CHANGED
|
@@ -1169,6 +1169,25 @@
|
|
|
1169
1169
|
--knob-focus-ringColor: #f97316; /* Focus ring color */
|
|
1170
1170
|
--knob-focus-ringOffset: 2px; /* Focus ring offset */
|
|
1171
1171
|
--knob-transition-duration: 100ms; /* Value change transition */
|
|
1172
|
+
--lightbox-overlay-background: rgba(0, 0, 0, 0.92); /* Lightbox overlay backdrop color */
|
|
1173
|
+
--lightbox-zIndex-overlay: 2000; /* Overlay z-index (above dialogs) */
|
|
1174
|
+
--lightbox-nav-size: 48px; /* Navigation button size */
|
|
1175
|
+
--lightbox-nav-iconSize: 24px; /* Navigation icon size */
|
|
1176
|
+
--lightbox-nav-color-default: rgba(255, 255, 255, 0.8); /* Nav button color */
|
|
1177
|
+
--lightbox-nav-color-hover: #ffffff; /* Nav button hover color */
|
|
1178
|
+
--lightbox-nav-background-default: rgba(255, 255, 255, 0.1); /* Nav button background */
|
|
1179
|
+
--lightbox-nav-background-hover: rgba(255, 255, 255, 0.2); /* Nav button hover background */
|
|
1180
|
+
--lightbox-nav-radius: 9999px; /* Nav button border radius */
|
|
1181
|
+
--lightbox-closeButton-size: 40px; /* Close button size */
|
|
1182
|
+
--lightbox-closeButton-iconSize: 20px; /* Close icon size */
|
|
1183
|
+
--lightbox-counter-color: rgba(255, 255, 255, 0.7); /* Counter text color */
|
|
1184
|
+
--lightbox-counter-fontSize: 14px; /* Counter font size */
|
|
1185
|
+
--lightbox-thumbnail-size: 64px; /* Thumbnail width/height */
|
|
1186
|
+
--lightbox-thumbnail-gap: 6px; /* Gap between thumbnails */
|
|
1187
|
+
--lightbox-thumbnail-radius: 5px; /* Thumbnail border radius */
|
|
1188
|
+
--lightbox-thumbnail-border-active: #ffffff; /* Active thumbnail border color */
|
|
1189
|
+
--lightbox-thumbnail-opacity-default: 0.5; /* Inactive thumbnail opacity */
|
|
1190
|
+
--lightbox-thumbnail-opacity-active: 1; /* Active thumbnail opacity */
|
|
1172
1191
|
--listbox-padding: 6px; /* Container padding */
|
|
1173
1192
|
--listbox-radius: 5px; /* Container border radius */
|
|
1174
1193
|
--listbox-border-width: 1px;
|
package/dist/scss/_tokens.scss
CHANGED
|
@@ -1283,6 +1283,27 @@ $knob-focus-ringColor: var(--knob-focus-ringColor); // Focus ring color
|
|
|
1283
1283
|
$knob-focus-ringOffset: var(--knob-focus-ringOffset); // Focus ring offset
|
|
1284
1284
|
$knob-transition-duration: var(--knob-transition-duration); // Value change transition
|
|
1285
1285
|
|
|
1286
|
+
// Lightbox
|
|
1287
|
+
$lightbox-overlay-background: var(--lightbox-overlay-background); // Lightbox overlay backdrop color
|
|
1288
|
+
$lightbox-zIndex-overlay: var(--lightbox-zIndex-overlay); // Overlay z-index (above dialogs)
|
|
1289
|
+
$lightbox-nav-size: var(--lightbox-nav-size); // Navigation button size
|
|
1290
|
+
$lightbox-nav-iconSize: var(--lightbox-nav-iconSize); // Navigation icon size
|
|
1291
|
+
$lightbox-nav-color-default: var(--lightbox-nav-color-default); // Nav button color
|
|
1292
|
+
$lightbox-nav-color-hover: var(--lightbox-nav-color-hover); // Nav button hover color
|
|
1293
|
+
$lightbox-nav-background-default: var(--lightbox-nav-background-default); // Nav button background
|
|
1294
|
+
$lightbox-nav-background-hover: var(--lightbox-nav-background-hover); // Nav button hover background
|
|
1295
|
+
$lightbox-nav-radius: var(--lightbox-nav-radius); // Nav button border radius
|
|
1296
|
+
$lightbox-closeButton-size: var(--lightbox-closeButton-size); // Close button size
|
|
1297
|
+
$lightbox-closeButton-iconSize: var(--lightbox-closeButton-iconSize); // Close icon size
|
|
1298
|
+
$lightbox-counter-color: var(--lightbox-counter-color); // Counter text color
|
|
1299
|
+
$lightbox-counter-fontSize: var(--lightbox-counter-fontSize); // Counter font size
|
|
1300
|
+
$lightbox-thumbnail-size: var(--lightbox-thumbnail-size); // Thumbnail width/height
|
|
1301
|
+
$lightbox-thumbnail-gap: var(--lightbox-thumbnail-gap); // Gap between thumbnails
|
|
1302
|
+
$lightbox-thumbnail-radius: var(--lightbox-thumbnail-radius); // Thumbnail border radius
|
|
1303
|
+
$lightbox-thumbnail-border-active: var(--lightbox-thumbnail-border-active); // Active thumbnail border color
|
|
1304
|
+
$lightbox-thumbnail-opacity-default: var(--lightbox-thumbnail-opacity-default); // Inactive thumbnail opacity
|
|
1305
|
+
$lightbox-thumbnail-opacity-active: var(--lightbox-thumbnail-opacity-active); // Active thumbnail opacity
|
|
1306
|
+
|
|
1286
1307
|
// Listbox
|
|
1287
1308
|
$listbox-padding: var(--listbox-padding); // Container padding
|
|
1288
1309
|
$listbox-radius: var(--listbox-radius); // Container border radius
|
package/dist/ts/tokens.ts
CHANGED
|
@@ -1172,6 +1172,25 @@ export const tokenNames = [
|
|
|
1172
1172
|
'knob-focus-ringColor',
|
|
1173
1173
|
'knob-focus-ringOffset',
|
|
1174
1174
|
'knob-transition-duration',
|
|
1175
|
+
'lightbox-overlay-background',
|
|
1176
|
+
'lightbox-zIndex-overlay',
|
|
1177
|
+
'lightbox-nav-size',
|
|
1178
|
+
'lightbox-nav-iconSize',
|
|
1179
|
+
'lightbox-nav-color-default',
|
|
1180
|
+
'lightbox-nav-color-hover',
|
|
1181
|
+
'lightbox-nav-background-default',
|
|
1182
|
+
'lightbox-nav-background-hover',
|
|
1183
|
+
'lightbox-nav-radius',
|
|
1184
|
+
'lightbox-closeButton-size',
|
|
1185
|
+
'lightbox-closeButton-iconSize',
|
|
1186
|
+
'lightbox-counter-color',
|
|
1187
|
+
'lightbox-counter-fontSize',
|
|
1188
|
+
'lightbox-thumbnail-size',
|
|
1189
|
+
'lightbox-thumbnail-gap',
|
|
1190
|
+
'lightbox-thumbnail-radius',
|
|
1191
|
+
'lightbox-thumbnail-border-active',
|
|
1192
|
+
'lightbox-thumbnail-opacity-default',
|
|
1193
|
+
'lightbox-thumbnail-opacity-active',
|
|
1175
1194
|
'listbox-padding',
|
|
1176
1195
|
'listbox-radius',
|
|
1177
1196
|
'listbox-border-width',
|
|
@@ -2391,6 +2410,7 @@ export const tokenCategories = {
|
|
|
2391
2410
|
inputSwitch: ['inputSwitch-width-sm', 'inputSwitch-width-md', 'inputSwitch-width-lg', 'inputSwitch-height-sm', 'inputSwitch-height-md', 'inputSwitch-height-lg', 'inputSwitch-thumb-size-sm', 'inputSwitch-thumb-size-md', 'inputSwitch-thumb-size-lg', 'inputSwitch-thumb-offset', 'inputSwitch-thumb-color-default', 'inputSwitch-thumb-shadow', 'inputSwitch-track-background-off', 'inputSwitch-track-background-on', 'inputSwitch-track-background-disabled', 'inputSwitch-label-gap', 'inputSwitch-label-color-default', 'inputSwitch-label-color-disabled', 'inputSwitch-focus-ringWidth', 'inputSwitch-focus-ringColor', 'inputSwitch-focus-ringOffset'] as const,
|
|
2392
2411
|
input-text: ['input-text-height-sm', 'input-text-height-md', 'input-text-height-lg', 'input-text-padding-x', 'input-text-padding-y', 'input-text-fontSize-sm', 'input-text-fontSize-md', 'input-text-fontSize-lg', 'input-text-radius', 'input-text-border-width', 'input-text-border-color-default', 'input-text-border-color-hover', 'input-text-border-color-focus', 'input-text-border-color-disabled', 'input-text-border-color-error', 'input-text-background-default', 'input-text-background-disabled', 'input-text-background-hover', 'input-text-text-default', 'input-text-text-placeholder', 'input-text-text-disabled', 'input-text-focus-ringWidth', 'input-text-focus-ringColor', 'input-text-focus-ringOffset'] as const,
|
|
2393
2412
|
knob: ['knob-size-sm', 'knob-size-md', 'knob-size-lg', 'knob-strokeWidth-sm', 'knob-strokeWidth-md', 'knob-strokeWidth-lg', 'knob-track-color', 'knob-fill-color-default', 'knob-fill-color-disabled', 'knob-valueText-fontSize-sm', 'knob-valueText-fontSize-md', 'knob-valueText-fontSize-lg', 'knob-valueText-fontWeight', 'knob-valueText-color-default', 'knob-valueText-color-disabled', 'knob-focus-ringWidth', 'knob-focus-ringColor', 'knob-focus-ringOffset', 'knob-transition-duration'] as const,
|
|
2413
|
+
lightbox: ['lightbox-overlay-background', 'lightbox-zIndex-overlay', 'lightbox-nav-size', 'lightbox-nav-iconSize', 'lightbox-nav-color-default', 'lightbox-nav-color-hover', 'lightbox-nav-background-default', 'lightbox-nav-background-hover', 'lightbox-nav-radius', 'lightbox-closeButton-size', 'lightbox-closeButton-iconSize', 'lightbox-counter-color', 'lightbox-counter-fontSize', 'lightbox-thumbnail-size', 'lightbox-thumbnail-gap', 'lightbox-thumbnail-radius', 'lightbox-thumbnail-border-active', 'lightbox-thumbnail-opacity-default', 'lightbox-thumbnail-opacity-active'] as const,
|
|
2394
2414
|
listbox: ['listbox-padding', 'listbox-radius', 'listbox-border-width', 'listbox-border-color-default', 'listbox-border-color-focus', 'listbox-border-color-disabled', 'listbox-background-default', 'listbox-background-disabled', 'listbox-maxHeight', 'listbox-filter-height', 'listbox-filter-padding-x', 'listbox-filter-background', 'listbox-filter-border', 'listbox-filter-radius', 'listbox-filter-text', 'listbox-filter-placeholder', 'listbox-filter-marginBottom', 'listbox-option-padding-x', 'listbox-option-padding-y', 'listbox-option-radius', 'listbox-option-background-default', 'listbox-option-background-hover', 'listbox-option-background-selected', 'listbox-option-background-focus', 'listbox-option-text-default', 'listbox-option-text-selected', 'listbox-option-text-disabled', 'listbox-option-fontSize', 'listbox-focus-ringWidth', 'listbox-focus-ringColor', 'listbox-focus-ringOffset', 'listbox-checkbox-size', 'listbox-checkbox-marginRight'] as const,
|
|
2395
2415
|
mega-menu: ['mega-menu-root-background', 'mega-menu-root-padding-x', 'mega-menu-root-padding-y', 'mega-menu-root-gap', 'mega-menu-rootItem-padding-x', 'mega-menu-rootItem-padding-y', 'mega-menu-rootItem-text-color-default', 'mega-menu-rootItem-text-color-hover', 'mega-menu-rootItem-text-color-active', 'mega-menu-rootItem-text-fontSize', 'mega-menu-rootItem-text-fontWeight-default', 'mega-menu-rootItem-text-fontWeight-active', 'mega-menu-rootItem-background-default', 'mega-menu-rootItem-background-hover', 'mega-menu-rootItem-background-active', 'mega-menu-rootItem-radius', 'mega-menu-rootItem-indicator-height', 'mega-menu-rootItem-indicator-color', 'mega-menu-panel-background', 'mega-menu-panel-border-color', 'mega-menu-panel-border-width', 'mega-menu-panel-radius', 'mega-menu-panel-shadow', 'mega-menu-panel-padding', 'mega-menu-panel-gap', 'mega-menu-panel-maxWidth', 'mega-menu-panel-zIndex', 'mega-menu-column-minWidth', 'mega-menu-column-gap', 'mega-menu-category-text-color', 'mega-menu-category-text-fontSize', 'mega-menu-category-text-fontWeight', 'mega-menu-category-text-textTransform', 'mega-menu-category-text-letterSpacing', 'mega-menu-category-margin-bottom', 'mega-menu-item-padding-x', 'mega-menu-item-padding-y', 'mega-menu-item-text-color-default', 'mega-menu-item-text-color-hover', 'mega-menu-item-text-fontSize', 'mega-menu-item-background-hover', 'mega-menu-item-radius', 'mega-menu-item-icon-size', 'mega-menu-item-icon-color', 'mega-menu-item-icon-gap', 'mega-menu-item-description-color', 'mega-menu-item-description-fontSize'] as const,
|
|
2396
2416
|
menu: ['menu-background', 'menu-border-width', 'menu-border-color', 'menu-radius', 'menu-shadow', 'menu-padding', 'menu-minWidth', 'menu-maxHeight', 'menu-zIndex', 'menu-item-padding-x', 'menu-item-padding-y', 'menu-item-gap', 'menu-item-radius', 'menu-item-background-default', 'menu-item-background-hover', 'menu-item-background-active', 'menu-item-background-disabled', 'menu-item-text-default', 'menu-item-text-disabled', 'menu-item-icon-size', 'menu-item-icon-color-default', 'menu-item-icon-color-disabled', 'menu-item-fontSize', 'menu-separator-color', 'menu-separator-margin'] as const,
|
package/package.json
CHANGED
|
@@ -283,7 +283,139 @@ $c: galleria;
|
|
|
283
283
|
right: 16px;
|
|
284
284
|
}
|
|
285
285
|
|
|
286
|
-
//
|
|
286
|
+
// Counter
|
|
287
|
+
.#{$c}__counter {
|
|
288
|
+
position: absolute;
|
|
289
|
+
bottom: var(--galleria-counter-bottom, 16px);
|
|
290
|
+
left: 50%;
|
|
291
|
+
transform: translateX(-50%);
|
|
292
|
+
padding: var(--galleria-counter-padding, 6px 16px);
|
|
293
|
+
background: var(--galleria-counter-background, rgba(0, 0, 0, 0.6));
|
|
294
|
+
color: var(--galleria-counter-color, #fff);
|
|
295
|
+
font-size: var(--galleria-counter-fontSize, 14px);
|
|
296
|
+
border-radius: var(--galleria-counter-radius, 20px);
|
|
297
|
+
pointer-events: none;
|
|
298
|
+
z-index: 2;
|
|
299
|
+
font-variant-numeric: tabular-nums;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Fullscreen thumbnails
|
|
303
|
+
.#{$c}__fullscreen-thumbnails {
|
|
304
|
+
position: absolute;
|
|
305
|
+
bottom: 0;
|
|
306
|
+
left: 0;
|
|
307
|
+
right: 0;
|
|
308
|
+
padding: var(--galleria-fullscreen-thumbnails-padding, 12px 16px);
|
|
309
|
+
background: var(--galleria-fullscreen-thumbnails-background, rgba(0, 0, 0, 0.5));
|
|
310
|
+
overflow-x: auto;
|
|
311
|
+
z-index: 2;
|
|
312
|
+
|
|
313
|
+
.#{$c}__thumbnail-container {
|
|
314
|
+
justify-content: center;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.#{$c}__thumbnail--fullscreen {
|
|
319
|
+
width: var(--galleria-fullscreen-thumbnail-width, 64px);
|
|
320
|
+
height: var(--galleria-fullscreen-thumbnail-height, 40px);
|
|
321
|
+
border-color: rgba(255, 255, 255, 0.3);
|
|
322
|
+
|
|
323
|
+
&.#{$c}__thumbnail--active {
|
|
324
|
+
border-color: #fff;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// When fullscreen has thumbnails, shift counter up above them
|
|
329
|
+
.#{$c}__fullscreen:has(.#{$c}__fullscreen-thumbnails) > .#{$c}__counter {
|
|
330
|
+
bottom: 80px;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Loading spinner
|
|
334
|
+
.#{$c}__loader {
|
|
335
|
+
position: absolute;
|
|
336
|
+
inset: 0;
|
|
337
|
+
display: flex;
|
|
338
|
+
align-items: center;
|
|
339
|
+
justify-content: center;
|
|
340
|
+
z-index: 1;
|
|
341
|
+
pointer-events: none;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.#{$c}__loader--fullscreen {
|
|
345
|
+
position: absolute;
|
|
346
|
+
z-index: 3;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.#{$c}__spinner {
|
|
350
|
+
width: var(--galleria-spinner-size, 40px);
|
|
351
|
+
height: var(--galleria-spinner-size, 40px);
|
|
352
|
+
color: var(--galleria-spinner-color, rgba(255, 255, 255, 0.7));
|
|
353
|
+
animation: galleria-spin 1s linear infinite;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
@keyframes galleria-spin {
|
|
357
|
+
to { transform: rotate(360deg); }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Zoom
|
|
361
|
+
.#{$c}__fullscreen--zoomed {
|
|
362
|
+
cursor: default;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.#{$c}__fullscreen-image {
|
|
366
|
+
transition: transform 300ms ease;
|
|
367
|
+
cursor: zoom-in;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
.#{$c}__fullscreen-image--zoomed {
|
|
371
|
+
cursor: zoom-out;
|
|
372
|
+
z-index: 1;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Fade transition
|
|
376
|
+
.galleria-fade-enter-active,
|
|
377
|
+
.galleria-fade-leave-active {
|
|
378
|
+
transition: opacity var(--galleria-transition-duration, 300ms) ease;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.galleria-fade-enter-from,
|
|
382
|
+
.galleria-fade-leave-to {
|
|
383
|
+
opacity: 0;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Slide left transition (next)
|
|
387
|
+
.galleria-slide-left-enter-active,
|
|
388
|
+
.galleria-slide-left-leave-active {
|
|
389
|
+
transition: transform var(--galleria-transition-duration, 300ms) ease, opacity var(--galleria-transition-duration, 300ms) ease;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.galleria-slide-left-enter-from {
|
|
393
|
+
transform: translateX(30px);
|
|
394
|
+
opacity: 0;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.galleria-slide-left-leave-to {
|
|
398
|
+
transform: translateX(-30px);
|
|
399
|
+
opacity: 0;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Slide right transition (prev)
|
|
403
|
+
.galleria-slide-right-enter-active,
|
|
404
|
+
.galleria-slide-right-leave-active {
|
|
405
|
+
transition: transform var(--galleria-transition-duration, 300ms) ease, opacity var(--galleria-transition-duration, 300ms) ease;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
.galleria-slide-right-enter-from {
|
|
409
|
+
transform: translateX(-30px);
|
|
410
|
+
opacity: 0;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.galleria-slide-right-leave-to {
|
|
414
|
+
transform: translateX(30px);
|
|
415
|
+
opacity: 0;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Fullscreen open/close transition
|
|
287
419
|
.galleria-fullscreen-enter-active,
|
|
288
420
|
.galleria-fullscreen-leave-active {
|
|
289
421
|
transition: opacity 200ms ease;
|
|
@@ -2,13 +2,31 @@
|
|
|
2
2
|
<div class="galleria" :class="additionalClasses">
|
|
3
3
|
<div class="galleria__main">
|
|
4
4
|
<!-- Preview area -->
|
|
5
|
-
<div
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
<div
|
|
6
|
+
class="galleria__preview"
|
|
7
|
+
@click="onPreviewClick"
|
|
8
|
+
@touchstart.passive="onTouchStart"
|
|
9
|
+
@touchmove.passive="onTouchMove"
|
|
10
|
+
@touchend="onTouchEnd"
|
|
11
|
+
>
|
|
12
|
+
<!-- Loading spinner -->
|
|
13
|
+
<div v-if="imageLoading" class="galleria__loader">
|
|
14
|
+
<svg class="galleria__spinner" viewBox="0 0 50 50">
|
|
15
|
+
<circle cx="25" cy="25" r="20" fill="none" stroke="currentColor" stroke-width="4" stroke-dasharray="80" stroke-linecap="round" />
|
|
16
|
+
</svg>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<Transition :name="transitionName" mode="out-in">
|
|
20
|
+
<img
|
|
21
|
+
v-if="activeItem"
|
|
22
|
+
:key="activeIndex"
|
|
23
|
+
:src="activeItem.src || activeItem"
|
|
24
|
+
:alt="activeItem.alt || `Image ${activeIndex + 1}`"
|
|
25
|
+
class="galleria__preview-image"
|
|
26
|
+
@load="onImageLoad"
|
|
27
|
+
@error="onImageLoad"
|
|
28
|
+
/>
|
|
29
|
+
</Transition>
|
|
12
30
|
|
|
13
31
|
<!-- Navigation buttons -->
|
|
14
32
|
<button
|
|
@@ -83,11 +101,17 @@
|
|
|
83
101
|
<Transition name="galleria-fullscreen">
|
|
84
102
|
<div
|
|
85
103
|
v-if="fullscreenVisible"
|
|
104
|
+
ref="fullscreenRef"
|
|
86
105
|
class="galleria__fullscreen"
|
|
87
|
-
|
|
106
|
+
:class="{ 'galleria__fullscreen--zoomed': isZoomed }"
|
|
107
|
+
tabindex="0"
|
|
108
|
+
@click="onFullscreenClick"
|
|
88
109
|
@keydown.escape="closeFullscreen"
|
|
89
110
|
@keydown.left="prev"
|
|
90
111
|
@keydown.right="next"
|
|
112
|
+
@touchstart.passive="onTouchStart"
|
|
113
|
+
@touchmove.passive="onTouchMove"
|
|
114
|
+
@touchend="onTouchEnd"
|
|
91
115
|
>
|
|
92
116
|
<button
|
|
93
117
|
type="button"
|
|
@@ -113,13 +137,28 @@
|
|
|
113
137
|
</svg>
|
|
114
138
|
</button>
|
|
115
139
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
140
|
+
<!-- Loading spinner in fullscreen -->
|
|
141
|
+
<div v-if="imageLoading" class="galleria__loader galleria__loader--fullscreen">
|
|
142
|
+
<svg class="galleria__spinner" viewBox="0 0 50 50">
|
|
143
|
+
<circle cx="25" cy="25" r="20" fill="none" stroke="currentColor" stroke-width="4" stroke-dasharray="80" stroke-linecap="round" />
|
|
144
|
+
</svg>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
<Transition :name="transitionName" mode="out-in">
|
|
148
|
+
<img
|
|
149
|
+
v-if="activeItem"
|
|
150
|
+
:key="activeIndex"
|
|
151
|
+
:src="activeItem.src || activeItem"
|
|
152
|
+
:alt="activeItem.alt || `Image ${activeIndex + 1}`"
|
|
153
|
+
class="galleria__fullscreen-image"
|
|
154
|
+
:class="{ 'galleria__fullscreen-image--zoomed': isZoomed }"
|
|
155
|
+
:style="zoomStyle"
|
|
156
|
+
@click.stop="onImageClick"
|
|
157
|
+
@dblclick.stop="toggleZoom"
|
|
158
|
+
@load="onImageLoad"
|
|
159
|
+
@error="onImageLoad"
|
|
160
|
+
/>
|
|
161
|
+
</Transition>
|
|
123
162
|
|
|
124
163
|
<button
|
|
125
164
|
v-if="items.length > 1"
|
|
@@ -132,6 +171,31 @@
|
|
|
132
171
|
<polyline points="9 18 15 12 9 6" />
|
|
133
172
|
</svg>
|
|
134
173
|
</button>
|
|
174
|
+
|
|
175
|
+
<!-- Counter -->
|
|
176
|
+
<div v-if="showCounter && items.length > 1" class="galleria__counter">
|
|
177
|
+
{{ activeIndex + 1 }} / {{ items.length }}
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<!-- Fullscreen thumbnails -->
|
|
181
|
+
<div v-if="showFullscreenThumbnails && items.length > 1" class="galleria__fullscreen-thumbnails">
|
|
182
|
+
<div class="galleria__thumbnail-container">
|
|
183
|
+
<button
|
|
184
|
+
v-for="(item, index) in items"
|
|
185
|
+
:key="index"
|
|
186
|
+
type="button"
|
|
187
|
+
class="galleria__thumbnail galleria__thumbnail--fullscreen"
|
|
188
|
+
:class="{ 'galleria__thumbnail--active': index === activeIndex }"
|
|
189
|
+
:aria-label="`View image ${index + 1}`"
|
|
190
|
+
@click.stop="goTo(index)"
|
|
191
|
+
>
|
|
192
|
+
<img
|
|
193
|
+
:src="item.thumbnail || item.src || item"
|
|
194
|
+
:alt="item.alt || `Thumbnail ${index + 1}`"
|
|
195
|
+
/>
|
|
196
|
+
</button>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
135
199
|
</div>
|
|
136
200
|
</Transition>
|
|
137
201
|
</Teleport>
|
|
@@ -142,7 +206,7 @@
|
|
|
142
206
|
|
|
143
207
|
<script setup lang="ts">
|
|
144
208
|
import type { GalleriaProps, GalleriaEmits } from '../../types';
|
|
145
|
-
import { computed, ref, watch, onMounted, onUnmounted } from "vue";
|
|
209
|
+
import { computed, ref, watch, onMounted, onUnmounted, nextTick } from "vue";
|
|
146
210
|
|
|
147
211
|
defineOptions({ name: 'FtpGalleria' });
|
|
148
212
|
|
|
@@ -153,17 +217,34 @@ const props = withDefaults(defineProps<GalleriaProps>(), {
|
|
|
153
217
|
showThumbnails: true,
|
|
154
218
|
showItemNavigators: true,
|
|
155
219
|
showIndicators: false,
|
|
220
|
+
showCounter: true,
|
|
221
|
+
showFullscreenThumbnails: true,
|
|
222
|
+
enableZoom: true,
|
|
156
223
|
circular: false,
|
|
157
224
|
autoplay: false,
|
|
158
225
|
autoplayInterval: 4000,
|
|
159
226
|
thumbnailsPosition: "bottom",
|
|
227
|
+
transition: "fade",
|
|
160
228
|
});
|
|
161
229
|
|
|
162
230
|
const emit = defineEmits(["update:activeIndex", "show", "hide"]);
|
|
163
231
|
|
|
164
232
|
const internalIndex = ref(props.activeIndex);
|
|
165
233
|
const fullscreenVisible = ref(false);
|
|
234
|
+
const fullscreenRef = ref<HTMLElement | null>(null);
|
|
166
235
|
const autoplayTimer = ref<ReturnType<typeof setInterval> | null>(null);
|
|
236
|
+
const imageLoading = ref(false);
|
|
237
|
+
const isZoomed = ref(false);
|
|
238
|
+
const zoomScale = ref(1);
|
|
239
|
+
const zoomOrigin = ref({ x: 50, y: 50 });
|
|
240
|
+
const slideDirection = ref<'left' | 'right'>('left');
|
|
241
|
+
|
|
242
|
+
// Touch tracking
|
|
243
|
+
const touchStartX = ref(0);
|
|
244
|
+
const touchStartY = ref(0);
|
|
245
|
+
const touchDeltaX = ref(0);
|
|
246
|
+
// Pinch zoom
|
|
247
|
+
const initialPinchDistance = ref(0);
|
|
167
248
|
|
|
168
249
|
const activeIndex = computed({
|
|
169
250
|
get: () => internalIndex.value,
|
|
@@ -175,6 +256,12 @@ const activeIndex = computed({
|
|
|
175
256
|
|
|
176
257
|
const activeItem = computed(() => props.items[activeIndex.value]);
|
|
177
258
|
|
|
259
|
+
const transitionName = computed(() => {
|
|
260
|
+
if (props.transition === 'none') return '';
|
|
261
|
+
if (props.transition === 'slide') return slideDirection.value === 'left' ? 'galleria-slide-left' : 'galleria-slide-right';
|
|
262
|
+
return 'galleria-fade';
|
|
263
|
+
});
|
|
264
|
+
|
|
178
265
|
const additionalClasses = computed(() =>
|
|
179
266
|
[
|
|
180
267
|
`galleria--thumbnails-${props.thumbnailsPosition}`,
|
|
@@ -184,11 +271,53 @@ const additionalClasses = computed(() =>
|
|
|
184
271
|
.join(" ")
|
|
185
272
|
);
|
|
186
273
|
|
|
274
|
+
const zoomStyle = computed(() => {
|
|
275
|
+
if (!isZoomed.value) return {};
|
|
276
|
+
return {
|
|
277
|
+
transform: `scale(${zoomScale.value})`,
|
|
278
|
+
transformOrigin: `${zoomOrigin.value.x}% ${zoomOrigin.value.y}%`,
|
|
279
|
+
cursor: 'zoom-out',
|
|
280
|
+
};
|
|
281
|
+
});
|
|
282
|
+
|
|
187
283
|
watch(() => props.activeIndex, (newValue) => {
|
|
188
284
|
internalIndex.value = newValue;
|
|
189
285
|
});
|
|
190
286
|
|
|
287
|
+
// Reset zoom on image change
|
|
288
|
+
watch(() => activeIndex.value, () => {
|
|
289
|
+
isZoomed.value = false;
|
|
290
|
+
zoomScale.value = 1;
|
|
291
|
+
imageLoading.value = true;
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
// Preload adjacent images
|
|
295
|
+
watch(() => activeIndex.value, () => {
|
|
296
|
+
preloadAdjacent();
|
|
297
|
+
}, { immediate: true });
|
|
298
|
+
|
|
299
|
+
const preloadAdjacent = () => {
|
|
300
|
+
const preloadIndex = (i: number) => {
|
|
301
|
+
const item = props.items[i];
|
|
302
|
+
if (!item) return;
|
|
303
|
+
const img = new Image();
|
|
304
|
+
img.src = item.src || item;
|
|
305
|
+
};
|
|
306
|
+
if (activeIndex.value > 0) preloadIndex(activeIndex.value - 1);
|
|
307
|
+
if (activeIndex.value < props.items.length - 1) preloadIndex(activeIndex.value + 1);
|
|
308
|
+
// Also preload circular edges
|
|
309
|
+
if (props.circular && props.items.length > 1) {
|
|
310
|
+
if (activeIndex.value === 0) preloadIndex(props.items.length - 1);
|
|
311
|
+
if (activeIndex.value === props.items.length - 1) preloadIndex(0);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
const onImageLoad = () => {
|
|
316
|
+
imageLoading.value = false;
|
|
317
|
+
};
|
|
318
|
+
|
|
191
319
|
const prev = () => {
|
|
320
|
+
slideDirection.value = 'right';
|
|
192
321
|
if (activeIndex.value > 0) {
|
|
193
322
|
activeIndex.value--;
|
|
194
323
|
} else if (props.circular && props.items.length > 0) {
|
|
@@ -197,6 +326,7 @@ const prev = () => {
|
|
|
197
326
|
};
|
|
198
327
|
|
|
199
328
|
const next = () => {
|
|
329
|
+
slideDirection.value = 'left';
|
|
200
330
|
if (activeIndex.value < props.items.length - 1) {
|
|
201
331
|
activeIndex.value++;
|
|
202
332
|
} else if (props.circular) {
|
|
@@ -205,6 +335,7 @@ const next = () => {
|
|
|
205
335
|
};
|
|
206
336
|
|
|
207
337
|
const goTo = (index: any) => {
|
|
338
|
+
slideDirection.value = index > activeIndex.value ? 'left' : 'right';
|
|
208
339
|
activeIndex.value = index;
|
|
209
340
|
};
|
|
210
341
|
|
|
@@ -217,15 +348,96 @@ const onPreviewClick = () => {
|
|
|
217
348
|
const openFullscreen = () => {
|
|
218
349
|
fullscreenVisible.value = true;
|
|
219
350
|
document.body.style.overflow = "hidden";
|
|
351
|
+
nextTick(() => fullscreenRef.value?.focus());
|
|
220
352
|
emit("show");
|
|
221
353
|
};
|
|
222
354
|
|
|
223
355
|
const closeFullscreen = () => {
|
|
224
356
|
fullscreenVisible.value = false;
|
|
357
|
+
isZoomed.value = false;
|
|
358
|
+
zoomScale.value = 1;
|
|
225
359
|
document.body.style.overflow = "";
|
|
226
360
|
emit("hide");
|
|
227
361
|
};
|
|
228
362
|
|
|
363
|
+
const onFullscreenClick = (e: MouseEvent) => {
|
|
364
|
+
// Only close if clicking the backdrop itself
|
|
365
|
+
if (e.target === fullscreenRef.value) {
|
|
366
|
+
closeFullscreen();
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
const onImageClick = () => {
|
|
371
|
+
// Single click on image in fullscreen — do nothing (dblclick handles zoom)
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
// Zoom
|
|
375
|
+
const toggleZoom = (e?: MouseEvent) => {
|
|
376
|
+
if (!props.enableZoom || !fullscreenVisible.value) return;
|
|
377
|
+
if (isZoomed.value) {
|
|
378
|
+
isZoomed.value = false;
|
|
379
|
+
zoomScale.value = 1;
|
|
380
|
+
} else {
|
|
381
|
+
isZoomed.value = true;
|
|
382
|
+
zoomScale.value = 2.5;
|
|
383
|
+
if (e) {
|
|
384
|
+
const rect = (e.target as HTMLElement).getBoundingClientRect();
|
|
385
|
+
zoomOrigin.value = {
|
|
386
|
+
x: ((e.clientX - rect.left) / rect.width) * 100,
|
|
387
|
+
y: ((e.clientY - rect.top) / rect.height) * 100,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
// Touch/swipe
|
|
394
|
+
const onTouchStart = (e: TouchEvent) => {
|
|
395
|
+
if (e.touches.length === 2 && fullscreenVisible.value && props.enableZoom) {
|
|
396
|
+
// Pinch start
|
|
397
|
+
initialPinchDistance.value = getPinchDistance(e);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
touchStartX.value = e.touches[0].clientX;
|
|
401
|
+
touchStartY.value = e.touches[0].clientY;
|
|
402
|
+
touchDeltaX.value = 0;
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const onTouchMove = (e: TouchEvent) => {
|
|
406
|
+
if (e.touches.length === 2 && fullscreenVisible.value && props.enableZoom) {
|
|
407
|
+
// Pinch zoom
|
|
408
|
+
const dist = getPinchDistance(e);
|
|
409
|
+
const ratio = dist / initialPinchDistance.value;
|
|
410
|
+
if (ratio > 1.3 && !isZoomed.value) {
|
|
411
|
+
isZoomed.value = true;
|
|
412
|
+
zoomScale.value = 2.5;
|
|
413
|
+
zoomOrigin.value = { x: 50, y: 50 };
|
|
414
|
+
} else if (ratio < 0.7 && isZoomed.value) {
|
|
415
|
+
isZoomed.value = false;
|
|
416
|
+
zoomScale.value = 1;
|
|
417
|
+
}
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
touchDeltaX.value = e.touches[0].clientX - touchStartX.value;
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const onTouchEnd = () => {
|
|
424
|
+
if (isZoomed.value) return; // Don't swipe when zoomed
|
|
425
|
+
if (Math.abs(touchDeltaX.value) > 50) {
|
|
426
|
+
if (touchDeltaX.value > 0) {
|
|
427
|
+
prev();
|
|
428
|
+
} else {
|
|
429
|
+
next();
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
touchDeltaX.value = 0;
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const getPinchDistance = (e: TouchEvent): number => {
|
|
436
|
+
const dx = e.touches[0].clientX - e.touches[1].clientX;
|
|
437
|
+
const dy = e.touches[0].clientY - e.touches[1].clientY;
|
|
438
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
439
|
+
};
|
|
440
|
+
|
|
229
441
|
const startAutoplay = () => {
|
|
230
442
|
if (typeof window === 'undefined') return;
|
|
231
443
|
if (props.autoplay && props.items.length > 1) {
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
@use "../../../dist/scss/tokens" as *;
|
|
2
|
+
|
|
3
|
+
$c: lightbox;
|
|
4
|
+
|
|
5
|
+
// Overlay
|
|
6
|
+
.#{$c}__overlay {
|
|
7
|
+
position: fixed;
|
|
8
|
+
inset: 0;
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: center;
|
|
12
|
+
z-index: var(--lightbox-zIndex-overlay, 2000);
|
|
13
|
+
background-color: var(--lightbox-overlay-background, rgba(0, 0, 0, 0.92));
|
|
14
|
+
outline: none;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Close button
|
|
18
|
+
.#{$c}__close {
|
|
19
|
+
position: absolute;
|
|
20
|
+
top: 16px;
|
|
21
|
+
right: 16px;
|
|
22
|
+
z-index: 1;
|
|
23
|
+
display: flex;
|
|
24
|
+
align-items: center;
|
|
25
|
+
justify-content: center;
|
|
26
|
+
width: var(--lightbox-closeButton-size, 40px);
|
|
27
|
+
height: var(--lightbox-closeButton-size, 40px);
|
|
28
|
+
padding: 0;
|
|
29
|
+
border: none;
|
|
30
|
+
background: var(--lightbox-nav-background-default, rgba(255, 255, 255, 0.1));
|
|
31
|
+
border-radius: var(--lightbox-nav-radius, 9999px);
|
|
32
|
+
color: var(--lightbox-nav-color-default, rgba(255, 255, 255, 0.8));
|
|
33
|
+
cursor: pointer;
|
|
34
|
+
transition: background-color 0.15s ease, color 0.15s ease;
|
|
35
|
+
|
|
36
|
+
&:hover {
|
|
37
|
+
background: var(--lightbox-nav-background-hover, rgba(255, 255, 255, 0.2));
|
|
38
|
+
color: var(--lightbox-nav-color-hover, #ffffff);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
&:focus-visible {
|
|
42
|
+
outline: 2px solid rgba(255, 255, 255, 0.5);
|
|
43
|
+
outline-offset: 2px;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
svg {
|
|
47
|
+
width: var(--lightbox-closeButton-iconSize, 20px);
|
|
48
|
+
height: var(--lightbox-closeButton-iconSize, 20px);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Counter
|
|
53
|
+
.#{$c}__counter {
|
|
54
|
+
position: absolute;
|
|
55
|
+
top: 20px;
|
|
56
|
+
left: 50%;
|
|
57
|
+
transform: translateX(-50%);
|
|
58
|
+
z-index: 1;
|
|
59
|
+
color: var(--lightbox-counter-color, rgba(255, 255, 255, 0.7));
|
|
60
|
+
font-size: var(--lightbox-counter-fontSize, 14px);
|
|
61
|
+
font-variant-numeric: tabular-nums;
|
|
62
|
+
user-select: none;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Navigation buttons
|
|
66
|
+
.#{$c}__nav {
|
|
67
|
+
position: absolute;
|
|
68
|
+
top: 50%;
|
|
69
|
+
transform: translateY(-50%);
|
|
70
|
+
z-index: 1;
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
width: var(--lightbox-nav-size, 48px);
|
|
75
|
+
height: var(--lightbox-nav-size, 48px);
|
|
76
|
+
padding: 0;
|
|
77
|
+
border: none;
|
|
78
|
+
background: var(--lightbox-nav-background-default, rgba(255, 255, 255, 0.1));
|
|
79
|
+
border-radius: var(--lightbox-nav-radius, 9999px);
|
|
80
|
+
color: var(--lightbox-nav-color-default, rgba(255, 255, 255, 0.8));
|
|
81
|
+
cursor: pointer;
|
|
82
|
+
transition: background-color 0.15s ease, color 0.15s ease, opacity 0.15s ease;
|
|
83
|
+
|
|
84
|
+
&:hover {
|
|
85
|
+
background: var(--lightbox-nav-background-hover, rgba(255, 255, 255, 0.2));
|
|
86
|
+
color: var(--lightbox-nav-color-hover, #ffffff);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
&:focus-visible {
|
|
90
|
+
outline: 2px solid rgba(255, 255, 255, 0.5);
|
|
91
|
+
outline-offset: 2px;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
svg {
|
|
95
|
+
width: var(--lightbox-nav-iconSize, 24px);
|
|
96
|
+
height: var(--lightbox-nav-iconSize, 24px);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.#{$c}__nav--prev {
|
|
101
|
+
left: 16px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.#{$c}__nav--next {
|
|
105
|
+
right: 16px;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Image container
|
|
109
|
+
.#{$c}__image-container {
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
justify-content: center;
|
|
113
|
+
width: 100%;
|
|
114
|
+
height: 100%;
|
|
115
|
+
padding: 64px 80px;
|
|
116
|
+
overflow: hidden;
|
|
117
|
+
|
|
118
|
+
@media (max-width: 768px) {
|
|
119
|
+
padding: 64px 16px;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Image
|
|
124
|
+
.#{$c}__image {
|
|
125
|
+
max-width: 100%;
|
|
126
|
+
max-height: 100%;
|
|
127
|
+
object-fit: contain;
|
|
128
|
+
user-select: none;
|
|
129
|
+
cursor: default;
|
|
130
|
+
transition: transform 0.3s ease;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.#{$c}__image--zoomed {
|
|
134
|
+
cursor: zoom-out;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Thumbnails
|
|
138
|
+
.#{$c}__thumbnails {
|
|
139
|
+
position: absolute;
|
|
140
|
+
bottom: 16px;
|
|
141
|
+
left: 50%;
|
|
142
|
+
transform: translateX(-50%);
|
|
143
|
+
display: flex;
|
|
144
|
+
gap: var(--lightbox-thumbnail-gap, 8px);
|
|
145
|
+
padding: 8px;
|
|
146
|
+
background: rgba(0, 0, 0, 0.4);
|
|
147
|
+
border-radius: 12px;
|
|
148
|
+
max-width: calc(100vw - 32px);
|
|
149
|
+
overflow-x: auto;
|
|
150
|
+
scrollbar-width: none;
|
|
151
|
+
|
|
152
|
+
&::-webkit-scrollbar {
|
|
153
|
+
display: none;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.#{$c}__thumbnail {
|
|
158
|
+
flex-shrink: 0;
|
|
159
|
+
width: var(--lightbox-thumbnail-size, 64px);
|
|
160
|
+
height: var(--lightbox-thumbnail-size, 64px);
|
|
161
|
+
padding: 0;
|
|
162
|
+
border: 2px solid transparent;
|
|
163
|
+
border-radius: var(--lightbox-thumbnail-radius, 6px);
|
|
164
|
+
background: none;
|
|
165
|
+
cursor: pointer;
|
|
166
|
+
overflow: hidden;
|
|
167
|
+
opacity: var(--lightbox-thumbnail-opacity-default, 0.5);
|
|
168
|
+
transition: opacity 0.15s ease, border-color 0.15s ease;
|
|
169
|
+
|
|
170
|
+
&:hover {
|
|
171
|
+
opacity: 0.8;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
&:focus-visible {
|
|
175
|
+
outline: 2px solid rgba(255, 255, 255, 0.5);
|
|
176
|
+
outline-offset: 2px;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
img {
|
|
180
|
+
width: 100%;
|
|
181
|
+
height: 100%;
|
|
182
|
+
object-fit: cover;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.#{$c}__thumbnail--active {
|
|
187
|
+
opacity: var(--lightbox-thumbnail-opacity-active, 1);
|
|
188
|
+
border-color: var(--lightbox-thumbnail-border-active, #ffffff);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Transitions
|
|
192
|
+
.lightbox-fade-enter-active,
|
|
193
|
+
.lightbox-fade-leave-active {
|
|
194
|
+
transition: opacity 0.25s ease;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.lightbox-fade-enter-from,
|
|
198
|
+
.lightbox-fade-leave-to {
|
|
199
|
+
opacity: 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Slide transitions
|
|
203
|
+
.lightbox-slide-left-enter-active,
|
|
204
|
+
.lightbox-slide-left-leave-active,
|
|
205
|
+
.lightbox-slide-right-enter-active,
|
|
206
|
+
.lightbox-slide-right-leave-active {
|
|
207
|
+
transition: transform 0.3s ease, opacity 0.3s ease;
|
|
208
|
+
position: absolute;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.lightbox-slide-left-enter-from {
|
|
212
|
+
transform: translateX(60px);
|
|
213
|
+
opacity: 0;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.lightbox-slide-left-leave-to {
|
|
217
|
+
transform: translateX(-60px);
|
|
218
|
+
opacity: 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
.lightbox-slide-right-enter-from {
|
|
222
|
+
transform: translateX(-60px);
|
|
223
|
+
opacity: 0;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.lightbox-slide-right-leave-to {
|
|
227
|
+
transform: translateX(60px);
|
|
228
|
+
opacity: 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Mobile adjustments
|
|
232
|
+
@media (max-width: 768px) {
|
|
233
|
+
.#{$c}__nav {
|
|
234
|
+
width: 40px;
|
|
235
|
+
height: 40px;
|
|
236
|
+
|
|
237
|
+
svg {
|
|
238
|
+
width: 20px;
|
|
239
|
+
height: 20px;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.#{$c}__nav--prev {
|
|
244
|
+
left: 8px;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.#{$c}__nav--next {
|
|
248
|
+
right: 8px;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.#{$c}__close {
|
|
252
|
+
top: 8px;
|
|
253
|
+
right: 8px;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.#{$c}__counter {
|
|
257
|
+
top: 12px;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.#{$c}__thumbnail {
|
|
261
|
+
width: 48px;
|
|
262
|
+
height: 48px;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Teleport to="body">
|
|
3
|
+
<Transition name="lightbox-fade">
|
|
4
|
+
<div
|
|
5
|
+
v-if="visible"
|
|
6
|
+
class="lightbox__overlay"
|
|
7
|
+
role="dialog"
|
|
8
|
+
aria-modal="true"
|
|
9
|
+
aria-label="Image lightbox"
|
|
10
|
+
tabindex="-1"
|
|
11
|
+
@click="onOverlayClick"
|
|
12
|
+
@keydown.escape="close"
|
|
13
|
+
@keydown.left="prev"
|
|
14
|
+
@keydown.right="next"
|
|
15
|
+
>
|
|
16
|
+
<!-- Close button -->
|
|
17
|
+
<button
|
|
18
|
+
type="button"
|
|
19
|
+
class="lightbox__close"
|
|
20
|
+
aria-label="Close lightbox"
|
|
21
|
+
@click.stop="close"
|
|
22
|
+
>
|
|
23
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
24
|
+
<line x1="18" y1="6" x2="6" y2="18" />
|
|
25
|
+
<line x1="6" y1="6" x2="18" y2="18" />
|
|
26
|
+
</svg>
|
|
27
|
+
</button>
|
|
28
|
+
|
|
29
|
+
<!-- Counter -->
|
|
30
|
+
<div v-if="showCounter && images.length > 1" class="lightbox__counter">
|
|
31
|
+
{{ currentIndex + 1 }} / {{ images.length }}
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<!-- Previous button -->
|
|
35
|
+
<button
|
|
36
|
+
v-if="images.length > 1"
|
|
37
|
+
type="button"
|
|
38
|
+
class="lightbox__nav lightbox__nav--prev"
|
|
39
|
+
aria-label="Previous image"
|
|
40
|
+
@click.stop="prev"
|
|
41
|
+
>
|
|
42
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
43
|
+
<polyline points="15 18 9 12 15 6" />
|
|
44
|
+
</svg>
|
|
45
|
+
</button>
|
|
46
|
+
|
|
47
|
+
<!-- Image -->
|
|
48
|
+
<div
|
|
49
|
+
class="lightbox__image-container"
|
|
50
|
+
@click.stop
|
|
51
|
+
@touchstart="onTouchStart"
|
|
52
|
+
@touchmove="onTouchMove"
|
|
53
|
+
@touchend="onTouchEnd"
|
|
54
|
+
>
|
|
55
|
+
<Transition :name="slideDirection">
|
|
56
|
+
<img
|
|
57
|
+
:key="currentIndex"
|
|
58
|
+
:src="currentImage"
|
|
59
|
+
:alt="`Image ${currentIndex + 1} of ${images.length}`"
|
|
60
|
+
class="lightbox__image"
|
|
61
|
+
:class="{ 'lightbox__image--zoomed': isZoomed }"
|
|
62
|
+
:style="zoomStyle"
|
|
63
|
+
draggable="false"
|
|
64
|
+
@click.stop="toggleZoom"
|
|
65
|
+
@load="onImageLoad"
|
|
66
|
+
/>
|
|
67
|
+
</Transition>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<!-- Next button -->
|
|
71
|
+
<button
|
|
72
|
+
v-if="images.length > 1"
|
|
73
|
+
type="button"
|
|
74
|
+
class="lightbox__nav lightbox__nav--next"
|
|
75
|
+
aria-label="Next image"
|
|
76
|
+
@click.stop="next"
|
|
77
|
+
>
|
|
78
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
79
|
+
<polyline points="9 18 15 12 9 6" />
|
|
80
|
+
</svg>
|
|
81
|
+
</button>
|
|
82
|
+
|
|
83
|
+
<!-- Thumbnails -->
|
|
84
|
+
<div v-if="showThumbnails && images.length > 1" class="lightbox__thumbnails" @click.stop>
|
|
85
|
+
<button
|
|
86
|
+
v-for="(image, index) in images"
|
|
87
|
+
:key="index"
|
|
88
|
+
type="button"
|
|
89
|
+
class="lightbox__thumbnail"
|
|
90
|
+
:class="{ 'lightbox__thumbnail--active': index === currentIndex }"
|
|
91
|
+
:aria-label="`View image ${index + 1}`"
|
|
92
|
+
@click="goTo(index)"
|
|
93
|
+
>
|
|
94
|
+
<img :src="typeof image === 'string' ? image : image.src || image.thumbnail" :alt="`Thumbnail ${index + 1}`" />
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</Transition>
|
|
99
|
+
</Teleport>
|
|
100
|
+
</template>
|
|
101
|
+
|
|
102
|
+
<style src="./Lightbox.scss"></style>
|
|
103
|
+
|
|
104
|
+
<script setup lang="ts">
|
|
105
|
+
import type { LightboxProps } from '../../types';
|
|
106
|
+
import { computed, ref, watch, onUnmounted, nextTick } from "vue";
|
|
107
|
+
|
|
108
|
+
defineOptions({ name: 'FtpLightbox' });
|
|
109
|
+
|
|
110
|
+
const props = withDefaults(defineProps<LightboxProps>(), {
|
|
111
|
+
images: () => [],
|
|
112
|
+
visible: false,
|
|
113
|
+
activeIndex: 0,
|
|
114
|
+
showThumbnails: false,
|
|
115
|
+
showCounter: true,
|
|
116
|
+
closeOnClickOutside: true,
|
|
117
|
+
zoom: false,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const emit = defineEmits<{
|
|
121
|
+
(e: 'update:visible', value: boolean): void
|
|
122
|
+
(e: 'update:activeIndex', value: number): void
|
|
123
|
+
(e: 'show'): void
|
|
124
|
+
(e: 'hide'): void
|
|
125
|
+
}>();
|
|
126
|
+
|
|
127
|
+
const internalIndex = ref(props.activeIndex);
|
|
128
|
+
const isZoomed = ref(false);
|
|
129
|
+
const slideDirection = ref('lightbox-slide-left');
|
|
130
|
+
|
|
131
|
+
// Touch/swipe state
|
|
132
|
+
const touchStartX = ref(0);
|
|
133
|
+
const touchStartY = ref(0);
|
|
134
|
+
const touchDeltaX = ref(0);
|
|
135
|
+
const isSwiping = ref(false);
|
|
136
|
+
|
|
137
|
+
const currentIndex = computed({
|
|
138
|
+
get: () => internalIndex.value,
|
|
139
|
+
set: (val) => {
|
|
140
|
+
internalIndex.value = val;
|
|
141
|
+
emit('update:activeIndex', val);
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const currentImage = computed(() => {
|
|
146
|
+
const img = props.images[currentIndex.value];
|
|
147
|
+
if (!img) return '';
|
|
148
|
+
return typeof img === 'string' ? img : (img as any).src || '';
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const zoomStyle = computed(() => {
|
|
152
|
+
if (!isZoomed.value) return {};
|
|
153
|
+
return { cursor: 'zoom-out', transform: 'scale(2)' };
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
watch(() => props.activeIndex, (val) => {
|
|
157
|
+
internalIndex.value = val;
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Body scroll lock
|
|
161
|
+
let didLockScroll = false;
|
|
162
|
+
|
|
163
|
+
watch(() => props.visible, async (newValue) => {
|
|
164
|
+
if (newValue) {
|
|
165
|
+
document.body.style.overflow = 'hidden';
|
|
166
|
+
didLockScroll = true;
|
|
167
|
+
emit('show');
|
|
168
|
+
await nextTick();
|
|
169
|
+
// Focus the overlay for keyboard events
|
|
170
|
+
const overlay = document.querySelector('.lightbox__overlay') as HTMLElement;
|
|
171
|
+
overlay?.focus();
|
|
172
|
+
} else {
|
|
173
|
+
if (didLockScroll) {
|
|
174
|
+
document.body.style.overflow = '';
|
|
175
|
+
didLockScroll = false;
|
|
176
|
+
}
|
|
177
|
+
isZoomed.value = false;
|
|
178
|
+
emit('hide');
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const close = () => {
|
|
183
|
+
emit('update:visible', false);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const onOverlayClick = () => {
|
|
187
|
+
if (props.closeOnClickOutside) {
|
|
188
|
+
close();
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const prev = () => {
|
|
193
|
+
if (isZoomed.value) return;
|
|
194
|
+
slideDirection.value = 'lightbox-slide-right';
|
|
195
|
+
if (currentIndex.value > 0) {
|
|
196
|
+
currentIndex.value--;
|
|
197
|
+
} else {
|
|
198
|
+
currentIndex.value = props.images.length - 1;
|
|
199
|
+
}
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const next = () => {
|
|
203
|
+
if (isZoomed.value) return;
|
|
204
|
+
slideDirection.value = 'lightbox-slide-left';
|
|
205
|
+
if (currentIndex.value < props.images.length - 1) {
|
|
206
|
+
currentIndex.value++;
|
|
207
|
+
} else {
|
|
208
|
+
currentIndex.value = 0;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const goTo = (index: number) => {
|
|
213
|
+
slideDirection.value = index > currentIndex.value ? 'lightbox-slide-left' : 'lightbox-slide-right';
|
|
214
|
+
isZoomed.value = false;
|
|
215
|
+
currentIndex.value = index;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const toggleZoom = () => {
|
|
219
|
+
if (!props.zoom) return;
|
|
220
|
+
isZoomed.value = !isZoomed.value;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const onImageLoad = () => {
|
|
224
|
+
// Could emit event or handle loading state
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// Touch/swipe support
|
|
228
|
+
const onTouchStart = (e: TouchEvent) => {
|
|
229
|
+
if (isZoomed.value) return;
|
|
230
|
+
touchStartX.value = e.touches[0].clientX;
|
|
231
|
+
touchStartY.value = e.touches[0].clientY;
|
|
232
|
+
touchDeltaX.value = 0;
|
|
233
|
+
isSwiping.value = false;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const onTouchMove = (e: TouchEvent) => {
|
|
237
|
+
if (isZoomed.value) return;
|
|
238
|
+
const deltaX = e.touches[0].clientX - touchStartX.value;
|
|
239
|
+
const deltaY = e.touches[0].clientY - touchStartY.value;
|
|
240
|
+
|
|
241
|
+
// Only swipe horizontally
|
|
242
|
+
if (Math.abs(deltaX) > Math.abs(deltaY)) {
|
|
243
|
+
isSwiping.value = true;
|
|
244
|
+
touchDeltaX.value = deltaX;
|
|
245
|
+
e.preventDefault();
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const onTouchEnd = () => {
|
|
250
|
+
if (!isSwiping.value || isZoomed.value) return;
|
|
251
|
+
const threshold = 50;
|
|
252
|
+
if (touchDeltaX.value > threshold) {
|
|
253
|
+
prev();
|
|
254
|
+
} else if (touchDeltaX.value < -threshold) {
|
|
255
|
+
next();
|
|
256
|
+
}
|
|
257
|
+
isSwiping.value = false;
|
|
258
|
+
touchDeltaX.value = 0;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
onUnmounted(() => {
|
|
262
|
+
if (didLockScroll) {
|
|
263
|
+
document.body.style.overflow = '';
|
|
264
|
+
didLockScroll = false;
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
defineExpose({
|
|
269
|
+
prev,
|
|
270
|
+
next,
|
|
271
|
+
goTo,
|
|
272
|
+
close,
|
|
273
|
+
});
|
|
274
|
+
</script>
|
package/src/types/index.ts
CHANGED
|
@@ -707,10 +707,14 @@ export interface GalleriaProps {
|
|
|
707
707
|
showThumbnails?: boolean
|
|
708
708
|
showItemNavigators?: boolean
|
|
709
709
|
showIndicators?: boolean
|
|
710
|
+
showCounter?: boolean
|
|
711
|
+
showFullscreenThumbnails?: boolean
|
|
712
|
+
enableZoom?: boolean
|
|
710
713
|
circular?: boolean
|
|
711
714
|
autoplay?: boolean
|
|
712
715
|
autoplayInterval?: number
|
|
713
716
|
thumbnailsPosition?: Position
|
|
717
|
+
transition?: 'fade' | 'slide' | 'none'
|
|
714
718
|
}
|
|
715
719
|
|
|
716
720
|
export interface GalleriaEmits {
|
|
@@ -956,6 +960,34 @@ export interface KnobEmits {
|
|
|
956
960
|
(e: 'change', payload: { value: number }): void
|
|
957
961
|
}
|
|
958
962
|
|
|
963
|
+
// ============================================================
|
|
964
|
+
// Lightbox
|
|
965
|
+
// ============================================================
|
|
966
|
+
|
|
967
|
+
export interface LightboxImage {
|
|
968
|
+
src: string
|
|
969
|
+
thumbnail?: string
|
|
970
|
+
alt?: string
|
|
971
|
+
[key: string]: unknown
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
export interface LightboxProps {
|
|
975
|
+
images?: (string | LightboxImage)[]
|
|
976
|
+
visible?: boolean
|
|
977
|
+
activeIndex?: number
|
|
978
|
+
showThumbnails?: boolean
|
|
979
|
+
showCounter?: boolean
|
|
980
|
+
closeOnClickOutside?: boolean
|
|
981
|
+
zoom?: boolean
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
export interface LightboxEmits {
|
|
985
|
+
(e: 'update:visible', value: boolean): void
|
|
986
|
+
(e: 'update:activeIndex', value: number): void
|
|
987
|
+
(e: 'show'): void
|
|
988
|
+
(e: 'hide'): void
|
|
989
|
+
}
|
|
990
|
+
|
|
959
991
|
// ============================================================
|
|
960
992
|
// ListBox
|
|
961
993
|
// ============================================================
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lightbox": {
|
|
3
|
+
"overlay": {
|
|
4
|
+
"background": {
|
|
5
|
+
"value": "rgba(0, 0, 0, 0.92)",
|
|
6
|
+
"type": "color",
|
|
7
|
+
"description": "Lightbox overlay backdrop color"
|
|
8
|
+
}
|
|
9
|
+
},
|
|
10
|
+
"zIndex": {
|
|
11
|
+
"overlay": {
|
|
12
|
+
"value": "2000",
|
|
13
|
+
"type": "number",
|
|
14
|
+
"description": "Overlay z-index (above dialogs)"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"nav": {
|
|
18
|
+
"size": {
|
|
19
|
+
"value": "48px",
|
|
20
|
+
"type": "dimension",
|
|
21
|
+
"description": "Navigation button size"
|
|
22
|
+
},
|
|
23
|
+
"iconSize": {
|
|
24
|
+
"value": "24px",
|
|
25
|
+
"type": "dimension",
|
|
26
|
+
"description": "Navigation icon size"
|
|
27
|
+
},
|
|
28
|
+
"color": {
|
|
29
|
+
"default": {
|
|
30
|
+
"value": "rgba(255, 255, 255, 0.8)",
|
|
31
|
+
"type": "color",
|
|
32
|
+
"description": "Nav button color"
|
|
33
|
+
},
|
|
34
|
+
"hover": {
|
|
35
|
+
"value": "#ffffff",
|
|
36
|
+
"type": "color",
|
|
37
|
+
"description": "Nav button hover color"
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
"background": {
|
|
41
|
+
"default": {
|
|
42
|
+
"value": "rgba(255, 255, 255, 0.1)",
|
|
43
|
+
"type": "color",
|
|
44
|
+
"description": "Nav button background"
|
|
45
|
+
},
|
|
46
|
+
"hover": {
|
|
47
|
+
"value": "rgba(255, 255, 255, 0.2)",
|
|
48
|
+
"type": "color",
|
|
49
|
+
"description": "Nav button hover background"
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"radius": {
|
|
53
|
+
"value": "{radius.full}",
|
|
54
|
+
"type": "dimension",
|
|
55
|
+
"description": "Nav button border radius"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"closeButton": {
|
|
59
|
+
"size": {
|
|
60
|
+
"value": "40px",
|
|
61
|
+
"type": "dimension",
|
|
62
|
+
"description": "Close button size"
|
|
63
|
+
},
|
|
64
|
+
"iconSize": {
|
|
65
|
+
"value": "20px",
|
|
66
|
+
"type": "dimension",
|
|
67
|
+
"description": "Close icon size"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"counter": {
|
|
71
|
+
"color": {
|
|
72
|
+
"value": "rgba(255, 255, 255, 0.7)",
|
|
73
|
+
"type": "color",
|
|
74
|
+
"description": "Counter text color"
|
|
75
|
+
},
|
|
76
|
+
"fontSize": {
|
|
77
|
+
"value": "14px",
|
|
78
|
+
"type": "dimension",
|
|
79
|
+
"description": "Counter font size"
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"thumbnail": {
|
|
83
|
+
"size": {
|
|
84
|
+
"value": "64px",
|
|
85
|
+
"type": "dimension",
|
|
86
|
+
"description": "Thumbnail width/height"
|
|
87
|
+
},
|
|
88
|
+
"gap": {
|
|
89
|
+
"value": "{space.xs}",
|
|
90
|
+
"type": "dimension",
|
|
91
|
+
"description": "Gap between thumbnails"
|
|
92
|
+
},
|
|
93
|
+
"radius": {
|
|
94
|
+
"value": "{radius.default}",
|
|
95
|
+
"type": "dimension",
|
|
96
|
+
"description": "Thumbnail border radius"
|
|
97
|
+
},
|
|
98
|
+
"border": {
|
|
99
|
+
"active": {
|
|
100
|
+
"value": "#ffffff",
|
|
101
|
+
"type": "color",
|
|
102
|
+
"description": "Active thumbnail border color"
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
"opacity": {
|
|
106
|
+
"default": {
|
|
107
|
+
"value": "0.5",
|
|
108
|
+
"type": "number",
|
|
109
|
+
"description": "Inactive thumbnail opacity"
|
|
110
|
+
},
|
|
111
|
+
"active": {
|
|
112
|
+
"value": "1",
|
|
113
|
+
"type": "number",
|
|
114
|
+
"description": "Active thumbnail opacity"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|