@iqual/nuxt-icms 1.6.1 → 1.6.2-rc.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 (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 +23 -22
  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
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <div
3
- :class="themedClass('containerClass', 'facets-ui space-y-5')"
4
3
  v-if="groupEntries.length"
4
+ :class="themedClass('containerClass', 'facets-ui space-y-5')"
5
5
  >
6
6
  <!-- Filters row -->
7
7
  <DesignSystemAtomsGridRow
@@ -19,14 +19,14 @@
19
19
  >
20
20
  <!-- Text search input -->
21
21
  <SearchInput
22
- :inputClass="
22
+ v-model="internalSearchTerm"
23
+ :input-class="
23
24
  themedClass(
24
25
  'textSearchInputClass',
25
26
  'w-full pr-6 pl-12 py-3 border transition-all bg-white text-black border-0 selection:bg-primary-500 selection:text-white border-1 border-gray-200 placeholder-gray-300',
26
27
  )
27
28
  "
28
- :colorScheme="colorScheme"
29
- v-model="internalSearchTerm"
29
+ :color-scheme="colorScheme"
30
30
  />
31
31
  </div>
32
32
 
@@ -47,7 +47,7 @@
47
47
  :ui="{
48
48
  content: themedClass(
49
49
  'popoverUiContentClass',
50
- 'rounded-none ring-0',
50
+ 'z-[103] rounded-none ring-0',
51
51
  ),
52
52
  }"
53
53
  >
@@ -84,8 +84,8 @@
84
84
  <template v-for="opt in opts" :key="opt.value">
85
85
  <button
86
86
  type="button"
87
- @click="toggle(field.field, opt.value)"
88
87
  :class="filterOptionButton({ colorScheme })"
88
+ @click="toggle(field.field, opt.value)"
89
89
  >
90
90
  <UCheckbox
91
91
  :model-value="
@@ -130,13 +130,13 @@
130
130
  selections[field.field] && selections[field.field]?.length
131
131
  )
132
132
  "
133
- @click="clearField(field.field)"
134
- :buttonClass="
133
+ :button-class="
135
134
  themedClass('resetFilterButtonButtonClass', 'py-1 px-3')
136
135
  "
137
- :labelClass="themedClass('resetFilterButtonLabelClass')"
138
- buttonStyle="focus"
139
- :colorScheme="colorScheme"
136
+ :label-class="themedClass('resetFilterButtonLabelClass')"
137
+ button-style="focus"
138
+ :color-scheme="colorScheme"
139
+ @click="clearField(field.field)"
140
140
  />
141
141
  </div>
142
142
  </div>
@@ -157,11 +157,11 @@
157
157
  <DesignSystemAtomsButton
158
158
  v-if="selections && Object.keys(selections || {}).length"
159
159
  :label="$texts('listing.clear_filters', 'Clear all filters')"
160
+ :button-class="themedClass('resetFiltersButtonButtonClass')"
161
+ :label-class="themedClass('resetFiltersButtonLabelClass')"
162
+ button-style="focus"
163
+ :color-scheme="colorScheme"
160
164
  @click="clearAll"
161
- :buttonClass="themedClass('resetFiltersButtonButtonClass')"
162
- :labelClass="themedClass('resetFiltersButtonLabelClass')"
163
- buttonStyle="focus"
164
- :colorScheme="colorScheme"
165
165
  />
166
166
  </div>
167
167
  </DesignSystemAtomsGridRow>
@@ -319,18 +319,6 @@ const grouped = computed(() => {
319
319
  return map
320
320
  })
321
321
 
322
- // Flatten for utility lookups.
323
- const optionMap = computed(() => {
324
- // Map key: filter string -> NormalizedFacetOption
325
- const map = new Map<string, NormalizedFacetOption>()
326
- for (const [, arr] of grouped.value) {
327
- for (const opt of arr) {
328
- map.set(opt.value, opt)
329
- }
330
- }
331
- return map
332
- })
333
-
334
322
  // Stable array of [field, options[]] to simplify template & avoid calling entries() on undefined.
335
323
  // Array of [{ filter, label }, options[]] for template
336
324
  const groupEntries = computed(() =>
@@ -339,8 +327,8 @@ const groupEntries = computed(() =>
339
327
 
340
328
  // Per-field trigger labels
341
329
  function fieldTriggerLabel(fieldData: { field: string; label: string }) {
342
- let field: string = fieldData.field
343
- let label: string = fieldData.label
330
+ const field: string = fieldData.field
331
+ const label: string = fieldData.label
344
332
  const arr = selections.value[field] || []
345
333
  if (!arr.length) return props.placeholder || `${label}`
346
334
  return `${props.placeholder || label} (${arr.length})`
@@ -353,13 +341,19 @@ function toggle(field: string, value: string) {
353
341
  if (idx === -1) arr.push(value)
354
342
  else arr.splice(idx, 1)
355
343
  // Remove empty arrays to keep payload small.
356
- if (!arr.length) delete selections.value[field]
344
+ if (!arr.length) removeField(field)
357
345
  emitAll()
358
346
  }
359
347
 
348
+ function removeField(field: string) {
349
+ selections.value = Object.fromEntries(
350
+ Object.entries(selections.value).filter(([key]) => key !== field),
351
+ )
352
+ }
353
+
360
354
  function clearField(field: string) {
361
355
  if (selections.value[field]) {
362
- delete selections.value[field]
356
+ removeField(field)
363
357
  emitAll()
364
358
  }
365
359
  }
@@ -3,9 +3,8 @@
3
3
  <!-- Facets Section (only for 'all' listing type) -->
4
4
  <div v-if="listingType === 'all' && facets.length > 0">
5
5
  <SearchFacetsFacetDropdownUI
6
- :facets="facetData"
7
6
  v-model="facetSelections"
8
- @facets-change="onFacetsChange"
7
+ :facets="facetData"
9
8
  :total-count="totalCount"
10
9
  :displayed-count="
11
10
  props.groupResultsByFacet
@@ -16,29 +15,28 @@
16
15
  : displayedResults?.[0]?.results?.length
17
16
  "
18
17
  :loading="isLoading"
19
- :colorScheme="colorScheme"
18
+ :color-scheme="colorScheme"
20
19
  :class="
21
20
  themedClass('listingTypeAllSearchFacetsFacetDropdownUIClass', 'mb-6')
22
21
  "
23
22
  :enable-text-search="enableTextSearch"
24
23
  :search-term="searchTerm"
25
- :alignHorizontalFull="alignHorizontalFull"
24
+ :align-horizontal-full="alignHorizontalFull"
25
+ @facets-change="onFacetsChange"
26
26
  @update:search-term="onSearchTermChange"
27
27
  />
28
28
 
29
29
  <template v-if="debug">
30
30
  <!-- Debug info (only if enabled) -->
31
31
  <pre
32
- class="mt-4 rounded-sm bg-gray-50 p-3 text-xs whitespace-pre"
33
32
  v-if="Object.keys(facetSelections).length"
34
- >{{ JSON.stringify(facetSelections, null, 2) }}</pre
35
- >
33
+ class="mt-4 rounded-sm bg-gray-50 p-3 text-xs whitespace-pre"
34
+ >{{ JSON.stringify(facetSelections, null, 2) }}</pre>
36
35
  <pre
37
- class="mt-2 rounded-sm bg-blue-50 p-3 text-xs whitespace-pre"
38
36
  v-if="facetData.length"
37
+ class="mt-2 rounded-sm bg-blue-50 p-3 text-xs whitespace-pre"
39
38
  >
40
- Facets: {{ facetData.length }} items</pre
41
- >
39
+ Facets: {{ facetData.length }} items</pre>
42
40
  </template>
43
41
  </div>
44
42
 
@@ -47,8 +45,7 @@ Facets: {{ facetData.length }} items</pre
47
45
  <pre class="mb-4 rounded-sm bg-yellow-50 p-3 text-xs whitespace-pre">
48
46
  Active Filters ({{ listingType }}): {{
49
47
  JSON.stringify(activeFilters, null, 2)
50
- }}</pre
51
- >
48
+ }}</pre>
52
49
  </template>
53
50
 
54
51
  <!-- Results Section -->
@@ -103,21 +100,26 @@ Facets: {{ facetData.length }} items</pre
103
100
  >
104
101
  <slot
105
102
  name="group-load-more"
106
- :loadMore="() => loadMoreGroup(group)"
103
+ :load-more="() => loadMoreGroup(group)"
107
104
  :loading="isGroupLoading(group)"
108
105
  :group="group"
109
106
  >
110
107
  <DesignSystemAtomsButton
111
- :colorScheme="colorScheme"
112
- buttonStyle="normal"
108
+ :color-scheme="colorScheme"
109
+ button-style="normal"
113
110
  :label="
114
111
  isGroupLoading(group)
115
112
  ? defaultLoadingText
116
113
  : defaultLoadMoreText
117
114
  "
118
115
  :disabled="isGroupLoading(group)"
119
- :buttonClass="cn(themedClass('groupLoadMoreButtonClass'), loadMoreButtonClass)"
120
- :labelClass="themedClass('groupLoadMoreLabelClass')"
116
+ :button-class="
117
+ cn(
118
+ themedClass('groupLoadMoreButtonClass'),
119
+ loadMoreButtonClass,
120
+ )
121
+ "
122
+ :label-class="themedClass('groupLoadMoreLabelClass')"
121
123
  @click="loadMoreGroup(group)"
122
124
  />
123
125
  </slot>
@@ -150,7 +152,7 @@ Facets: {{ facetData.length }} items</pre
150
152
  )
151
153
  "
152
154
  >
153
- <span :class="loadingText({ colorScheme })">
155
+ <span :class="loadingTextClass({ colorScheme })">
154
156
  {{ defaultLoadingText }}
155
157
  </span>
156
158
  </div>
@@ -168,17 +170,17 @@ Facets: {{ facetData.length }} items</pre
168
170
  )
169
171
  "
170
172
  >
171
- <slot name="load-more" :loadMore="loadMore" :loading="loadingMore">
173
+ <slot name="load-more" :load-more="loadMore" :loading="loadingMore">
172
174
  <DesignSystemAtomsButton
173
175
  type="button"
174
- :colorScheme="colorScheme"
175
- buttonStyle="normal"
176
+ :color-scheme="colorScheme"
177
+ button-style="normal"
176
178
  :label="loadingMore ? defaultLoadingText : defaultLoadMoreText"
177
179
  :disabled="loadingMore"
178
- :buttonClass="
180
+ :button-class="
179
181
  cn(themedClass('globalLoadMoreButtonClass'), loadMoreButtonClass)
180
182
  "
181
- :labelClass="themedClass('globalLoadMoreLabelClass')"
183
+ :label-class="themedClass('globalLoadMoreLabelClass')"
182
184
  @click="loadMore"
183
185
  />
184
186
  </slot>
@@ -400,11 +402,29 @@ const buildSearchFilter = (
400
402
  }
401
403
  }
402
404
 
403
- // Initial query to get all available facets (only for 'all' listing type)
404
- const { data: initialFacetsData } = await useAsyncData(
405
- props.listingType === 'all'
406
- ? `facetedSearch-facets-${props.index}-${JSON.stringify(props.baseFilter)}-${searchTerm.value}`
407
- : 'skip-facets',
405
+ // A language switch is a client-side route change: the component instance is
406
+ // reused and its setup does not re-run. The GraphQL client already sends the
407
+ // current language per request, but the useAsyncData cache keys must include
408
+ // the language so a language change triggers a refetch instead of serving the
409
+ // previous language's cached payload.
410
+ const currentLanguage = useCurrentLanguage()
411
+
412
+ // Freeze the setup-time values in the keys: the available-facets list should
413
+ // only refetch on language change, not on text-search input (unchanged
414
+ // behavior); result updates for filter changes go through
415
+ // updateSearchResults().
416
+ const setupSearchTerm = searchTerm.value
417
+ const setupActiveFilters = JSON.stringify(activeFilters.value)
418
+
419
+ // Initial query to get all available facets (only for 'all' listing type).
420
+ // Deferred to the client (server: false, lazy: true): the facet options are
421
+ // interactive UI, not indexable content, and this query is expensive on the
422
+ // backend — running it during SSR blocks navigation.
423
+ const { data: initialFacetsData } = useAsyncData(
424
+ () =>
425
+ props.listingType === 'all'
426
+ ? `facetedSearch-facets-${props.index}-${JSON.stringify(props.baseFilter)}-${setupSearchTerm}-${currentLanguage.value}`
427
+ : 'skip-facets',
408
428
  async () => {
409
429
  if (props.listingType !== 'all') {
410
430
  return null
@@ -414,16 +434,21 @@ const { data: initialFacetsData } = await useAsyncData(
414
434
  facets: props.facets,
415
435
  filter: props.baseFilter,
416
436
  keys: searchTerm.value ?? '',
437
+ // Only the facets and the count are used from this query, but the
438
+ // backend loads up to 50 result entities unless a limit is set.
439
+ limit: 1,
417
440
  }).then((v) => {
418
441
  return v.data?.searchApiQuery ?? null
419
442
  })
420
443
  },
444
+ { server: false, lazy: true },
421
445
  )
422
446
 
423
447
  // Initial query to get search results
424
448
  const { data: initialResultsData, refresh: refreshResults } =
425
449
  await useAsyncData(
426
- `facetedSearch-results-${props.index}-${props.listingType}-${JSON.stringify(props.baseFilter)}-${JSON.stringify(activeFilters.value)}`,
450
+ () =>
451
+ `facetedSearch-results-${props.index}-${props.listingType}-${JSON.stringify(props.baseFilter)}-${setupActiveFilters}-${currentLanguage.value}`,
427
452
  async () => {
428
453
  const filter = buildSearchFilter()
429
454
 
@@ -438,7 +463,10 @@ const { data: initialResultsData, refresh: refreshResults } =
438
463
  const response = await useGraphqlQuery('layoutOverviewResultsSearch', {
439
464
  indexId: props.index,
440
465
  filter,
441
- facets: props.facets,
466
+ // Facets (and their per-facet results) are only needed to group the
467
+ // results; the facet dropdown has its own query.
468
+ facets: props.groupResultsByFacet ? props.facets : undefined,
469
+ grouped: !!props.groupResultsByFacet,
442
470
  limit: props?.groupResultsByFacet
443
471
  ? props.itemsPerPage * 10
444
472
  : props.itemsPerPage,
@@ -486,13 +514,17 @@ const { data: initialResultsData, refresh: refreshResults } =
486
514
  },
487
515
  )
488
516
 
489
- // Initialize facets from initial query (only for 'all' listing type)
490
- if (initialFacetsData.value) {
491
- const enrichedFacets = await useEnrichFacetLabels(
492
- initialFacetsData.value.facets || [],
493
- )
494
- allFacets.value = enrichedFacets
495
- }
517
+ // Initialize the facets once the deferred facets query resolves on the client
518
+ // and re-enrich the labels after a refetch (e.g. language switch).
519
+ watch(
520
+ initialFacetsData,
521
+ async (facetsData) => {
522
+ if (facetsData) {
523
+ allFacets.value = await useEnrichFacetLabels(facetsData.facets || [])
524
+ }
525
+ },
526
+ { immediate: true },
527
+ )
496
528
 
497
529
  // Function to update search results when facets change (only for 'all' listing type)
498
530
  async function updateSearchResults(selections: Record<string, string[]>) {
@@ -521,7 +553,8 @@ async function updateSearchResults(selections: Record<string, string[]>) {
521
553
  const response = await useGraphqlQuery('layoutOverviewResultsSearch', {
522
554
  indexId: props.index,
523
555
  filter,
524
- facets: props.facets,
556
+ facets: props.groupResultsByFacet ? props.facets : undefined,
557
+ grouped: !!props.groupResultsByFacet,
525
558
  limit: props?.groupResultsByFacet
526
559
  ? props.itemsPerPage * 10
527
560
  : props.itemsPerPage,
@@ -851,7 +884,7 @@ async function loadMoreGroup(group: any) {
851
884
  }
852
885
  }
853
886
 
854
- const loadingText = tv({
887
+ const loadingTextClass = tv({
855
888
  base: themedClass('loadingTextBase'),
856
889
  variants: {
857
890
  colorScheme: {
@@ -911,13 +944,4 @@ watchEffect(() => {
911
944
  }
912
945
  }
913
946
  })
914
-
915
- // Expose functions for parent components if needed
916
- defineExpose({
917
- refreshResults,
918
- loadMore,
919
- facetSelections: readonly(facetSelections),
920
- totalCount: readonly(totalCount),
921
- displayedResults: readonly(displayedResults),
922
- })
923
947
  </script>
@@ -1,14 +1,16 @@
1
1
  query layoutOverviewFacetsSearch(
2
- $indexId: String!,
3
- $facets: [String!],
4
- $filter: SearchApiQueryFilterInput,
2
+ $indexId: String!
3
+ $facets: [String!]
4
+ $filter: SearchApiQueryFilterInput
5
5
  $keys: String
6
+ $limit: Int
6
7
  ) {
7
- searchApiQuery(
8
- indexId: $indexId,
9
- facets: $facets,
10
- filter: $filter,
8
+ searchApiQuery(
9
+ indexId: $indexId
10
+ facets: $facets
11
+ filter: $filter
11
12
  keys: $keys
13
+ limit: $limit
12
14
  ) {
13
15
  count
14
16
  facets {
@@ -24,16 +26,17 @@ query layoutOverviewFacetsSearch(
24
26
  }
25
27
 
26
28
  query layoutOverviewResultsSearch(
27
- $indexId: String!,
28
- $filter: SearchApiQueryFilterInput,
29
- $limit: Int,
30
- $offset: Int,
29
+ $indexId: String!
30
+ $filter: SearchApiQueryFilterInput
31
+ $limit: Int
32
+ $offset: Int
31
33
  $keys: String
32
- $facets: [String!],
34
+ $facets: [String!]
33
35
  $sort: [SearchApiQuerySortInput!]
36
+ $grouped: Boolean = false
34
37
  ) {
35
- searchApiQuery(
36
- indexId: $indexId,
38
+ searchApiQuery(
39
+ indexId: $indexId
37
40
  filter: $filter
38
41
  facets: $facets
39
42
  limit: $limit
@@ -47,7 +50,9 @@ query layoutOverviewResultsSearch(
47
50
  ...entityTeaser
48
51
  ...paragraphTeaser
49
52
  }
50
- facets {
53
+ # Per-facet result lists are only read when results are grouped by a
54
+ # facet; resolving them loads and renders every result entity per facet.
55
+ facets @include(if: $grouped) {
51
56
  field
52
57
  label
53
58
  filter {
@@ -18,18 +18,19 @@
18
18
  <Media
19
19
  v-if="companyLogo"
20
20
  :media="companyLogo"
21
- :mediaClass="themedClass('mediaClass', 'mb-8 w-32')"
22
- :figcaptionClass="themedClass('figcaptionClass')"
23
- :elementClass="themedClass('elementClass')"
21
+ :media-class="themedClass('mediaClass', 'mb-8 w-32')"
22
+ :figcaption-class="themedClass('figcaptionClass')"
23
+ :element-class="themedClass('elementClass')"
24
24
  />
25
25
 
26
26
  <DesignSystemOrganismsContentBlock
27
+ v-if="companyName || companyDescription"
27
28
  :title="companyName"
28
- :titleWeight="'p'"
29
- :titleStyle="'heading-5'"
30
- :titleClass="themedClass('companyNameClass')"
29
+ :title-weight="'p'"
30
+ :title-style="'heading-5'"
31
+ :title-class="themedClass('companyNameClass')"
31
32
  :text="companyDescription"
32
- :textClass="themedClass('companyDescriptionClass')"
33
+ :text-class="themedClass('companyDescriptionClass')"
33
34
  />
34
35
  </div>
35
36
 
@@ -125,10 +126,14 @@
125
126
  "
126
127
  >
127
128
  <DesignSystemOrganismsContentBlock
129
+ v-if="address"
128
130
  :text="address"
129
- :textClass="themedClass('addressClass')"
131
+ :text-class="themedClass('addressClass')"
130
132
  />
131
- <div :class="themedClass('contactInfoContainerClass')">
133
+ <div
134
+ v-if="phoneNumber || emailAddress"
135
+ :class="themedClass('contactInfoContainerClass')"
136
+ >
132
137
  <NuxtLink
133
138
  v-if="phoneNumber"
134
139
  :to="`tel:${phoneNumber}`"
@@ -161,7 +166,7 @@
161
166
  </DesignSystemAtomsContainer>
162
167
 
163
168
  <DesignSystemAtomsContainer
164
- v-if="socialMediaLinks?.length || footerMetaMenuLinks?.length"
169
+ v-if="socialMediaIcons.length || footerMetaMenuLinks?.length"
165
170
  :spacing="{ top: 'none', bottom: 'none' }"
166
171
  :class="themedClass('metaContainerClass', 'py-6')"
167
172
  >
@@ -175,24 +180,19 @@
175
180
  "
176
181
  >
177
182
  <div
178
- :class="
179
- themedClass(
180
- 'socialMediaContainerClass',
181
- 'flex gap-4 empty:hidden',
182
- )
183
- "
183
+ v-if="socialMediaIcons.length"
184
+ :class="themedClass('socialMediaContainerClass', 'flex gap-4')"
184
185
  >
185
- <template v-for="(link, index) in socialMediaLinks" :key="index">
186
+ <template v-for="(icon, index) in socialMediaIcons" :key="index">
186
187
  <NuxtLink
187
- v-if="link?.uri?.path"
188
- :to="link?.uri?.path"
189
- :title="getSocialMediaIconMeta(link.uri.path).title"
188
+ :to="icon.path"
189
+ :external="icon.external"
190
+ :title="icon.title"
190
191
  target="_blank"
191
192
  :class="themedClass('socialMediaIconLinkClass', 'group')"
192
193
  >
193
194
  <SpriteSymbol
194
- v-if="getSocialMediaIconMeta(link.uri.path)?.sprite"
195
- :name="getSocialMediaIconMeta(link.uri.path).sprite"
195
+ :name="icon.sprite"
196
196
  :class="
197
197
  themedClass(
198
198
  'socialMediaIconClass',
@@ -205,7 +205,7 @@
205
205
  </div>
206
206
 
207
207
  <div
208
- v-if="footerMetaMenuLinks"
208
+ v-if="footerMetaMenuLinks?.length"
209
209
  :class="
210
210
  themedClass('footerMetaMenuContainerClass', 'flex items-center')
211
211
  "
@@ -267,11 +267,6 @@ const {
267
267
  socialMediaLinks,
268
268
  } = await useGlobalConfig()
269
269
 
270
- interface SocialMediaIconMeta {
271
- sprite: NuxtSvgSpriteSymbol
272
- title: string
273
- }
274
-
275
270
  const socialIconsMap: Record<string, NuxtSvgSpriteSymbol> = {
276
271
  facebook: 'facebook',
277
272
  instagram: 'instagram',
@@ -281,9 +276,37 @@ const socialIconsMap: Record<string, NuxtSvgSpriteSymbol> = {
281
276
  pinterest: 'pinterest',
282
277
  }
283
278
 
284
- const getSocialMediaIconMeta = (url: string): SocialMediaIconMeta => {
285
- const domain = new URL(url).hostname.split('.com')[0].split('.').slice(-1)[0]
286
- const title = domain.charAt(0).toUpperCase() + domain.slice(1)
287
- return { sprite: socialIconsMap[domain], title: title }
279
+ /**
280
+ * Derive the icon and the label of a social media link from its domain, e.g.
281
+ * "https://www.instagram.com/foo" becomes the "instagram" sprite.
282
+ */
283
+ const getSocialMediaIconMeta = (url: string) => {
284
+ const domain =
285
+ new URL(url).hostname.split('.com')[0]?.split('.').slice(-1)[0] ?? ''
286
+ return {
287
+ sprite: socialIconsMap[domain],
288
+ title: domain.charAt(0).toUpperCase() + domain.slice(1),
289
+ }
288
290
  }
291
+
292
+ const socialMediaIcons = computed(() =>
293
+ (socialMediaLinks.value || []).flatMap((link) => {
294
+ const resolved = resolveLinkUrl(link?.uri)
295
+ if (!resolved.url) {
296
+ return []
297
+ }
298
+ const meta = getSocialMediaIconMeta(resolved.url)
299
+ if (!meta.sprite) {
300
+ return []
301
+ }
302
+ return [
303
+ {
304
+ path: resolved.url,
305
+ external: resolved.isFile,
306
+ ...meta,
307
+ sprite: meta.sprite,
308
+ },
309
+ ]
310
+ }),
311
+ )
289
312
  </script>
@@ -11,17 +11,16 @@
11
11
  :link="link?.link?.uri?.path"
12
12
  :params="link?.params"
13
13
  :target-blank="link?.targetBlank"
14
- :buttonClass="themedClass('buttonClass', 'xs:w-fit')"
15
- :labelClass="themedClass('labelClass')"
16
- colorScheme="none"
17
- buttonStyle="focus"
14
+ :button-class="themedClass('buttonClass', 'xs:w-fit')"
15
+ :label-class="themedClass('labelClass')"
16
+ color-scheme="none"
17
+ button-style="focus"
18
18
  @click="close()"
19
19
  />
20
20
  </div>
21
21
  </template>
22
22
 
23
23
  <script lang="ts" setup>
24
-
25
24
  defineProps<{
26
25
  buttons: any[] | undefined
27
26
  }>()
@@ -16,13 +16,14 @@
16
16
  >
17
17
  <li
18
18
  v-for="item in menuItems"
19
+ :key="item.id"
20
+ :class="
21
+ themedClass('listItemClass', 'relative flex h-full items-stretch')
22
+ "
19
23
  @mouseenter="setOpen(item, true)"
20
24
  @mouseleave="setOpen(item, false)"
21
25
  @focusin="setOpen(item, true)"
22
26
  @focusout="setOpen(item, false)"
23
- :class="
24
- themedClass('listItemClass', 'relative flex h-full items-stretch')
25
- "
26
27
  >
27
28
  <component :is="elementType(item)" v-bind="componentAttrs(item)">
28
29
  {{ item.label }}
@@ -79,7 +80,7 @@
79
80
  >
80
81
  <DesktopMainMenuDropdown
81
82
  :item="item"
82
- :menuItemActive="menuItemActive"
83
+ :menu-item-active="menuItemActive"
83
84
  />
84
85
  </ul>
85
86
  </Transition>
@@ -2,16 +2,16 @@
2
2
  <li
3
3
  v-for="child in item.children"
4
4
  :key="child.id"
5
- @mouseenter="setOpen(child, true)"
6
- @mouseleave="setOpen(child, false)"
7
- @focusin="setOpen(child, true)"
8
- @focusout="setOpen(child, false)"
9
5
  :class="
10
6
  themedClass(
11
7
  'listItemClass',
12
8
  'relative first-of-type:pt-1 last-of-type:pb-1',
13
9
  )
14
10
  "
11
+ @mouseenter="setOpen(child, true)"
12
+ @mouseleave="setOpen(child, false)"
13
+ @focusin="setOpen(child, true)"
14
+ @focusout="setOpen(child, false)"
15
15
  >
16
16
  <component :is="elementType(child)" v-bind="componentAttrs(child)">
17
17
  {{ child.label }}
@@ -68,7 +68,7 @@
68
68
  >
69
69
  <DesktopMainMenuDropdown
70
70
  :item="child"
71
- :menuItemActive="menuItemActive"
71
+ :menu-item-active="menuItemActive"
72
72
  />
73
73
  </ul>
74
74
  </Transition>