@iqual/nuxt-icms 1.2.1 → 1.2.2

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.
Files changed (38) hide show
  1. package/components/DesignSystem/Molecules/MediaLightbox.vue +273 -0
  2. package/components/DesignSystem/Molecules/MediaSlider.vue +19 -25
  3. package/components/DesignSystem/README.md +489 -0
  4. package/components/Media/RemoteVideo/index.vue +3 -3
  5. package/components/Node/Icms/Page/index.vue +5 -3
  6. package/components/Paragraph/Card/Layout/index.vue +2 -2
  7. package/components/Paragraph/Contact/index.vue +1 -2
  8. package/components/Paragraph/Downloads/index.vue +1 -2
  9. package/components/Paragraph/FAQ/Layout/index.vue +1 -2
  10. package/components/Paragraph/FlipTeasers/index.vue +2 -2
  11. package/components/Paragraph/Gallery/index.vue +16 -14
  12. package/components/Paragraph/IconText/Layout/index.vue +1 -2
  13. package/components/Paragraph/Table/index.vue +2 -2
  14. package/components/Paragraph/Team/Layout/index.vue +2 -2
  15. package/components/Paragraph/Teasers/index.vue +3 -3
  16. package/components/Paragraph/Timeline/Layout/index.vue +6 -3
  17. package/components/Paragraph/iFrame/index.vue +16 -2
  18. package/components/Section/Footer/index.vue +15 -14
  19. package/components/Section/Header/GlobalMenu.vue +34 -34
  20. package/components/Section/Header/MetaMenu.vue +17 -19
  21. package/components/Section/Header/index.vue +21 -22
  22. package/composables/useGlobalConfig.ts +57 -0
  23. package/composables/useGraphqlCacheability.ts +22 -0
  24. package/composables/useInitData.ts +20 -112
  25. package/composables/useMediaLightbox.ts +114 -0
  26. package/composables/useMenu.ts +66 -0
  27. package/modules/dynamicEntityComponentResolver.ts +0 -20
  28. package/package.json +1 -2
  29. package/plugins/graphqlMiddleware.ts +27 -68
  30. package/plugins/initData.ts +105 -0
  31. package/{composables → queries}/query.initData.graphql +2 -8
  32. package/queries/query.loadTranslations.graphql +5 -0
  33. package/types/initData.ts +1 -0
  34. package/components/Section/Header/SearchLink.vue +0 -26
  35. package/composables/useLightbox.ts +0 -81
  36. package/composables/useMainMenuLinks.ts +0 -69
  37. package/composables/useMetaMenuLinks.ts +0 -44
  38. package/types/fslightbox-vue.d.ts +0 -1
@@ -0,0 +1,273 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <Transition
4
+ enter-active-class="transition-opacity duration-300"
5
+ enter-from-class="opacity-0"
6
+ enter-to-class="opacity-100"
7
+ leave-active-class="transition-opacity duration-300"
8
+ leave-from-class="opacity-100"
9
+ leave-to-class="opacity-0"
10
+ >
11
+ <div
12
+ v-if="isOpen"
13
+ ref="lightboxContainer"
14
+ class="fixed inset-0 z-50 flex items-center justify-center bg-black/90 backdrop-blur-sm"
15
+ @click.self="$emit('close')"
16
+ >
17
+ <!-- Close button -->
18
+ <button
19
+ type="button"
20
+ class="absolute right-4 top-4 z-10 rounded-full bg-black/50 p-2 text-white transition-colors hover:bg-black/70 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-black"
21
+ aria-label="Close lightbox"
22
+ @click="$emit('close')"
23
+ >
24
+ <X :size="24" />
25
+ </button>
26
+
27
+ <!-- Loading spinner -->
28
+ <div
29
+ v-if="isLoading"
30
+ class="absolute inset-0 flex items-center justify-center"
31
+ aria-live="polite"
32
+ aria-label="Loading media"
33
+ >
34
+ <div class="h-8 w-8 animate-spin rounded-full border-2 border-white border-t-transparent"></div>
35
+ </div>
36
+
37
+ <!-- Carousel-based media container -->
38
+ <div class="relative h-full w-full max-h-[90vh] max-w-[90vw] flex items-center justify-center">
39
+ <Carousel
40
+ v-if="mediaObjects.length > 0"
41
+ :opts="carouselOptions"
42
+ orientation="horizontal"
43
+ class="w-full h-full flex flex-col"
44
+ @init-api="onCarouselInit"
45
+ >
46
+ <template #default="{ scrollPrev, scrollNext, canScrollPrev, canScrollNext }">
47
+ <!-- Navigation buttons using carousel methods -->
48
+ <button
49
+ v-if="mediaObjects.length > 1"
50
+ type="button"
51
+ class="absolute left-4 top-1/2 z-10 -translate-y-1/2 rounded-full bg-black/50 p-2 text-white transition-colors hover:bg-black/70 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-black disabled:opacity-50 disabled:cursor-not-allowed"
52
+ aria-label="Previous media"
53
+ :disabled="!canScrollPrev"
54
+ @click="scrollPrev"
55
+ >
56
+ <ChevronLeft :size="24" />
57
+ </button>
58
+
59
+ <button
60
+ v-if="mediaObjects.length > 1"
61
+ type="button"
62
+ class="absolute right-4 top-1/2 z-10 -translate-y-1/2 rounded-full bg-black/50 p-2 text-white transition-colors hover:bg-black/70 focus:outline-none focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-black disabled:opacity-50 disabled:cursor-not-allowed"
63
+ aria-label="Next media"
64
+ :disabled="!canScrollNext"
65
+ @click="scrollNext"
66
+ >
67
+ <ChevronRight :size="24" />
68
+ </button>
69
+
70
+ <CarouselContent class="h-full flex-1">
71
+ <CarouselItem
72
+ v-for="(media, index) in mediaObjects"
73
+ :key="index"
74
+ class="flex h-full w-full items-center justify-center min-h-0"
75
+ >
76
+ <!-- Use Media component directly -->
77
+ <Media
78
+ :media="media"
79
+ :show-caption="false"
80
+ :video-autoplay="index === currentIndex && media.type === 'MediaVideo'"
81
+ :video-muted="true"
82
+ :video-controls="true"
83
+ :media-class="getMediaWrapperClass(media)"
84
+ :element-class="getMediaElementClass(media)"
85
+ class="max-h-full max-w-full"
86
+ @load="handleMediaLoad"
87
+ @error="handleMediaError"
88
+ />
89
+
90
+ <!-- Fallback/error state -->
91
+ <div
92
+ v-if="!['MediaImage', 'MediaVideo', 'MediaRemoteVideo', 'MediaIcon'].includes(media.type)"
93
+ class="flex flex-col items-center text-white"
94
+ >
95
+ <AlertCircle :size="48" class="mb-4 text-red-400" />
96
+ <p class="text-lg">Unable to display this media</p>
97
+ <p class="text-sm text-gray-400">Unsupported format: {{ media.type }}</p>
98
+ </div>
99
+ </CarouselItem>
100
+ </CarouselContent>
101
+ </template>
102
+ </Carousel>
103
+ </div>
104
+
105
+ <!-- Caption display -->
106
+ <div
107
+ v-if="currentCaption"
108
+ class="absolute bottom-0 left-4 h-[5vh] right-4 z-10 text-center leading-[5vh]"
109
+ >
110
+ <p class="text-white">
111
+ {{ currentCaption }}
112
+ </p>
113
+ </div>
114
+
115
+ <!-- Counter -->
116
+ <div
117
+ v-if="mediaObjects.length > 1"
118
+ class="absolute top-2 left-2 z-10 rounded px-3 py-1 text-sm text-white"
119
+ aria-live="polite"
120
+ >
121
+ {{ currentIndex + 1 }} / {{ mediaObjects.length }}
122
+ </div>
123
+ </div>
124
+ </Transition>
125
+ </Teleport>
126
+ </template>
127
+
128
+ <script lang="ts" setup>
129
+ import { ref, computed, nextTick, onMounted, onUnmounted, watch } from 'vue'
130
+ import { ChevronLeft, ChevronRight, X, AlertCircle } from 'lucide-vue-next'
131
+ import { Carousel, CarouselContent, CarouselItem } from '@iqual/nuxt-icms/components/ui/carousel'
132
+ import type { CarouselApi } from '@iqual/nuxt-icms/components/ui/carousel'
133
+ import { Media } from '#components'
134
+ import type { MediaFragment } from '#graphql-operations'
135
+
136
+ interface Props {
137
+ mediaObjects: MediaFragment[]
138
+ isOpen: boolean
139
+ currentIndex: number
140
+ captions?: string[]
141
+ }
142
+
143
+ interface Emits {
144
+ (e: 'close'): void
145
+ (e: 'update:currentIndex', index: number): void
146
+ }
147
+
148
+ const props = withDefaults(defineProps<Props>(), {
149
+ captions: () => []
150
+ })
151
+
152
+ const emit = defineEmits<Emits>()
153
+
154
+ // Refs
155
+ const lightboxContainer = ref<HTMLElement>()
156
+ const isLoading = ref(false)
157
+ const carouselApi = ref<CarouselApi>()
158
+
159
+ // Computed properties
160
+ const currentCaption = computed(() => props.captions[props.currentIndex] || '')
161
+
162
+ // Carousel configuration
163
+ const carouselOptions = computed(() => ({
164
+ startIndex: props.currentIndex,
165
+ loop: true,
166
+ skipSnaps: false,
167
+ dragFree: false,
168
+ containScroll: 'trimSnaps' as const
169
+ }))
170
+
171
+ // Helper to get wrapper classes for media containers
172
+ const getMediaWrapperClass = (media: MediaFragment) => {
173
+ switch (media.type) {
174
+ case 'MediaRemoteVideo':
175
+ return 'w-full h-full flex items-center justify-center'
176
+ default:
177
+ return 'w-full h-full flex items-center justify-center'
178
+ }
179
+ }
180
+
181
+ // Helper to get element classes for media
182
+ const getMediaElementClass = (media: MediaFragment) => {
183
+ switch (media.type) {
184
+ case 'MediaImage':
185
+ return 'max-h-full max-w-full object-contain'
186
+ case 'MediaVideo':
187
+ return 'max-h-full max-w-full'
188
+ case 'MediaRemoteVideo':
189
+ return 'w-full h-full max-w-[90vw] max-h-[90vh]'
190
+ default:
191
+ return ''
192
+ }
193
+ }
194
+
195
+ // Media loading handlers
196
+ const handleMediaLoad = () => {
197
+ isLoading.value = false
198
+ }
199
+
200
+ const handleMediaError = () => {
201
+ isLoading.value = false
202
+ console.warn('Failed to load media item')
203
+ }
204
+
205
+ // Carousel event handlers
206
+ const onCarouselInit = (api: CarouselApi) => {
207
+ carouselApi.value = api
208
+
209
+ if (api) {
210
+ api.on('select', () => {
211
+ const selectedIndex = api.selectedScrollSnap()
212
+ emit('update:currentIndex', selectedIndex)
213
+ })
214
+ }
215
+ }
216
+
217
+ // Update carousel when currentIndex changes externally
218
+ const updateCarousel = async () => {
219
+ if (carouselApi.value) {
220
+ await nextTick()
221
+ carouselApi.value.scrollTo(props.currentIndex, false)
222
+ }
223
+ }
224
+
225
+ // Watch for external index changes
226
+ watch(() => props.currentIndex, updateCarousel)
227
+
228
+ // Keyboard navigation
229
+ const handleKeydown = (event: KeyboardEvent) => {
230
+ if (!props.isOpen) return
231
+
232
+ switch (event.key) {
233
+ case 'Escape':
234
+ emit('close')
235
+ break
236
+ case 'ArrowLeft':
237
+ event.preventDefault()
238
+ carouselApi.value?.scrollPrev()
239
+ break
240
+ case 'ArrowRight':
241
+ event.preventDefault()
242
+ carouselApi.value?.scrollNext()
243
+ break
244
+ }
245
+ }
246
+
247
+ // Focus management
248
+ const focusLightbox = () => {
249
+ nextTick(() => {
250
+ lightboxContainer.value?.focus()
251
+ })
252
+ }
253
+
254
+ // Lifecycle
255
+ onMounted(() => {
256
+ document.addEventListener('keydown', handleKeydown)
257
+ if (props.isOpen) {
258
+ focusLightbox()
259
+ }
260
+ })
261
+
262
+ onUnmounted(() => {
263
+ document.removeEventListener('keydown', handleKeydown)
264
+ })
265
+
266
+ // Watch for lightbox opening
267
+ watch(() => props.isOpen, (isOpen) => {
268
+ if (isOpen) {
269
+ focusLightbox()
270
+ updateCarousel()
271
+ }
272
+ })
273
+ </script>
@@ -34,56 +34,50 @@
34
34
  </CarouselNext>
35
35
  </Carousel>
36
36
 
37
- <FsLightbox
38
- :toggler="toggleLightbox"
39
- :sources="sources"
40
- :slide="currentSlide"
41
- :customAttributes="customAttributes"
37
+ <!-- MediaLightbox with Media component integration -->
38
+ <DesignSystemMoleculesMediaLightbox
39
+ :media-objects="media?.filter((item): item is MediaFragment => Boolean(item)) || []"
40
+ :is-open="isOpen"
41
+ :current-index="currentIndex"
42
42
  :captions="captions"
43
+ @close="closeLightbox"
44
+ @update:current-index="setCurrentIndex"
43
45
  />
44
46
  </div>
45
47
  </template>
46
48
 
47
49
  <script lang="ts" setup>
48
50
  import cn from '@iqual/nuxt-icms/composables/useCn'
51
+ import { useMediaLightbox } from '@iqual/nuxt-icms/composables/useMediaLightbox'
49
52
  import { tv } from 'tailwind-variants'
50
53
  import { ChevronLeft, ChevronRight } from 'lucide-vue-next'
51
54
  import type { MediaFragment } from '#graphql-operations'
52
- import FsLightbox from 'fslightbox-vue'
53
55
 
54
56
  const props = defineProps<{
55
57
  media?: (MediaFragment | null)[] | undefined
56
58
  sliderClass?: string
57
59
  mediaClass?: string
58
- colorScheme?: string
60
+ colorScheme?: 'white'
59
61
  showCaption?: boolean
60
62
  figcaptionClass?: string
61
63
  elementClass?: string
62
64
  }>()
63
65
 
64
66
  const {
65
- toggleLightbox,
66
- currentSlide,
67
+ isOpen,
68
+ currentIndex,
67
69
  openLightbox,
68
- sources,
69
- customAttributes,
70
+ closeLightbox,
71
+ setCurrentIndex,
70
72
  captions,
71
- } = useLightbox(props.media)
73
+ } = useMediaLightbox(props.media)
72
74
 
73
75
  const carouselButton = tv({
74
- base: 'size-10 border-none disabled:opacity-40 @sm:size:12 @md:size:16 [&_svg]:size-auto',
76
+ base: 'bg-white text-black ring-black',
75
77
  variants: {
76
78
  colorScheme: {
77
- none: 'bg-gray-50 text-black hover:bg-black hover:text-white dark:bg-gray-50 dark:text-black hover:dark:bg-black hover:dark:text-white',
78
- dark: 'bg-black text-white hover:bg-gray-50 hover:text-black dark:bg-black dark:text-white hover:dark:bg-gray-50 hover:dark:text-black',
79
- light:
80
- 'bg-gray-50 text-black hover:bg-black hover:text-white dark:bg-gray-50 dark:text-black hover:dark:bg-black hover:dark:text-white',
81
- accent:
82
- 'bg-primary-600 text-white hover:bg-primary-800 hover:text-white dark:bg-primary-600 dark:text-white hover:dark:bg-primary-800 hover:dark:text-white',
83
- },
84
- },
85
- defaultVariants: {
86
- colorScheme: 'none',
87
- },
79
+ white: 'bg-black text-white ring-white'
80
+ }
81
+ }
88
82
  })
89
- </script>
83
+ </script>