@for-the-people-initiative/design-system 1.3.2 → 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.
@@ -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;
@@ -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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@for-the-people-initiative/design-system",
3
3
  "private": false,
4
- "version": "1.3.2",
4
+ "version": "1.3.4",
5
5
  "type": "module",
6
6
  "description": "Design system with design tokens, SCSS utilities, and Vue 3 components",
7
7
  "author": "For The People",
@@ -18,6 +18,7 @@
18
18
  "dist",
19
19
  "src/scss",
20
20
  "src/components",
21
+ "src/types",
21
22
  "tokens"
22
23
  ],
23
24
  "exports": {
@@ -283,7 +283,139 @@ $c: galleria;
283
283
  right: 16px;
284
284
  }
285
285
 
286
- // Transition animations
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 class="galleria__preview" @click="onPreviewClick">
6
- <img
7
- v-if="activeItem"
8
- :src="activeItem.src || activeItem"
9
- :alt="activeItem.alt || `Image ${activeIndex + 1}`"
10
- class="galleria__preview-image"
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
- @click="closeFullscreen"
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
- <img
117
- v-if="activeItem"
118
- :src="activeItem.src || activeItem"
119
- :alt="activeItem.alt || `Image ${activeIndex + 1}`"
120
- class="galleria__fullscreen-image"
121
- @click.stop
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) {