@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
@@ -10,5 +10,4 @@ const props = defineProps<ParagraphIcmsTestimonialElementFragment>()
10
10
  const blokkliData = defineBlokkli({
11
11
  bundle: 'icms_testimonial_element',
12
12
  })
13
-
14
13
  </script>
@@ -17,5 +17,4 @@ const blokkliData = defineBlokkli({
17
17
  'colorScheme',
18
18
  ],
19
19
  })
20
-
21
20
  </script>
@@ -4,6 +4,7 @@
4
4
  :type="type"
5
5
  :label="label"
6
6
  :title="title"
7
+ :external="external"
7
8
  :target="targetBlank ? '_blank' : '_self'"
8
9
  :ui="{
9
10
  base: cn(
@@ -34,6 +35,8 @@ const props = defineProps<{
34
35
  title?: string
35
36
  link?: string
36
37
  params?: string
38
+ /** Force a native browser navigation (bypasses the Vue router), e.g. for file downloads. */
39
+ external?: boolean
37
40
  targetBlank?: boolean
38
41
  buttonStyle?: ButtonStyle
39
42
  colorScheme?: ColorScheme
@@ -1,5 +1,7 @@
1
1
  <template>
2
- <div :class="themedClass('class', 'container mx-auto grid grid-cols-12 gap-6')">
2
+ <div
3
+ :class="themedClass('class', 'container mx-auto grid grid-cols-12 gap-6')"
4
+ >
3
5
  <slot />
4
6
  </div>
5
7
  </template>
@@ -9,15 +9,16 @@
9
9
  "
10
10
  >
11
11
  <DesignSystemAtomsButton
12
- v-for="link in links"
12
+ v-for="(link, index) in links"
13
+ :key="`${link?.props.link?.uri.path}-${index}`"
13
14
  :label="link?.props.label"
14
15
  :link="link?.props.link?.uri.path"
15
16
  :params="link?.props.params"
16
17
  :target-blank="link?.props.targetBlank"
17
18
  :title="link?.props.title"
18
- :colorScheme="colorScheme"
19
- :buttonClass="themedClass('buttonClass')"
20
- :labelClass="themedClass('labelClass')"
19
+ :color-scheme="colorScheme"
20
+ :button-class="themedClass('buttonClass')"
21
+ :label-class="themedClass('labelClass')"
21
22
  />
22
23
  </div>
23
24
  </template>
@@ -4,19 +4,19 @@
4
4
  <Media
5
5
  v-if="icon"
6
6
  :media="icon"
7
- :mediaClass="themedClass('mediaClass', 'size-10')"
8
- :figcaptionClass="themedClass('figcaptionClass')"
9
- :elementClass="themedClass('elementClass')"
7
+ :media-class="themedClass('mediaClass', 'size-10')"
8
+ :figcaption-class="themedClass('figcaptionClass')"
9
+ :element-class="themedClass('elementClass')"
10
10
  />
11
11
  </div>
12
12
  <DesignSystemOrganismsContentBlock
13
13
  :title="title"
14
- :titleWeight="titleWeight"
15
- :titleClass="
14
+ :title-weight="titleWeight"
15
+ :title-class="
16
16
  themedClass('titleClass', 'text-xl lg:text-xl font-semibold mb-2')
17
17
  "
18
18
  :text="text"
19
- :textClass="themedClass('textClass')"
19
+ :text-class="themedClass('textClass')"
20
20
  />
21
21
  </div>
22
22
  </template>
@@ -21,13 +21,13 @@
21
21
  <div
22
22
  v-if="isOpen"
23
23
  ref="lightboxContainer"
24
- @click.self="$emit('close')"
25
24
  :class="
26
25
  themedClass(
27
26
  'lightboxContainerClass',
28
27
  'fixed inset-0 z-103 flex items-center justify-center bg-black/90 backdrop-blur-xs',
29
28
  )
30
29
  "
30
+ @click.self="$emit('close')"
31
31
  >
32
32
  <!-- Close button -->
33
33
  <button
@@ -35,13 +35,13 @@
35
35
  :aria-label="
36
36
  $texts('lightbox.close_button_aria_label', 'Close lightbox')
37
37
  "
38
- @click="$emit('close')"
39
38
  :class="
40
39
  themedClass(
41
40
  'lightboxCloseButtonClass',
42
41
  'absolute top-4 right-4 z-10 cursor-pointer rounded-full bg-black/50 p-2 text-white transition-colors hover:bg-black/70 focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-black focus:outline-hidden',
43
42
  )
44
43
  "
44
+ @click="$emit('close')"
45
45
  >
46
46
  <X :size="24" :class="themedClass('lightboxCloseButtonIconClass')" />
47
47
  </button>
@@ -67,7 +67,7 @@
67
67
  'h-8 w-8 animate-spin rounded-full border-2 border-white border-t-transparent',
68
68
  )
69
69
  "
70
- ></div>
70
+ />
71
71
  </div>
72
72
 
73
73
  <!-- Carousel-based media container -->
@@ -86,9 +86,8 @@
86
86
  :start-index="currentIndex"
87
87
  :arrows="mediaObjects.length > 1"
88
88
  :watch-drag="mediaObjects.length > 1"
89
- nextIcon="i-lucide-chevron-right"
90
- prevIcon="i-lucide-chevron-left"
91
- @select="onCarouselSelect"
89
+ next-icon="i-lucide-chevron-right"
90
+ prev-icon="i-lucide-chevron-left"
92
91
  :ui="{
93
92
  root: themedClass('carouselClass', 'flex h-full w-full flex-col'),
94
93
  item: themedClass('carouselClass', 'basis-full h-full'),
@@ -102,6 +101,7 @@
102
101
  'absolute left-4 top-1/2 z-10 -translate-y-1/2 rounded-full bg-black disabled:bg-black aria-disabled:bg-black active:bg-black ring-black p-2 text-white transition-colors hover:bg-black focus:outline-hidden focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-black disabled:cursor-not-allowed disabled:opacity-50 cursor-pointer',
103
102
  ),
104
103
  }"
104
+ @select="onCarouselSelect"
105
105
  >
106
106
  <template #default="{ item, index }">
107
107
  <div
@@ -121,10 +121,10 @@
121
121
  "
122
122
  :video-muted="true"
123
123
  :video-controls="true"
124
+ :media-class="getMediaWrapperClass(item)"
125
+ :element-class="getMediaElementClass(item)"
124
126
  @load="handleMediaLoad"
125
127
  @error="handleMediaError"
126
- :mediaClass="getMediaWrapperClass(item)"
127
- :elementClass="getMediaElementClass(item)"
128
128
  />
129
129
 
130
130
  <!-- Fallback/error state -->
@@ -3,12 +3,11 @@
3
3
  <UCarousel
4
4
  v-if="media"
5
5
  ref="carouselRef"
6
- v-model="carouselIndex"
7
6
  :items="media"
8
7
  :arrows="media.length > 1"
9
8
  :watch-drag="media.length > 1"
10
- nextIcon="i-lucide-chevron-right"
11
- prevIcon="i-lucide-chevron-left"
9
+ next-icon="i-lucide-chevron-right"
10
+ prev-icon="i-lucide-chevron-left"
12
11
  :class="themedClass('carouselClass')"
13
12
  :ui="{
14
13
  item: 'basis-full',
@@ -42,15 +41,15 @@
42
41
  <Media
43
42
  v-if="item"
44
43
  :media="item"
45
- :imageSizes="imageSizes"
46
- :imageAspectRatios="imageAspectRatios"
47
- :imageFormat="imageFormat"
48
- :showCaption="showCaption"
49
- :mediaClass="themedClass('mediaClass')"
50
- :figcaptionClass="
44
+ :image-sizes="imageSizes"
45
+ :image-aspect-ratios="imageAspectRatios"
46
+ :image-format="imageFormat"
47
+ :show-caption="showCaption"
48
+ :media-class="themedClass('mediaClass')"
49
+ :figcaption-class="
51
50
  themedClass('figcaptionClass', 'mt-2 italic text-center')
52
51
  "
53
- :elementClass="themedClass('elementClass', 'w-full')"
52
+ :element-class="themedClass('elementClass', 'w-full')"
54
53
  />
55
54
  </div>
56
55
  </template>
@@ -100,7 +99,6 @@ const props = defineProps<{
100
99
  }>()
101
100
 
102
101
  const carouselRef = ref()
103
- const carouselIndex = ref(0)
104
102
 
105
103
  const {
106
104
  isOpen,
@@ -3,16 +3,17 @@
3
3
  <NuxtLink
4
4
  :to="link"
5
5
  :title="title"
6
+ :external="external"
6
7
  :target="targetBlank ? '_blank' : '_self'"
7
8
  :class="themedClass('linkClass')"
8
9
  >
9
10
  <Media
10
11
  v-if="logo"
11
12
  :media="logo"
12
- imageSizes="xs:100vw sm:100vw md:50vw lg:580px"
13
- :mediaClass="themedClass('mediaClass', 'h-full')"
14
- :figcaptionClass="themedClass('figcaptionClass')"
15
- :elementClass="themedClass('elementClass')"
13
+ image-sizes="xs:100vw sm:100vw md:50vw lg:580px"
14
+ :media-class="themedClass('mediaClass', 'h-full')"
15
+ :figcaption-class="themedClass('figcaptionClass')"
16
+ :element-class="themedClass('elementClass')"
16
17
  />
17
18
  </NuxtLink>
18
19
  </div>
@@ -29,6 +30,8 @@ defineProps<{
29
30
  logo?: MediaFragment
30
31
  link?: string
31
32
  title?: string
33
+ /** Force a native browser navigation (bypasses the Vue router), e.g. for file downloads. */
34
+ external?: boolean
32
35
  targetBlank?: boolean
33
36
  linkClass?: string
34
37
  mediaClass?: string
@@ -35,8 +35,8 @@
35
35
  <span v-if="part.type === 'text'">{{ part.content }}</span>
36
36
  <span v-else-if="part.type === 'html'" v-html="part.content" />
37
37
  <component
38
- v-else-if="part.type === 'icon'"
39
38
  :is="part.component"
39
+ v-else-if="part.type === 'icon'"
40
40
  :size="part.size || 24"
41
41
  :stroke-width="part.strokeWidth || 2"
42
42
  class="inline-block"
@@ -73,10 +73,29 @@
73
73
  </template>
74
74
 
75
75
  <script lang="ts" setup>
76
+ import type { Component } from 'vue'
76
77
  import type { ColorScheme } from '@iqual/nuxt-icms/types/global'
77
78
  import { tv } from 'tailwind-variants'
78
79
  import * as LucideIcons from 'lucide-vue-next'
79
80
 
81
+ /**
82
+ * A cell is rendered either as a single value or, when it mixes text and
83
+ * inline icons, as a list of parts.
84
+ */
85
+ type TableCellPart =
86
+ | { type: 'text'; content: unknown }
87
+ | { type: 'html'; content: string }
88
+ | { type: 'icon'; component: Component; size: number; strokeWidth: number }
89
+
90
+ type TableCell = TableCellPart | TableCellPart[]
91
+
92
+ function hasContent(part: TableCellPart): boolean {
93
+ if (part.type === 'icon') {
94
+ return true
95
+ }
96
+ return !!part.content && String(part.content).trim() !== ''
97
+ }
98
+
80
99
  const themedClass = useTheme('DesignSystemMoleculesTable')
81
100
 
82
101
  const props = defineProps<{
@@ -193,11 +212,15 @@ const customSvgIcons: Record<string, string> = {
193
212
  cross: crossIcon,
194
213
  }
195
214
 
215
+ type ResolvedIcon =
216
+ { type: 'custom'; svg: string } | { type: 'lucide'; component: Component }
217
+
196
218
  // Helper to resolve icon from lucide or custom registry
197
- const resolveIcon = (iconName: string) => {
219
+ const resolveIcon = (iconName: string): ResolvedIcon | null => {
198
220
  // Check if it's a custom SVG icon first
199
- if (customSvgIcons[iconName]) {
200
- return { type: 'custom', svg: customSvgIcons[iconName] }
221
+ const customSvg = customSvgIcons[iconName]
222
+ if (customSvg) {
223
+ return { type: 'custom', svg: customSvg }
201
224
  }
202
225
 
203
226
  // Convert kebab-case to PascalCase for Lucide icon lookup
@@ -294,7 +317,7 @@ const tableRows = computed(() => {
294
317
  .sort((a, b) => a.weight - b.weight)
295
318
  .map(({ data }) => {
296
319
  const { weight, ...rowWithoutWeight } = data
297
- return Object.values(rowWithoutWeight).map((item) => {
320
+ return Object.values(rowWithoutWeight).map((item): TableCell => {
298
321
  if (typeof item !== 'string') {
299
322
  return { type: 'text', content: item }
300
323
  }
@@ -309,11 +332,14 @@ const tableRows = computed(() => {
309
332
  }
310
333
 
311
334
  // Build array of parts (text and icons mixed)
312
- const parts: any[] = []
335
+ const parts: TableCellPart[] = []
313
336
  let lastIndex = 0
314
337
 
315
338
  for (const match of matches) {
316
339
  const iconName = match[1]
340
+ if (!iconName) {
341
+ continue
342
+ }
317
343
  const matchStart = match.index! + (match[0].startsWith(' ') ? 1 : 0)
318
344
  const matchEnd = match.index! + match[0].length
319
345
 
@@ -356,19 +382,9 @@ const tableRows = computed(() => {
356
382
  })
357
383
  })
358
384
  .filter((row) =>
359
- row.some((cell) => {
360
- if (Array.isArray(cell)) {
361
- return cell.some(
362
- (part) =>
363
- part.type === 'icon' ||
364
- (part.content && String(part.content).trim() !== ''),
365
- )
366
- }
367
- return (
368
- cell.type === 'icon' ||
369
- (cell.content && String(cell.content).trim() !== '')
370
- )
371
- }),
385
+ row.some((cell) =>
386
+ Array.isArray(cell) ? cell.some(hasContent) : hasContent(cell),
387
+ ),
372
388
  )
373
389
  })
374
390
  </script>
@@ -1,28 +1,59 @@
1
1
  <template>
2
- <div v-if="pretitle || title || subtitle" :class="themedClass('containerClass')">
2
+ <div
3
+ v-if="pretitle || title || subtitle"
4
+ :class="themedClass('containerClass')"
5
+ >
3
6
  <p
4
7
  v-if="pretitle"
5
8
  :class="
6
9
  themedClass('pretitleClass', 'mb-2 text-xl font-semibold lg:text-2xl')
7
10
  "
8
- >
9
- {{ pretitle }}
10
- </p>
11
- <h1 v-if="title && weight === 'h1'" :class="themedClass('titleClass', titleClassList)" v-html="title.replace(/\n/g, '<br>')"></h1>
12
- <h2 v-if="title && weight === 'h2'" :class="themedClass('titleClass', titleClassList)" v-html="title.replace(/\n/g, '<br>')"></h2>
13
- <h3 v-if="title && weight === 'h3'" :class="themedClass('titleClass', titleClassList)" v-html="title.replace(/\n/g, '<br>')"></h3>
14
- <h4 v-if="title && weight === 'h4'" :class="themedClass('titleClass', titleClassList)" v-html="title.replace(/\n/g, '<br>')"></h4>
15
- <h5 v-if="title && weight === 'h5'" :class="themedClass('titleClass', titleClassList)" v-html="title.replace(/\n/g, '<br>')"></h5>
16
- <h6 v-if="title && weight === 'h6'" :class="themedClass('titleClass', titleClassList)" v-html="title.replace(/\n/g, '<br>')"></h6>
17
- <p v-if="title && weight === 'p'" :class="themedClass('titleClass', titleClassList)" v-html="title.replace(/\n/g, '<br>')"></p>
18
- <p v-if="subtitle" :class="themedClass('subtitleClass', 'mb-2 font-semibold')">
19
- {{ subtitle }}
20
- </p>
11
+ v-html="pretitle.replace(/\n/g, '<br>')"
12
+ />
13
+ <h1
14
+ v-if="title && weight === 'h1'"
15
+ :class="themedClass('titleClass', titleClassList)"
16
+ v-html="title.replace(/\n/g, '<br>')"
17
+ />
18
+ <h2
19
+ v-if="title && weight === 'h2'"
20
+ :class="themedClass('titleClass', titleClassList)"
21
+ v-html="title.replace(/\n/g, '<br>')"
22
+ />
23
+ <h3
24
+ v-if="title && weight === 'h3'"
25
+ :class="themedClass('titleClass', titleClassList)"
26
+ v-html="title.replace(/\n/g, '<br>')"
27
+ />
28
+ <h4
29
+ v-if="title && weight === 'h4'"
30
+ :class="themedClass('titleClass', titleClassList)"
31
+ v-html="title.replace(/\n/g, '<br>')"
32
+ />
33
+ <h5
34
+ v-if="title && weight === 'h5'"
35
+ :class="themedClass('titleClass', titleClassList)"
36
+ v-html="title.replace(/\n/g, '<br>')"
37
+ />
38
+ <h6
39
+ v-if="title && weight === 'h6'"
40
+ :class="themedClass('titleClass', titleClassList)"
41
+ v-html="title.replace(/\n/g, '<br>')"
42
+ />
43
+ <p
44
+ v-if="title && weight === 'p'"
45
+ :class="themedClass('titleClass', titleClassList)"
46
+ v-html="title.replace(/\n/g, '<br>')"
47
+ />
48
+ <p
49
+ v-if="subtitle"
50
+ :class="themedClass('subtitleClass', 'mb-2 font-semibold')"
51
+ v-html="subtitle.replace(/\n/g, '<br>')"
52
+ />
21
53
  </div>
22
54
  </template>
23
55
 
24
56
  <script lang="ts" setup>
25
-
26
57
  const themedClass = useTheme('DesignSystemMoleculesTitleComposition')
27
58
 
28
59
  const props = withDefaults(
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <div class="relative">
3
- <div v-if="isEditing" class="absolute inset-0 z-10 h-full w-full"></div>
3
+ <div v-if="isEditing" class="absolute inset-0 z-10 h-full w-full" />
4
4
  <iframe
5
5
  class="h-full w-full"
6
6
  :title="title"
@@ -8,7 +8,7 @@
8
8
  :style="{ height: height ? `${height}px` : '600px' }"
9
9
  frameborder="0"
10
10
  loading="lazy"
11
- ></iframe>
11
+ />
12
12
  </div>
13
13
  </template>
14
14
 
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <DesignSystemAtomsContainer
3
- :colorScheme="colorScheme"
3
+ :color-scheme="colorScheme"
4
4
  :spacing="spacing"
5
5
  :class="themedClass('containerClass')"
6
6
  >
@@ -15,13 +15,13 @@
15
15
  >
16
16
  <DesignSystemOrganismsContentBlock
17
17
  :pretitle="pretitle"
18
- :pretitleClass="cn(themedClass('pretitleClass'), alignmentClassList)"
18
+ :pretitle-class="cn(themedClass('pretitleClass'), alignmentClassList)"
19
19
  :title="title"
20
- :titleWeight="titleWeight"
21
- :titleStyle="titleStyle"
22
- :titleClass="cn(themedClass('titleClass'), alignmentClassList)"
20
+ :title-weight="titleWeight"
21
+ :title-style="titleStyle"
22
+ :title-class="cn(themedClass('titleClass'), alignmentClassList)"
23
23
  :text="text"
24
- :textClass="
24
+ :text-class="
25
25
  cn(
26
26
  themedClass('textClass', 'text-lg xl:text-2xl [&>p]:mb-0!'),
27
27
  alignmentClassList,
@@ -58,6 +58,7 @@
58
58
 
59
59
  <script setup lang="ts">
60
60
  import type { MediaFragment } from '#graphql-operations'
61
+ import type { FieldListItem } from '#blokkli/types'
61
62
  import type {
62
63
  ColorScheme,
63
64
  AlignHorizontalFull,
@@ -75,7 +76,7 @@ defineProps<{
75
76
  subtitle?: string
76
77
  text?: string
77
78
  image?: MediaFragment
78
- buttons?: Array<object>
79
+ buttons?: Array<FieldListItem | null>
79
80
  containerClass?: string
80
81
  rowClass?: string
81
82
  firstColumnClass?: string
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <DesignSystemAtomsContainer
3
- :colorScheme="colorScheme"
3
+ :color-scheme="colorScheme"
4
4
  :spacing="spacing"
5
5
  :class="themedClass('containerClass', 'relative z-1')"
6
6
  >
@@ -16,11 +16,11 @@
16
16
  >
17
17
  <Media
18
18
  :media="image"
19
- imageSizes="xs:175px xl:350px"
20
- imageAspectRatios="1/1"
21
- :mediaClass="themedClass('mediaClass', '-mt-12 xl:w-full')"
22
- :figcaptionClass="themedClass('figcaptionClass')"
23
- :elementClass="themedClass('elementClass')"
19
+ image-sizes="xs:175px xl:350px"
20
+ image-aspect-ratios="1/1"
21
+ :media-class="themedClass('mediaClass', '-mt-12 xl:w-full')"
22
+ :figcaption-class="themedClass('figcaptionClass')"
23
+ :element-class="themedClass('elementClass')"
24
24
  />
25
25
  </div>
26
26
  <div
@@ -74,6 +74,7 @@
74
74
 
75
75
  <script setup lang="ts">
76
76
  import type { MediaFragment } from '#graphql-operations'
77
+ import type { FieldListItem } from '#blokkli/types'
77
78
  import type {
78
79
  ColorScheme,
79
80
  AlignHorizontalFull,
@@ -84,7 +85,7 @@ const themedClass = useTheme('DesignSystemOrganismsCallToActionFeature')
84
85
  defineProps<{
85
86
  text?: string
86
87
  image?: MediaFragment
87
- buttons?: Array<object>
88
+ buttons?: Array<FieldListItem | null>
88
89
  containerClass?: string
89
90
  rowClass?: string
90
91
  firstColumnClass?: string
@@ -1,6 +1,6 @@
1
1
  <template>
2
2
  <DesignSystemAtomsContainer
3
- :colorScheme="colorScheme"
3
+ :color-scheme="colorScheme"
4
4
  :spacing="spacing"
5
5
  :class="themedClass('containerClass', 'call-to-action relative')"
6
6
  >
@@ -21,15 +21,15 @@
21
21
  "
22
22
  >
23
23
  <DesignSystemOrganismsContentBlock
24
- containerClass="w-full"
24
+ container-class="w-full"
25
25
  :pretitle="pretitle"
26
- :pretitleClass="cn(themedClass('pretitleClass'), alignmentClassList)"
26
+ :pretitle-class="cn(themedClass('pretitleClass'), alignmentClassList)"
27
27
  :title="title"
28
- :titleWeight="titleWeight"
29
- :titleStyle="titleStyle"
30
- :titleClass="cn(themedClass('titleClass'), alignmentClassList)"
28
+ :title-weight="titleWeight"
29
+ :title-style="titleStyle"
30
+ :title-class="cn(themedClass('titleClass'), alignmentClassList)"
31
31
  :text="text"
32
- :textClass="
32
+ :text-class="
33
33
  cn(
34
34
  themedClass('textClass', 'text-lg xl:text-2xl'),
35
35
  alignmentClassList,
@@ -57,6 +57,7 @@
57
57
 
58
58
  <script setup lang="ts">
59
59
  import type { MediaFragment } from '#graphql-operations'
60
+ import type { FieldListItem } from '#blokkli/types'
60
61
  import type {
61
62
  ColorScheme,
62
63
  AlignHorizontalFull,
@@ -73,7 +74,7 @@ defineProps<{
73
74
  subtitle?: string
74
75
  text?: string
75
76
  image?: MediaFragment
76
- buttons?: Array<object>
77
+ buttons?: Array<FieldListItem | null>
77
78
  containerClass?: string
78
79
  rowClass?: string
79
80
  firstColumnClass?: string
@@ -2,11 +2,11 @@
2
2
  <div :class="themedClass('containerClass', 'w-full')">
3
3
  <DesignSystemMoleculesTitleComposition
4
4
  :pretitle="pretitle"
5
- :pretitleClass="themedClass('pretitleClass')"
5
+ :pretitle-class="themedClass('pretitleClass')"
6
6
  :title="title"
7
- :titleClass="themedClass('titleClass')"
7
+ :title-class="themedClass('titleClass')"
8
8
  :subtitle="subtitle"
9
- :subtitleClass="themedClass('subtitleClass')"
9
+ :subtitle-class="themedClass('subtitleClass')"
10
10
  :weight="titleWeight"
11
11
  :style="titleStyle"
12
12
  />
@@ -14,12 +14,12 @@
14
14
  <DesignSystemMoleculesMediaSlider
15
15
  v-if="media && media.length"
16
16
  :media="media"
17
- :imageFormat="imageFormat"
18
- :imageSizes="imageSizes"
19
- :colorScheme="colorScheme"
20
- :mediaClass="themedClass('mediaClass')"
21
- :containerClass="themedClass('mediaContainerClass')"
22
- :elementClass="themedClass('mediaElementClass')"
17
+ :image-format="imageFormat"
18
+ :image-sizes="imageSizes"
19
+ :color-scheme="colorScheme"
20
+ :media-class="themedClass('mediaClass')"
21
+ :container-class="themedClass('mediaContainerClass')"
22
+ :element-class="themedClass('mediaElementClass')"
23
23
  />
24
24
 
25
25
  <div
@@ -33,16 +33,16 @@
33
33
  textColumnClass,
34
34
  )
35
35
  "
36
- v-html="text"
36
+ v-html="processedText"
37
37
  />
38
38
 
39
39
  <DesignSystemMoleculesButtonGroup
40
40
  :links="links"
41
- :colorScheme="colorScheme"
42
- :containerClass="themedClass('buttonGroupContainerClass')"
43
- :buttonAlignment="buttonAlignment"
44
- :buttonClass="themedClass('buttonClass')"
45
- :labelClass="themedClass('labelClass')"
41
+ :color-scheme="colorScheme"
42
+ :container-class="themedClass('buttonGroupContainerClass')"
43
+ :button-alignment="buttonAlignment"
44
+ :button-class="themedClass('buttonClass')"
45
+ :label-class="themedClass('labelClass')"
46
46
  />
47
47
  </div>
48
48
  </template>
@@ -83,7 +83,7 @@ const props = defineProps<{
83
83
  textColumns?: string
84
84
  titleWeight?: string
85
85
  titleStyle?: string
86
- media?: MediaFragment[]
86
+ media?: (MediaFragment | null)[]
87
87
  imageAspectRatios?: string
88
88
  imageSizes?: string
89
89
  imageLoading?: 'lazy' | 'eager' | undefined
@@ -104,6 +104,16 @@ const props = defineProps<{
104
104
  labelClass?: string
105
105
  }>()
106
106
 
107
+ // Rich text from CKEditor already contains markup, but plain multi-line
108
+ // content (e.g. entered before an editor was attached to the format) keeps
109
+ // raw newlines which HTML collapses - convert those to <br>.
110
+ const processedText = computed(() => {
111
+ if (!props.text || props.text.includes('<')) {
112
+ return props.text
113
+ }
114
+ return props.text.replace(/\n/g, '<br>')
115
+ })
116
+
107
117
  const textColumnClass = computed(() => {
108
118
  if (props?.textColumns === 'two') {
109
119
  return 'md:column-count-2'