@iqual/nuxt-icms 1.6.1 → 1.6.2-rc.1

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 (260) hide show
  1. package/README.md +86 -0
  2. package/__tests__/anchors.spec.ts +43 -0
  3. package/__tests__/contract/blokkliComponents.spec.ts +59 -0
  4. package/__tests__/contract/componentFragments.spec.ts +24 -0
  5. package/__tests__/contract/languages.spec.ts +63 -0
  6. package/__tests__/contract/project.ts +144 -0
  7. package/__tests__/hreflang.spec.ts +39 -0
  8. package/{tests → __tests__}/pages.spec.ts +14 -5
  9. package/app.config.ts +15 -0
  10. package/app.vue +3 -21
  11. package/assets/css/backend.css +56 -0
  12. package/assets/css/base/base.css +2 -2
  13. package/assets/css/base/typography.css +2 -2
  14. package/assets/css/ckeditor.css +3 -4
  15. package/assets/css/components/decorations.css +1 -1
  16. package/assets/css/components/editor.css +1 -1
  17. package/assets/css/components/grid-container.css +19 -19
  18. package/assets/css/fe-fixes.css +6 -12
  19. package/assets/css/theme/colors/color-primary.css +33 -11
  20. package/assets/css/theme/colors/color-secondary.css +33 -11
  21. package/assets/css/utilities/utilities.css +1 -1
  22. package/assets/symbols/anchor.svg +3 -0
  23. package/blokkli.editAdapter.ts +56 -0
  24. package/bundles.ts +6 -6
  25. package/cache/__tests__/pageCache.spec.ts +540 -0
  26. package/cache/cacheTagRegistry.ts +124 -0
  27. package/cache/enabledForRequest.ts +21 -0
  28. package/cache/multiCache.serverOptions.ts +58 -0
  29. package/cache/pageCache.ts +360 -0
  30. package/components/Blokkli/Anchor/icon.svg +3 -0
  31. package/components/Blokkli/Anchor/index.vue +13 -0
  32. package/components/Blokkli/AnchorNavigation/icon.svg +8 -0
  33. package/components/Blokkli/AnchorNavigation/index.vue +9 -0
  34. package/components/Blokkli/Card/Element/index.vue +0 -1
  35. package/components/Blokkli/Collapsible/Element/index.vue +5 -0
  36. package/components/Blokkli/FAQ/Element/index.vue +0 -1
  37. package/components/Blokkli/Fragment/Separator/icon.svg +4 -0
  38. package/components/Blokkli/Fragment/Separator/index.vue +87 -0
  39. package/components/Blokkli/Gallery/index.vue +1 -3
  40. package/components/Blokkli/IconText/Element/index.vue +0 -1
  41. package/components/Blokkli/Introduction/index.vue +0 -1
  42. package/components/Blokkli/Listing/icon.svg +9 -0
  43. package/components/Blokkli/Listing/index.vue +24 -0
  44. package/components/Blokkli/Logo/Element/index.vue +0 -1
  45. package/components/Blokkli/Table/index.vue +0 -1
  46. package/components/Blokkli/Team/Element/index.vue +0 -1
  47. package/components/Blokkli/Team/Layout/index.vue +0 -1
  48. package/components/Blokkli/Testimonial/Element/index.vue +0 -1
  49. package/components/Blokkli/Text/index.vue +0 -1
  50. package/components/DesignSystem/Atoms/Button.vue +3 -0
  51. package/components/DesignSystem/Atoms/GridRow.vue +3 -1
  52. package/components/DesignSystem/Molecules/ButtonGroup.vue +5 -4
  53. package/components/DesignSystem/Molecules/IconText.vue +6 -6
  54. package/components/DesignSystem/Molecules/MediaLightbox.vue +8 -8
  55. package/components/DesignSystem/Molecules/MediaSlider.vue +9 -11
  56. package/components/DesignSystem/Molecules/Partnership.vue +7 -4
  57. package/components/DesignSystem/Molecules/Table.vue +35 -19
  58. package/components/DesignSystem/Molecules/TitleComposition.vue +46 -15
  59. package/components/DesignSystem/Molecules/iFrame.vue +2 -2
  60. package/components/DesignSystem/Organisms/CallToAction.vue +8 -7
  61. package/components/DesignSystem/Organisms/CallToActionFeature.vue +8 -7
  62. package/components/DesignSystem/Organisms/CallToActionFullwidth.vue +9 -8
  63. package/components/DesignSystem/Organisms/ContentBlock.vue +26 -16
  64. package/components/DesignSystem/Organisms/FacetedListing.vue +38 -17
  65. package/components/DesignSystem/Organisms/FactCard.vue +6 -7
  66. package/components/DesignSystem/Organisms/FeatureCard.vue +14 -14
  67. package/components/DesignSystem/Organisms/FlipTeaser.vue +19 -19
  68. package/components/DesignSystem/Organisms/HeaderHero.vue +11 -11
  69. package/components/DesignSystem/Organisms/HeaderTextImage.vue +10 -10
  70. package/components/DesignSystem/Organisms/LinkList.vue +20 -12
  71. package/components/DesignSystem/Organisms/LogoElement.vue +7 -4
  72. package/components/DesignSystem/Organisms/Map.vue +1 -1
  73. package/components/DesignSystem/Organisms/SearchResults.vue +15 -6
  74. package/components/DesignSystem/Organisms/SpotlightCard.vue +18 -18
  75. package/components/DesignSystem/Organisms/TeamElement.vue +19 -18
  76. package/components/DesignSystem/Organisms/TeaserCard.vue +16 -17
  77. package/components/DesignSystem/Organisms/TeaserSimple.vue +4 -31
  78. package/components/DesignSystem/Organisms/Testimonial.vue +13 -13
  79. package/components/DesignSystem/Organisms/TextCard.vue +10 -10
  80. package/components/DesignSystem/Organisms/TimelineElement.vue +20 -19
  81. package/components/DesignSystem/Organisms/TwoColumnTextElement.vue +5 -5
  82. package/components/DesignSystem/README.md +59 -40
  83. package/components/Drupal/Breadcrumb/index.vue +25 -12
  84. package/components/Drupal/Messages/index.vue +9 -4
  85. package/components/Drupal/Webform/index.vue +271 -51
  86. package/components/Drupal/Webform/mutation.graphql +17 -1
  87. package/components/Form/HiddenInput.vue +1 -4
  88. package/components/Form/SelectInput.vue +1 -1
  89. package/components/Form/SubmitButton.vue +2 -2
  90. package/components/Form/TextAreaInput.vue +3 -3
  91. package/components/Form/TextEmailInput.vue +1 -1
  92. package/components/Form/TextInput.vue +1 -1
  93. package/components/Form/TextPhoneInput.vue +1 -1
  94. package/components/Form/WebformMarkup.vue +1 -1
  95. package/components/Form/composables/useFormDefaultValue.ts +9 -13
  96. package/components/Form/composables/useFormElement.ts +5 -3
  97. package/components/Form/utils/types.ts +3 -3
  98. package/components/Media/Image/fragment.graphql +4 -0
  99. package/components/Media/Image/index.vue +27 -17
  100. package/components/Media/RemoteVideo/index.vue +3 -3
  101. package/components/Media/Video/index.vue +1 -1
  102. package/components/Node/Icms/Page/SearchResult/fragment.graphql +1 -1
  103. package/components/Node/Icms/Page/SearchResult/index.vue +2 -2
  104. package/components/Node/Icms/Page/Teaser/index.vue +1 -1
  105. package/components/Node/Icms/Page/index.vue +8 -2
  106. package/components/Paragraph/Anchor/fragment.graphql +4 -0
  107. package/components/Paragraph/Anchor/index.vue +48 -0
  108. package/components/Paragraph/AnchorNavigation/fragment.graphql +3 -0
  109. package/components/Paragraph/AnchorNavigation/index.vue +154 -0
  110. package/components/Paragraph/BlokkliFragment/fragment.graphql +5 -0
  111. package/components/Paragraph/Button/Element/fragment.graphql +1 -1
  112. package/components/Paragraph/Button/Element/index.vue +13 -8
  113. package/components/Paragraph/Card/Element/index.vue +1 -1
  114. package/components/Paragraph/Card/Layout/index.vue +9 -9
  115. package/components/Paragraph/Collapsible/Element/index.vue +2 -2
  116. package/components/Paragraph/Collapsible/Layout/index.vue +19 -18
  117. package/components/Paragraph/Contact/index.vue +17 -17
  118. package/components/Paragraph/Downloads/fragment.graphql +1 -1
  119. package/components/Paragraph/Downloads/index.vue +13 -13
  120. package/components/Paragraph/FAQ/Element/index.vue +4 -3
  121. package/components/Paragraph/FAQ/Layout/index.vue +42 -21
  122. package/components/Paragraph/FromLibrary/fragment.graphql +17 -0
  123. package/components/Paragraph/Gallery/index.vue +14 -14
  124. package/components/Paragraph/HeroImage/index.vue +26 -19
  125. package/components/Paragraph/IconText/Element/index.vue +4 -4
  126. package/components/Paragraph/IconText/Layout/index.vue +12 -12
  127. package/components/Paragraph/Introduction/index.vue +17 -17
  128. package/components/Paragraph/LinkLists/Element/fragment.graphql +1 -1
  129. package/components/Paragraph/LinkLists/Element/index.vue +5 -5
  130. package/components/Paragraph/LinkLists/Layout/index.vue +9 -9
  131. package/components/Paragraph/Listing/fragment.graphql +14 -0
  132. package/components/Paragraph/Listing/index.vue +91 -0
  133. package/components/Paragraph/Logo/Element/fragment.graphql +1 -1
  134. package/components/Paragraph/Logo/Element/index.vue +7 -4
  135. package/components/Paragraph/Media/index.vue +23 -21
  136. package/components/Paragraph/Partnerships/Element/fragment.graphql +1 -1
  137. package/components/Paragraph/Partnerships/Element/index.vue +10 -7
  138. package/components/Paragraph/Partnerships/Layout/index.vue +13 -13
  139. package/components/Paragraph/SingleTestimonial/index.vue +13 -13
  140. package/components/Paragraph/Table/index.vue +12 -12
  141. package/components/Paragraph/Team/Element/index.vue +16 -14
  142. package/components/Paragraph/Team/Layout/index.vue +9 -9
  143. package/components/Paragraph/Teasers/index.vue +14 -17
  144. package/components/Paragraph/Testimonial/Element/index.vue +19 -18
  145. package/components/Paragraph/Testimonial/Layout/index.vue +24 -18
  146. package/components/Paragraph/Text/fragment.graphql +1 -1
  147. package/components/Paragraph/Text/index.vue +8 -8
  148. package/components/Paragraph/TextMedia/index.vue +22 -22
  149. package/components/Paragraph/Timeline/Element/index.vue +19 -19
  150. package/components/Paragraph/Timeline/Layout/index.vue +51 -20
  151. package/components/Paragraph/TwoColumnText/Element/index.vue +13 -13
  152. package/components/Paragraph/TwoColumnText/Layout/index.vue +1 -1
  153. package/components/Paragraph/iFrame/index.vue +8 -8
  154. package/components/Search/Facets/FacetDropdownUI.vue +25 -31
  155. package/components/Search/Facets/FacetedSearchUI.vue +72 -48
  156. package/components/Search/Facets/fragment.graphql +20 -15
  157. package/components/Section/Footer/index.vue +55 -32
  158. package/components/Section/Header/Buttons.vue +4 -5
  159. package/components/Section/Header/DesktopMainMenu.vue +5 -4
  160. package/components/Section/Header/DesktopMainMenuDropdown.vue +5 -5
  161. package/components/Section/Header/LanguageDropdown.vue +2 -3
  162. package/components/Section/Header/Logo.vue +3 -3
  163. package/components/Section/Header/MainButtons.vue +4 -5
  164. package/components/Section/Header/MetaButtons.vue +5 -6
  165. package/components/Section/Header/MetaMenu.vue +1 -1
  166. package/components/Section/Header/MobileMainMenu.vue +1 -1
  167. package/components/Section/Header/MobileMainMenuItem.vue +2 -2
  168. package/components/Section/Header/index.vue +6 -13
  169. package/composables/__tests__/useImageSources.spec.ts +91 -0
  170. package/composables/breadcrumb.ts +12 -7
  171. package/composables/useAccordionScroll.ts +12 -4
  172. package/composables/useAllowedFragments.ts +14 -0
  173. package/composables/useAnchors.ts +48 -0
  174. package/composables/useChildTitleWeight.ts +1 -1
  175. package/composables/useCurrentLangcodeEnum.ts +1 -1
  176. package/composables/useCurrentNodeId.ts +8 -0
  177. package/composables/useDrupalUrl.ts +15 -0
  178. package/composables/useEnrichFacetLabels.ts +8 -6
  179. package/composables/useEntityRegistry.ts +3 -3
  180. package/composables/useGlobalConfig.ts +1 -2
  181. package/composables/useGraphqlCacheability.ts +12 -1
  182. package/composables/useHreflangLinks.ts +54 -0
  183. package/composables/useImageSources.ts +70 -28
  184. package/composables/useInitData.ts +1 -2
  185. package/composables/useIsHomepage.ts +1 -1
  186. package/composables/useLinkUrl.ts +30 -0
  187. package/composables/useMediaLightbox.ts +62 -50
  188. package/composables/useMenu.ts +1 -3
  189. package/composables/useMessageBus.ts +14 -7
  190. package/composables/useMobileMenuOverlayBus.ts +1 -1
  191. package/composables/useNavigationOffset.ts +49 -6
  192. package/composables/usePageCache.ts +33 -0
  193. package/composables/useSiteShell.ts +11 -0
  194. package/composables/useTheme.ts +17 -13
  195. package/config/__tests__/stubs/nuxt-multi-cache-config.ts +2 -0
  196. package/config/blokkli.ts +7 -13
  197. package/config/easyTexts.ts +1 -1
  198. package/config/graphqlMiddleware.ts +11 -4
  199. package/config/index.ts +5 -2
  200. package/config/languageNegotiation.ts +1 -1
  201. package/config/multiCache.ts +6 -1
  202. package/config/postcss-drupal/postcss.config.cjs +2 -2
  203. package/config/runtimeConfig.ts +7 -2
  204. package/config/svgIconSprite.ts +1 -2
  205. package/config/vuepal.ts +16 -18
  206. package/error.vue +1 -22
  207. package/layouts/default.vue +2 -6
  208. package/layouts/error.vue +7 -15
  209. package/modules/blokkliFragments.ts +76 -0
  210. package/modules/dynamicEntityComponentResolver.ts +47 -47
  211. package/modules/dynamicParagraphResolver.ts +43 -8
  212. package/modules/pageCache.ts +106 -0
  213. package/nuxt.config.ts +62 -33
  214. package/package.json +20 -19
  215. package/pages/[...slug]/index.vue +25 -4
  216. package/pages/index.vue +3 -3
  217. package/pages/node/[nid]/index.vue +2 -3
  218. package/patches/@blokkli+editor+2.0.0-alpha.13.patch +15 -1
  219. package/plugins/currentNode.ts +17 -0
  220. package/plugins/graphqlMiddleware.ts +10 -2
  221. package/plugins/initData.ts +15 -7
  222. package/plugins/registerNodeIcmsPageComponents.ts +6 -2
  223. package/plugins/siteShell.client.ts +94 -0
  224. package/queries/facetTermLabels.graphql +1 -5
  225. package/queries/fragment.linkUrl.graphql +12 -0
  226. package/queries/query.contentSearch.graphql +17 -2
  227. package/queries/query.initData.graphql +1 -1
  228. package/scripts/create-shade.ts +13 -4
  229. package/scripts/update-icms.ts +3 -1
  230. package/server/__tests__/graphqlMiddleware.serverOptions.spec.ts +2 -2
  231. package/server/__tests__/turnstile.spec.ts +119 -0
  232. package/server/api/icms/blokkli/last-changed.get.ts +24 -0
  233. package/server/api/icon/[...params].ts +3 -9
  234. package/server/graphqlMiddleware.serverOptions.ts +9 -8
  235. package/server/helpers/index.ts +0 -1
  236. package/server/middleware/turnstile.ts +51 -0
  237. package/server/plugins/frameAncestors.ts +20 -0
  238. package/server/plugins/pageCache.ts +59 -0
  239. package/server/utils/__tests__/accessDeniedHandover.spec.ts +49 -0
  240. package/server/utils/__tests__/backendRequestHeaders.spec.ts +115 -0
  241. package/server/utils/accessDeniedHandover.ts +48 -0
  242. package/server/utils/backendRequestHeaders.ts +51 -0
  243. package/server/utils/cacheability.ts +4 -4
  244. package/shared/__tests__/blokkliState.spec.ts +54 -0
  245. package/shared/__tests__/csp.spec.ts +53 -0
  246. package/shared/__tests__/drupalUrl.spec.ts +50 -0
  247. package/shared/blokkliState.ts +40 -0
  248. package/shared/csp.ts +47 -0
  249. package/shared/drupalUrl.ts +30 -0
  250. package/tailwind.config.ts +0 -5
  251. package/types/breadcrumb.ts +22 -0
  252. package/types/global.ts +15 -14
  253. package/types/langcode.ts +16 -0
  254. package/types/layerConfig.ts +84 -0
  255. package/utils/pageCache.ts +95 -0
  256. package/utils/turnstile.ts +35 -0
  257. package/composables/useWebformEnrichment.ts +0 -68
  258. package/server/__tests__/graphqlMiddleware.ts +0 -41
  259. package/server/multiCache.serverOptions.ts +0 -32
  260. package/vitest.config.mts +0 -28
@@ -5,6 +5,12 @@ interface ImageSource {
5
5
  sizes?: string
6
6
  srcset: string
7
7
  media?: string
8
+ /**
9
+ * Intrinsic size of the largest candidate, rendered as the width/height
10
+ * attributes so the browser reserves the box before the image has loaded.
11
+ */
12
+ width?: number
13
+ height?: number
8
14
  }
9
15
 
10
16
  interface UseImageSourcesOptions {
@@ -14,15 +20,18 @@ interface UseImageSourcesOptions {
14
20
  }
15
21
 
16
22
  // Local implementation of parseSizes from @nuxt/image
17
- function parseSizes(input: Record<string, string | number> | string): Record<string, string> {
23
+ function parseSizes(
24
+ input: Record<string, string | number> | string,
25
+ ): Record<string, string> {
18
26
  const sizes: Record<string, string> = {}
19
- if (typeof input === "string") {
27
+ if (typeof input === 'string') {
20
28
  for (const entry of input.split(/[\s,]+/).filter((e) => e)) {
21
- const s = entry.split(":")
29
+ const s = entry.split(':')
30
+ const [key = '', value = ''] = s
22
31
  if (s.length !== 2) {
23
- sizes["1px"] = s[0].trim()
32
+ sizes['1px'] = key.trim()
24
33
  } else {
25
- sizes[s[0].trim()] = s[1].trim()
34
+ sizes[key.trim()] = value.trim()
26
35
  }
27
36
  }
28
37
  } else {
@@ -32,8 +41,14 @@ function parseSizes(input: Record<string, string | number> | string): Record<str
32
41
  }
33
42
 
34
43
  // Helper function, copied from NuxtImage.
35
- function getSizesVariant(key: string, size: string, hwRatio: number, img: any): ImageSizesVariant | undefined {
36
- const screenMaxWidth = (img.options.screens && img.options.screens[key]) || Number.parseInt(key)
44
+ function getSizesVariant(
45
+ key: string,
46
+ size: string,
47
+ hwRatio: number,
48
+ img: any,
49
+ ): ImageSizesVariant | undefined {
50
+ const screenMaxWidth =
51
+ (img.options.screens && img.options.screens[key]) || Number.parseInt(key)
37
52
  const isFluid = size.endsWith('vw')
38
53
  if (!isFluid && /^\d+$/.test(size)) {
39
54
  size = size + 'px'
@@ -112,44 +127,56 @@ export function useImageSources(options: UseImageSourcesOptions) {
112
127
  if (!sourceImage || sourceImage === baseUrl) return []
113
128
 
114
129
  const sources: ImageSource[] = []
115
- const imgSizes = imageSizesRef.value ?? Object.keys(img.options.screens).map(key => `${key}:100vw`).join(' ')
130
+ const imgSizes =
131
+ imageSizesRef.value ??
132
+ Object.keys(img.options.screens)
133
+ .map((key) => `${key}:100vw`)
134
+ .join(' ')
116
135
 
117
136
  // No aspect ratios provided.
118
137
  if (!imageAspectRatiosRef.value) {
119
138
  const imageSize = img.getSizes(sourceImage, {
120
139
  sizes: imgSizes,
121
140
  modifiers: {
122
- format: 'webp'
123
- }
141
+ format: 'webp',
142
+ },
143
+ })
144
+ // Cast the ImageSizes result to our ImageSource interface; the natural
145
+ // aspect ratio comes from the original file.
146
+ sources.push({
147
+ ...(imageSize as ImageSource),
148
+ width: mediaRef.value?.dimensions?.width ?? undefined,
149
+ height: mediaRef.value?.dimensions?.height ?? undefined,
124
150
  })
125
- // Cast the ImageSizes result to our ImageSource interface
126
- sources.push(imageSize as ImageSource)
127
151
  return sources
128
152
  }
129
153
 
130
154
  // Aspect ratios provided.
131
155
  const parsedAspectRatios = parseSizes(imageAspectRatiosRef.value)
132
156
  const parsedImageSizes = parseSizes(imgSizes)
133
- const densities = img.options.densities
134
- const keys = Object.keys(parsedImageSizes)
135
- const lastKey = keys[keys.length - 1]
136
157
 
137
- for (const keyAspectRatios in parsedAspectRatios) {
138
- const [l, h] = parsedAspectRatios[keyAspectRatios].split('/').map(Number)
139
- const hwRatio = (l && h) ? h / l : 0
158
+ for (const [keyAspectRatios, aspectRatio] of Object.entries(
159
+ parsedAspectRatios,
160
+ )) {
161
+ const [l, h] = aspectRatio.split('/').map(Number)
162
+ const hwRatio = l && h ? h / l : 0
140
163
  const sizeVariants = []
141
164
  const srcsetVariants = []
142
165
 
143
166
  let screenSizes = Object.keys(parsedImageSizes)
144
167
  if (Object.keys(parsedImageSizes).includes(keyAspectRatios)) {
145
- const minSizeIndex = Object.keys(parsedImageSizes).indexOf(keyAspectRatios)
168
+ const minSizeIndex =
169
+ Object.keys(parsedImageSizes).indexOf(keyAspectRatios)
146
170
  screenSizes = Object.keys(parsedImageSizes).slice(minSizeIndex)
147
171
  }
148
172
 
149
- for (const index in screenSizes) {
150
- const screenSize = screenSizes[index]
151
-
152
- const variant = getSizesVariant(screenSize, String(parsedImageSizes[screenSize]), hwRatio, img)
173
+ for (const screenSize of screenSizes) {
174
+ const variant = getSizesVariant(
175
+ screenSize,
176
+ String(parsedImageSizes[screenSize]),
177
+ hwRatio,
178
+ img,
179
+ )
153
180
  if (variant === undefined) {
154
181
  continue
155
182
  }
@@ -164,17 +191,23 @@ export function useImageSources(options: UseImageSourcesOptions) {
164
191
  // Add srcset variants and ignore densities (for current 'size' processed).
165
192
  srcsetVariants.push({
166
193
  width: variant._cWidth,
194
+ height: variant._cHeight,
167
195
  src: img(sourceImage, {
168
196
  width: variant._cWidth ? variant._cWidth : undefined,
169
197
  height: variant._cHeight ? variant._cHeight : undefined,
170
198
  fit: 'cover',
171
199
  format: 'webp',
172
- gravity: mediaRef.value?.focalPoint?.focalPoint ? `${mediaRef.value?.focalPoint?.focalPoint?.x}x${mediaRef.value?.focalPoint?.focalPoint?.y}` : 'auto'
200
+ gravity: mediaRef.value?.focalPoint?.focalPoint
201
+ ? `${mediaRef.value?.focalPoint?.focalPoint?.x}x${mediaRef.value?.focalPoint?.focalPoint?.y}`
202
+ : 'auto',
173
203
  }),
174
204
  })
175
205
 
176
206
  // Make sure to not create duplicate size variants for the same screen size.
177
- if (keyAspectRatios != screenSize && Object.keys(parsedAspectRatios).includes(screenSize)) {
207
+ if (
208
+ keyAspectRatios != screenSize &&
209
+ Object.keys(parsedAspectRatios).includes(screenSize)
210
+ ) {
178
211
  break
179
212
  }
180
213
  }
@@ -183,13 +216,22 @@ export function useImageSources(options: UseImageSourcesOptions) {
183
216
  finaliseSizeVariants(sizeVariants)
184
217
  finaliseSrcsetVariants(srcsetVariants)
185
218
 
186
- const sizesVal = sizeVariants.length ? sizeVariants.map(v => `${v.media ? v.media + ' ' : ''}${v.size}`).join(', ') : undefined
219
+ const sizesVal = sizeVariants.length
220
+ ? sizeVariants
221
+ .map((v) => `${v.media ? v.media + ' ' : ''}${v.size}`)
222
+ .join(', ')
223
+ : undefined
187
224
  const suffix = sizesVal ? 'w' : 'x'
188
- const srcsetVal = srcsetVariants.map(v => `${v.src} ${v.width}${suffix}`).join(', ')
225
+ const srcsetVal = srcsetVariants
226
+ .map((v) => `${v.src} ${v.width}${suffix}`)
227
+ .join(', ')
189
228
 
229
+ const largest = srcsetVariants[srcsetVariants.length - 1]
190
230
  const source: ImageSource = {
191
231
  sizes: sizesVal,
192
232
  srcset: srcsetVal,
233
+ width: largest?.height ? largest.width : undefined,
234
+ height: largest?.height ? largest.height : undefined,
193
235
  }
194
236
 
195
237
  if (keyAspectRatios != Object.keys(parsedAspectRatios).slice(-1)[0]) {
@@ -202,7 +244,7 @@ export function useImageSources(options: UseImageSourcesOptions) {
202
244
 
203
245
  return {
204
246
  placeHolderImage,
205
- imageSources
247
+ imageSources,
206
248
  }
207
249
  }
208
250
 
@@ -20,7 +20,6 @@ export default function (): UseInitData {
20
20
  mainMenu: computed(() => data.value?.mainMenuLinks || []),
21
21
  footerMenu: computed(() => data.value?.footerMenuLinks || []),
22
22
  footerMetaMenu: computed(() => data.value?.footerMetaMenuLinks || []),
23
- }
23
+ },
24
24
  }
25
25
  }
26
-
@@ -1,4 +1,4 @@
1
1
  export function useIsHomepage() {
2
2
  const route = useRoute()
3
3
  return computed(() => route.name === 'home')
4
- }
4
+ }
@@ -0,0 +1,30 @@
1
+ export interface LinkFieldUri {
2
+ path?: string | null
3
+ entity?: {
4
+ /** Only selected for media entities by the `linkUrl` fragment. */
5
+ id?: string | null
6
+ } | null
7
+ }
8
+
9
+ export interface ResolvedLinkUrl {
10
+ /** The URL to render. */
11
+ url?: string
12
+ /** True when the link points to a media file (e.g. a document picked via linkit). */
13
+ isFile: boolean
14
+ }
15
+
16
+ /**
17
+ * Resolves the target URL of a link field queried with the `linkUrl` fragment.
18
+ *
19
+ * Links referencing a media entity (ICMS-595) resolve to its canonical
20
+ * /media/{id} path, which the backend serves as the file currently attached
21
+ * to that media. Unlike the file URL, it stays valid when the file is
22
+ * replaced. Such links must bypass the Vue router (NuxtLink `external`) since
23
+ * they are served by the backend.
24
+ */
25
+ export function resolveLinkUrl(uri?: LinkFieldUri | null): ResolvedLinkUrl {
26
+ return {
27
+ url: uri?.path || undefined,
28
+ isFile: !!uri?.entity?.id,
29
+ }
30
+ }
@@ -22,7 +22,7 @@ export function useMediaLightbox(media: (MediaFragment | null)[] | undefined) {
22
22
  get: () => isOpen.value,
23
23
  set: (value: boolean) => {
24
24
  isOpen.value = value
25
- }
25
+ },
26
26
  })
27
27
 
28
28
  // For backward compatibility - fsLightbox used 1-based indexing
@@ -30,65 +30,77 @@ export function useMediaLightbox(media: (MediaFragment | null)[] | undefined) {
30
30
  get: () => currentIndex.value + 1,
31
31
  set: (value: number) => {
32
32
  currentIndex.value = value - 1
33
- }
33
+ },
34
34
  })
35
35
 
36
36
  // Copy exact logic from the working composable
37
- const computedMedia = computed(() =>
38
- media?.map(item => {
39
- if (!item) return ''
40
- switch (item.type) {
41
- case 'MediaImage':
42
- return (item as any).sourceUrl?.path || ''
43
- case 'MediaVideo':
44
- return (item as any).url?.path || ''
45
- case 'MediaRemoteVideo':
46
- return (item as any).link || ''
47
- default:
48
- return ''
49
- }
50
- }).filter(Boolean) || []
37
+ const computedMedia = computed(
38
+ () =>
39
+ media
40
+ ?.map((item) => {
41
+ if (!item) return ''
42
+ switch (item.type) {
43
+ case 'MediaImage':
44
+ return (item as any).sourceUrl?.path || ''
45
+ case 'MediaVideo':
46
+ return (item as any).url?.path || ''
47
+ case 'MediaRemoteVideo':
48
+ return (item as any).link || ''
49
+ default:
50
+ return ''
51
+ }
52
+ })
53
+ .filter(Boolean) || [],
51
54
  )
52
55
 
53
56
  // Expose filtered media objects for Media components
54
57
  const mediaObjects = computed(() => media?.filter(Boolean) || [])
55
58
 
56
59
  // Copy exact logic from the working composable
57
- const computedCustomAttributes = computed(() =>
58
- media?.map(item => {
59
- if (!item) return {}
60
-
61
- switch (item.type) {
62
- case 'MediaImage':
63
- return {
64
- alt: (item as any).alt?.alt || '',
65
- title: item.title || ''
66
- }
60
+ const computedCustomAttributes = computed(
61
+ () =>
62
+ media?.map((item) => {
63
+ if (!item) return {}
67
64
 
68
- case 'MediaVideo':
69
- return {
70
- alt: ((item as any).thumbnailOverride || (item as any).thumbnail)?.alt || '',
71
- title: item.title || '',
72
- poster: ((item as any).thumbnailOverride || (item as any).thumbnail)?.entity?.uri?.first?.url || '',
73
- controls: true
74
- }
65
+ switch (item.type) {
66
+ case 'MediaImage':
67
+ return {
68
+ alt: (item as any).alt?.alt || '',
69
+ title: item.title || '',
70
+ }
75
71
 
76
- case 'MediaRemoteVideo':
77
- return {
78
- alt: ((item as any).thumbnailOverride || (item as any).thumbnail)?.alt || '',
79
- title: item.title || '',
80
- poster: ((item as any).thumbnailOverride || (item as any).thumbnail)?.entity?.uri?.first?.url || ''
81
- }
72
+ case 'MediaVideo':
73
+ return {
74
+ alt:
75
+ ((item as any).thumbnailOverride || (item as any).thumbnail)
76
+ ?.alt || '',
77
+ title: item.title || '',
78
+ poster:
79
+ ((item as any).thumbnailOverride || (item as any).thumbnail)
80
+ ?.entity?.uri?.first?.url || '',
81
+ controls: true,
82
+ }
82
83
 
83
- default:
84
- return {}
85
- }
86
- }) || []
84
+ case 'MediaRemoteVideo':
85
+ return {
86
+ alt:
87
+ ((item as any).thumbnailOverride || (item as any).thumbnail)
88
+ ?.alt || '',
89
+ title: item.title || '',
90
+ poster:
91
+ ((item as any).thumbnailOverride || (item as any).thumbnail)
92
+ ?.entity?.uri?.first?.url || '',
93
+ }
94
+
95
+ default:
96
+ return {}
97
+ }
98
+ }) || [],
87
99
  )
88
100
 
89
101
  // Copy exact logic from the working composable
90
- const computedCaptions = computed(() =>
91
- media?.map(item => (item as any)?.caption || '') || []
102
+ const computedCaptions = computed(
103
+ () => media?.map((item) => (item as any)?.caption || '') || [],
92
104
  )
93
105
 
94
106
  return {
@@ -98,17 +110,17 @@ export function useMediaLightbox(media: (MediaFragment | null)[] | undefined) {
98
110
  openLightbox,
99
111
  closeLightbox,
100
112
  setCurrentIndex,
101
-
113
+
102
114
  // Backward compatibility with fsLightbox API
103
115
  toggleLightbox,
104
116
  currentSlide,
105
-
117
+
106
118
  // Data for lightbox component
107
119
  sources: computedMedia,
108
120
  customAttributes: computedCustomAttributes,
109
121
  captions: computedCaptions,
110
-
122
+
111
123
  // Media objects for Media components
112
- mediaObjects
124
+ mediaObjects,
113
125
  }
114
- }
126
+ }
@@ -29,9 +29,7 @@ export async function useMenu(menuName: MenuName) {
29
29
  })
30
30
 
31
31
  const menuItemActive = computed(() => {
32
- return menuLinks.value.find((l) =>
33
- isLinkTreeActive(l, currentRoute.path),
34
- )
32
+ return menuLinks.value.find((l) => isLinkTreeActive(l, currentRoute.path))
35
33
  })
36
34
 
37
35
  const menuItemActiveLevel1 = computed(() => {
@@ -33,7 +33,7 @@ export type BusMessage = {
33
33
  message?: any
34
34
  }
35
35
 
36
- const messageBus: Ref<Array<BusMessage>> = ref([]);
36
+ const messageBus: Ref<Array<BusMessage>> = ref([])
37
37
 
38
38
  /**
39
39
  * Sends a message to the message bus.
@@ -43,7 +43,7 @@ const messageBus: Ref<Array<BusMessage>> = ref([]);
43
43
  * @param type - Optional message type identifier.
44
44
  */
45
45
  function send(message: any, receiver?: string, type?: string) {
46
- messageBus.value.push({ receiver, type, message });
46
+ messageBus.value.push({ receiver, type, message })
47
47
  }
48
48
 
49
49
  /**
@@ -55,9 +55,16 @@ function send(message: any, receiver?: string, type?: string) {
55
55
  * @returns Array of matching BusMessage objects (deep cloned).
56
56
  */
57
57
  function receive(receiver?: string, type?: string): BusMessage[] {
58
- return messageBus.value.filter(
59
- msg => (receiver === undefined || msg.receiver === receiver) && (type === undefined || msg.type === type)
60
- ).map(msg => ({ ...msg, message: JSON.parse(JSON.stringify(msg.message)) }));
58
+ return messageBus.value
59
+ .filter(
60
+ (msg) =>
61
+ (receiver === undefined || msg.receiver === receiver) &&
62
+ (type === undefined || msg.type === type),
63
+ )
64
+ .map((msg) => ({
65
+ ...msg,
66
+ message: JSON.parse(JSON.stringify(msg.message)),
67
+ }))
61
68
  }
62
69
 
63
70
  /**
@@ -70,5 +77,5 @@ export function useMessageBus() {
70
77
  bus: messageBus,
71
78
  send,
72
79
  receive,
73
- };
74
- }
80
+ }
81
+ }
@@ -8,7 +8,7 @@ import { ref, watch } from 'vue'
8
8
 
9
9
  export enum MobileMenuOverlayEvent {
10
10
  Open = 'open',
11
- Close = 'close'
11
+ Close = 'close',
12
12
  }
13
13
 
14
14
  const mobileMenuOverlayBus = ref<MobileMenuOverlayEvent | null>(null)
@@ -1,14 +1,57 @@
1
1
  import { useIsMobile } from '@iqual/nuxt-icms/composables/useIsMobile'
2
2
 
3
- const mobileNavHeight: number = 57 + 4
4
- const desktopNavHeight: number = 80 + 66 + 4
3
+ // Used during SSR and until the header has been measured.
4
+ const fallbackMainHeightMobile = 57
5
+ const fallbackMainHeightDesktop = 81
6
+ const fallbackMetaHeight = 66
5
7
 
6
- export function useNavigationOffset(): { navigationOffset: ComputedRef<number> } {
8
+ // Helper function for retrieving the element's height
9
+ const measure = (selector: string) =>
10
+ document.querySelector<HTMLElement>(selector)?.offsetHeight ?? 0
11
+
12
+ export function useNavigationOffset(): {
13
+ metaHeight: ComputedRef<number>
14
+ headerHeight: ComputedRef<number>
15
+ navigationOffset: ComputedRef<number>
16
+ } {
7
17
  const { isMobile } = useIsMobile()
8
18
 
9
- const navigationOffset = computed(() => {
10
- return isMobile.value ? mobileNavHeight : desktopNavHeight
19
+ // null until measured, so a non-existent element stays 0
20
+ const mainHeight = ref<number | null>(null)
21
+ const metaHeight = ref<number | null>(null)
22
+
23
+ const update = () => {
24
+ mainHeight.value = measure('[data-navigation-main]')
25
+ metaHeight.value = measure('[data-navigation-meta]')
26
+ }
27
+
28
+ onMounted(() => {
29
+ requestAnimationFrame(update)
30
+ window.addEventListener('resize', update, { passive: true })
11
31
  })
12
32
 
13
- return { navigationOffset }
33
+ onBeforeUnmount(() => window.removeEventListener('resize', update))
34
+
35
+ // The part of the header that stays on screen. The meta row is pulled out of
36
+ // view by the header's negative top offset, so it does not count.
37
+ const headerHeight = computed(
38
+ () =>
39
+ mainHeight.value ??
40
+ (isMobile.value ? fallbackMainHeightMobile : fallbackMainHeightDesktop),
41
+ )
42
+
43
+ // The whole header, for scrolling something to just below it.
44
+ const navigationOffset = computed(
45
+ () =>
46
+ headerHeight.value +
47
+ (isMobile.value ? 0 : (metaHeight.value ?? fallbackMetaHeight)) +
48
+ 4,
49
+ )
50
+
51
+ return {
52
+ // Only what was measured, so the header does not shift on the first frame.
53
+ metaHeight: computed(() => metaHeight.value ?? 0),
54
+ headerHeight,
55
+ navigationOffset,
56
+ }
14
57
  }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Opt the current page into the in-memory route (full page) cache.
3
+ *
4
+ * The page is only stored if every GraphQL response that went into it was
5
+ * cacheable (see useGraphqlCacheability) and it rendered with status 200.
6
+ * Requests with a Drupal session and blökkli requests never reach the route
7
+ * cache. In `varnish` mode the route cache stays off: Varnish caches the
8
+ * page instead.
9
+ */
10
+ export function usePageCache() {
11
+ if (!import.meta.server) {
12
+ return
13
+ }
14
+
15
+ const config = resolvePageCacheConfig(useRuntimeConfig().pageCache)
16
+ if (config.mode === 'varnish' || !config.routeMaxSize) {
17
+ return
18
+ }
19
+
20
+ useRouteCache((helper) => {
21
+ helper.setCacheable().setMaxAge(config.routeMaxAge)
22
+ })
23
+
24
+ // The cached page is served for every URL with the same cache key, which
25
+ // ignores tracking parameters (utm_*, gclid, …). Nuxt embeds the rendered
26
+ // URL in the payload, and on hydration the client router prefers it over
27
+ // the browser URL whenever they differ — it would hand the first visitor's
28
+ // click IDs to everyone. Without a query the client router takes the query
29
+ // from the browser instead. The server render itself still sees the full
30
+ // URL, so redirects keep the parameters.
31
+ const nuxtApp = useNuxtApp()
32
+ nuxtApp.payload.path = nuxtApp.payload.path?.split('?')[0]
33
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Whether the frontend is shown inside the Drupal backend's site shell.
3
+ *
4
+ * Editors browse the frontend in an iframe of the backend (Drupal keeps its
5
+ * navigation and top bar), so the frontend hides its own admin UI then.
6
+ */
7
+ export default function useSiteShell() {
8
+ return useState<{ framed: boolean }>('icmsSiteShell', () => ({
9
+ framed: false,
10
+ }))
11
+ }
@@ -4,11 +4,11 @@ import cn from '@iqual/nuxt-icms/composables/useCn'
4
4
  /**
5
5
  * A composable that provides theme-aware class generation for Vue components.
6
6
  * Automatically detects the calling component and merges theme classes with component props and default classes.
7
- *
7
+ *
8
8
  * @param componentName - Component name to manually override auto-detection
9
9
  * @param themeName - The name of the theme to use from app config (defaults to 'default')
10
10
  * @returns A function that generates themed classes for component elements
11
- *
11
+ *
12
12
  * @example
13
13
  * ```vue
14
14
  * <template>
@@ -16,21 +16,21 @@ import cn from '@iqual/nuxt-icms/composables/useCn'
16
16
  * <span :class="theme('label')">Click me</span>
17
17
  * </button>
18
18
  * </template>
19
- *
19
+ *
20
20
  * <script setup>
21
21
  * // Uses default theme
22
22
  * const customTheme = useTheme('CustomComponent')
23
- *
23
+ *
24
24
  * // Use a specific theme
25
25
  * const darkTheme = useTheme('CustomComponent', 'dark')
26
- *
26
+ *
27
27
  * // The returned function merges:
28
28
  * // 1. Default classes passed as second parameter
29
29
  * // 2. Theme classes from app.config.ts
30
30
  * // 3. Component prop classes (e.g., buttonClass, labelClass props)
31
31
  * </script>
32
32
  * ```
33
- *
33
+ *
34
34
  * @remarks
35
35
  * The composable expects theme configuration in app.config.ts structured as:
36
36
  * ```typescript
@@ -50,26 +50,30 @@ export function useTheme(componentName: string, themeName: string = 'default') {
50
50
  const { props } = instance || {}
51
51
  const appConfig = useAppConfig()
52
52
 
53
- const componentTheme = (appConfig?.theme as any)?.[themeName]?.[componentName] ?? {}
53
+ const componentTheme =
54
+ (appConfig?.theme as any)?.[themeName]?.[componentName] ?? {}
54
55
 
55
56
  /**
56
57
  * Generates themed classes by merging default classes, theme classes, and component prop classes.
57
- *
58
+ *
58
59
  * @param elementName - The name of the element to style (should match theme config keys and props)
59
60
  * @param defaultClass - Optional default CSS classes to include
60
61
  * @returns Merged CSS class string using the cn() utility
61
- *
62
+ *
62
63
  * @example
63
64
  * ```typescript
64
65
  * // Returns merged classes from theme config, props, and defaults
65
66
  * const buttonClasses = theme('button', 'rounded-lg')
66
- *
67
- * // Just use theme and prop classes without defaults
67
+ *
68
+ * // Just use theme and prop classes without defaults
68
69
  * const labelClasses = theme('label')
69
70
  * ```
70
71
  */
71
72
  return function (elementName: string, defaultClass?: string) {
72
- return cn(defaultClass, componentTheme[elementName] as string, props?.[elementName] as string)
73
+ return cn(
74
+ defaultClass,
75
+ componentTheme[elementName] as string,
76
+ props?.[elementName] as string,
77
+ )
73
78
  }
74
79
  }
75
-
@@ -4,3 +4,5 @@
4
4
  export const cdnCacheControlHeader = ''
5
5
  export const cdnCacheTagHeader = ''
6
6
  export const cdnEnabled = true
7
+ // Tag purges run immediately in tests instead of being batched.
8
+ export const cacheTagInvalidationDelay = 0