@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
@@ -4,31 +4,37 @@
4
4
  v-if="imageSources.length === 1"
5
5
  :srcset="imageSources[0]?.srcset"
6
6
  :sizes="imageSources[0]?.sizes"
7
+ :width="imageSources[0]?.width"
8
+ :height="imageSources[0]?.height"
7
9
  :alt="media?.alt?.alt"
8
10
  :title="media?.title"
9
11
  :src="placeHolderImage"
10
12
  :loading="imageLoading"
11
13
  :class="themedClass('elementClass', 'w-full')"
12
14
  />
13
- <source
14
- v-if="imageSources.length > 1"
15
- v-for="(source, key) in imageSources"
16
- :key="key"
17
- :srcset="source.srcset"
18
- :media="source.media"
19
- />
20
- <img
21
- v-if="imageSources.length > 1"
22
- :src="placeHolderImage"
23
- :alt="media?.alt?.alt"
24
- :title="media?.title"
25
- :loading="imageLoading"
26
- :class="themedClass('elementClass', 'w-full')"
27
- />
15
+ <template v-if="imageSources.length > 1">
16
+ <source
17
+ v-for="(source, key) in imageSources"
18
+ :key="key"
19
+ :srcset="source.srcset"
20
+ :media="source.media"
21
+ :width="source.width"
22
+ :height="source.height"
23
+ />
24
+ <img
25
+ :src="placeHolderImage"
26
+ :width="fallbackSource?.width"
27
+ :height="fallbackSource?.height"
28
+ :alt="media?.alt?.alt"
29
+ :title="media?.title"
30
+ :loading="imageLoading"
31
+ :class="themedClass('elementClass', 'w-full')"
32
+ />
33
+ </template>
28
34
  <figcaption
29
35
  v-if="showCaption && media?.caption"
30
- v-text="media?.caption"
31
36
  :class="themedClass('figcaptionClass', 'mt-2 text-center italic')"
37
+ v-text="media?.caption"
32
38
  />
33
39
  </picture>
34
40
  </template>
@@ -73,7 +79,7 @@ const imageAspectRatios = computed(() => {
73
79
  return ''
74
80
  }
75
81
 
76
- return
82
+ return ''
77
83
  })
78
84
 
79
85
  const { placeHolderImage, imageSources } = useImageSources({
@@ -81,4 +87,8 @@ const { placeHolderImage, imageSources } = useImageSources({
81
87
  imageAspectRatios: imageAspectRatios,
82
88
  imageSizes: toRef(props, 'imageSizes'),
83
89
  })
90
+
91
+ // The <img> of a <picture> is used when no <source> matches: the last source
92
+ // carries no media query and covers the widest screens.
93
+ const fallbackSource = computed(() => imageSources.value.at(-1))
84
94
  </script>
@@ -16,13 +16,13 @@
16
16
 
17
17
  <button
18
18
  v-if="!isPlaying"
19
- @click.prevent="isPlaying = true"
20
19
  :class="
21
20
  themedClass(
22
21
  'playButtonClass',
23
22
  'relative flex aspect-video w-full cursor-pointer items-center justify-center',
24
23
  )
25
24
  "
25
+ @click.prevent="isPlaying = true"
26
26
  >
27
27
  <img
28
28
  :src="thumbnailUrl"
@@ -30,7 +30,7 @@
30
30
  :class="
31
31
  themedClass(
32
32
  'thumbnailClass',
33
- 'absolute left-0 top-0 aspect-video size-full object-cover',
33
+ 'absolute top-0 left-0 aspect-video size-full object-cover',
34
34
  )
35
35
  "
36
36
  />
@@ -43,8 +43,8 @@
43
43
 
44
44
  <figcaption
45
45
  v-if="showCaption && media?.caption"
46
- v-text="media?.caption"
47
46
  :class="themedClass('figcaptionClass', 'mt-2 text-center italic')"
47
+ v-text="media?.caption"
48
48
  />
49
49
  </figure>
50
50
  </VuepalRemoteVideo>
@@ -13,8 +13,8 @@
13
13
  </video>
14
14
  <figcaption
15
15
  v-if="showCaption && media?.caption"
16
- v-text="media?.caption"
17
16
  :class="themedClass('figcaptionClass', 'mt-2 text-center italic')"
17
+ v-text="media?.caption"
18
18
  />
19
19
  </figure>
20
20
  </template>
@@ -7,4 +7,4 @@ fragment nodeIcmsPageSearchResult on NodeIcmsPage {
7
7
  link: url {
8
8
  path
9
9
  }
10
- }
10
+ }
@@ -8,5 +8,5 @@
8
8
 
9
9
  <script lang="ts" setup>
10
10
  import type { NodeIcmsPageSearchResultFragment } from '#graphql-operations'
11
- const props = defineProps<NodeIcmsPageSearchResultFragment>()
12
- </script>
11
+ defineProps<NodeIcmsPageSearchResultFragment>()
12
+ </script>
@@ -6,7 +6,7 @@
6
6
  :title="title || mainTitle"
7
7
  :text="text || mainText"
8
8
  :link="link"
9
- :linkLabel="linkLabel || $texts('teaser.link_label', 'Read more')"
9
+ :link-label="linkLabel || $texts('teaser.link_label', 'Read more')"
10
10
  />
11
11
  </template>
12
12
 
@@ -1,7 +1,11 @@
1
1
  <template>
2
2
  <main>
3
- <BlokkliProvider v-slot="{ entity }" v-bind="blokkliProps" :entity="props">
4
- <BlokkliField :list="paragraphs" name="field_icms_paragraphs" />
3
+ <BlokkliProvider v-bind="blokkliProps" :entity="props">
4
+ <BlokkliField
5
+ :list="paragraphs"
6
+ name="field_icms_paragraphs"
7
+ :allowed-fragments="allowedFragments"
8
+ />
5
9
  </BlokkliProvider>
6
10
  </main>
7
11
  </template>
@@ -11,6 +15,8 @@ import type { NodeIcmsPageFragment } from '#graphql-operations'
11
15
 
12
16
  const props = defineProps<NodeIcmsPageFragment>()
13
17
 
18
+ const allowedFragments = useAllowedFragments()
19
+
14
20
  // Pass the topics of the article to the context, to be used in Layouts
15
21
  provide('topics', props.topics)
16
22
  </script>
@@ -0,0 +1,4 @@
1
+ fragment paragraphIcmsLayoutAnchor on ParagraphIcmsLayoutAnchor {
2
+ label: fieldIcmsTitle
3
+ anchor: fieldIcmsAnchor
4
+ }
@@ -0,0 +1,48 @@
1
+ <template>
2
+ <div
3
+ v-if="isEditing"
4
+ :class="
5
+ themedClass(
6
+ 'placeholderClass',
7
+ 'flex items-center gap-2 p-4 text-sm text-gray-600',
8
+ )
9
+ "
10
+ >
11
+ <SpriteSymbol
12
+ name="anchor"
13
+ :class="themedClass('iconClass', 'size-6 shrink-0')"
14
+ />
15
+ <span>{{ label }}</span>
16
+ <span v-if="id" :class="themedClass('idClass', 'text-gray-400')">
17
+ #{{ id }}
18
+ </span>
19
+ </div>
20
+ <div v-else-if="id">
21
+ <!-- Wrapped, so the attributes blokkli sets on the root element can not
22
+ override the anchor's own id. -->
23
+ <span
24
+ :id="id"
25
+ :style="{ '--anchor-offset': `${anchorOffset}px` }"
26
+ :class="themedClass('anchorClass', 'scroll-mt-(--anchor-offset)')"
27
+ />
28
+ </div>
29
+ </template>
30
+
31
+ <script lang="ts" setup>
32
+ import type { ParagraphIcmsLayoutAnchorFragment } from '#graphql-operations'
33
+ import type { DefineBlokkliContext } from '#blokkli/types'
34
+ import {
35
+ resolveAnchorId,
36
+ useAnchorOffset,
37
+ } from '@iqual/nuxt-icms/composables/useAnchors'
38
+
39
+ const props = defineProps<ParagraphIcmsLayoutAnchorFragment>()
40
+
41
+ const themedClass = useTheme('ParagraphAnchor')
42
+
43
+ const { isEditing } = useAttrs().blokkliData as DefineBlokkliContext
44
+
45
+ const { anchorOffset } = useAnchorOffset()
46
+
47
+ const id = computed(() => resolveAnchorId(props.label, props.anchor))
48
+ </script>
@@ -0,0 +1,3 @@
1
+ fragment paragraphIcmsLayoutAnchorNavigation on ParagraphIcmsLayoutAnchorNavigation {
2
+ id
3
+ }
@@ -0,0 +1,154 @@
1
+ <template>
2
+ <DesignSystemAtomsContainer
3
+ v-if="anchors.length"
4
+ :spacing="{ top: 'none', bottom: 'none' }"
5
+ :style="{ '--anchor-navigation-top': `${headerOffset}px` }"
6
+ :class="containerClass"
7
+ >
8
+ <DesignSystemAtomsGridRow>
9
+ <nav
10
+ :aria-label="
11
+ $texts('anchor_navigation.nav_aria_label', 'Anchor navigation')
12
+ "
13
+ :class="themedClass('navClass', 'col-span-12')"
14
+ >
15
+ <ul
16
+ :class="
17
+ themedClass('listClass', 'flex flex-nowrap gap-6 overflow-x-auto')
18
+ "
19
+ >
20
+ <li
21
+ v-for="anchor in anchors"
22
+ :key="anchor.id"
23
+ :class="themedClass('listItemClass', 'shrink-0')"
24
+ >
25
+ <a
26
+ :href="`#${anchor.id}`"
27
+ :aria-current="anchor.id === activeId ? 'true' : undefined"
28
+ :class="linkClass(anchor.id === activeId)"
29
+ >
30
+ {{ anchor.label }}
31
+ </a>
32
+ </li>
33
+ </ul>
34
+ </nav>
35
+ </DesignSystemAtomsGridRow>
36
+ </DesignSystemAtomsContainer>
37
+ <div
38
+ v-else-if="isEditing"
39
+ :class="
40
+ themedClass(
41
+ 'placeholderClass',
42
+ 'flex items-center gap-2 p-4 text-sm text-gray-600',
43
+ )
44
+ "
45
+ >
46
+ <SpriteSymbol
47
+ name="anchor"
48
+ :class="themedClass('iconClass', 'size-5 shrink-0')"
49
+ />
50
+ {{
51
+ $texts(
52
+ 'anchor_navigation.editor_empty',
53
+ 'Add anchor blocks to fill this navigation',
54
+ )
55
+ }}
56
+ </div>
57
+ </template>
58
+
59
+ <script lang="ts" setup>
60
+ import cn from '@iqual/nuxt-icms/composables/useCn'
61
+ import type { DefineBlokkliContext } from '#blokkli/types'
62
+ import {
63
+ resolveAnchorId,
64
+ useAnchorOffset,
65
+ type Anchor,
66
+ } from '@iqual/nuxt-icms/composables/useAnchors'
67
+
68
+ const themedClass = useTheme('ParagraphAnchorNavigation')
69
+
70
+ const { $texts } = useNuxtApp()
71
+
72
+ const { isEditing, rootBlocks } = useAttrs().blokkliData as DefineBlokkliContext
73
+
74
+ const { headerOffset, anchorOffset } = useAnchorOffset()
75
+ const activeId = ref('')
76
+
77
+ // Anchors retrieved from Blokkli's rootBlocks without querying the DOM.
78
+ // Already in the right order and available during SSR.
79
+ const anchors = computed<Anchor[]>(() =>
80
+ rootBlocks.value.flatMap((block) => {
81
+ if (block.bundle !== 'icms_layout_anchor') {
82
+ return []
83
+ }
84
+
85
+ const label = block.props?.label?.trim() ?? ''
86
+ const id = resolveAnchorId(label, block.props?.anchor)
87
+
88
+ return id ? [{ id, label }] : []
89
+ }),
90
+ )
91
+
92
+ // Highlight the last anchor that scrolled past the bottom edge of the sticky bar.
93
+ const updateActive = () => {
94
+ const line = anchorOffset.value + 1
95
+
96
+ activeId.value =
97
+ anchors.value.findLast((anchor) => {
98
+ const top = document
99
+ .getElementById(anchor.id)
100
+ ?.getBoundingClientRect().top
101
+
102
+ return top !== undefined && top <= line
103
+ })?.id ?? ''
104
+ }
105
+
106
+ // Throttle to one check per frame.
107
+ let frame = 0
108
+ const scheduleUpdate = () => {
109
+ if (frame) {
110
+ return
111
+ }
112
+
113
+ frame = requestAnimationFrame(() => {
114
+ frame = 0
115
+ updateActive()
116
+ })
117
+ }
118
+
119
+ // Editing the page changes the anchors, so recheck which one is active.
120
+ watch(anchors, updateActive, { flush: 'post' })
121
+
122
+ onMounted(() => {
123
+ updateActive()
124
+ window.addEventListener('scroll', scheduleUpdate, { passive: true })
125
+ window.addEventListener('resize', scheduleUpdate, { passive: true })
126
+ })
127
+
128
+ onBeforeUnmount(() => {
129
+ window.removeEventListener('scroll', scheduleUpdate)
130
+ window.removeEventListener('resize', scheduleUpdate)
131
+ cancelAnimationFrame(frame)
132
+ })
133
+
134
+ // Not sticky while editing, it would cover the blocks in the editor canvas.
135
+ const containerClass = computed(() =>
136
+ cn(
137
+ themedClass('navigationContainerClass', 'border-b border-gray-100'),
138
+ isEditing
139
+ ? ''
140
+ : themedClass('stickyClass', 'sticky top-(--anchor-navigation-top) z-40'),
141
+ ),
142
+ )
143
+
144
+ const linkClass = (isActive: boolean) =>
145
+ cn(
146
+ themedClass(
147
+ 'linkClass',
148
+ 'hover:text-primary block py-4 text-sm whitespace-nowrap motion-safe:transition-colors',
149
+ ),
150
+ isActive
151
+ ? themedClass('linkActiveClass', 'text-primary font-semibold')
152
+ : themedClass('linkInactiveClass', 'text-gray-600'),
153
+ )
154
+ </script>
@@ -0,0 +1,5 @@
1
+ # Placeholder paragraph for a frontend-defined fragment (defineBlokkliFragment).
2
+ # @blokkli/editor's rendering component resolves the fragment component by `name`.
3
+ fragment paragraphBlokkliFragment on ParagraphBlokkliFragment {
4
+ name: fieldBlokkliFragmentName
5
+ }
@@ -3,7 +3,7 @@ fragment paragraphIcmsButtonElement on ParagraphIcmsButtonElement {
3
3
  title: fieldIcmsLinkTitle
4
4
  link: fieldIcmsLink {
5
5
  uri {
6
- path
6
+ ...linkUrl
7
7
  }
8
8
  }
9
9
  params: fieldIcmsLinkParams
@@ -3,13 +3,14 @@
3
3
  <DesignSystemAtomsButton
4
4
  :label="label"
5
5
  :title="title"
6
- :link="link?.uri?.path"
6
+ :link="buttonLink"
7
7
  :params="params"
8
- :target-blank="targetBlank"
9
- :buttonStyle="buttonStyle"
10
- :colorScheme="colorScheme"
11
- :buttonClass="themedClass('buttonClass')"
12
- :labelClass="themedClass('labelClass')"
8
+ :external="isFileLink"
9
+ :target-blank="targetBlank || isFileLink"
10
+ :button-style="buttonStyle"
11
+ :color-scheme="colorScheme"
12
+ :button-class="themedClass('buttonClass')"
13
+ :label-class="themedClass('labelClass')"
13
14
  />
14
15
  </div>
15
16
  </template>
@@ -20,9 +21,13 @@ import type { ParagraphIcmsButtonElementFragment } from '#graphql-operations'
20
21
 
21
22
  const themedClass = useTheme('ParagraphButtonElement')
22
23
 
23
- const { options, isEditing } = useAttrs().blokkliData as any
24
+ const { options } = useAttrs().blokkliData as any
24
25
 
25
- defineProps<ParagraphIcmsButtonElementFragment>()
26
+ const props = defineProps<ParagraphIcmsButtonElementFragment>()
27
+
28
+ const resolvedLink = computed(() => resolveLinkUrl(props.link?.uri))
29
+ const isFileLink = computed(() => resolvedLink.value.isFile)
30
+ const buttonLink = computed(() => resolvedLink.value.url)
26
31
 
27
32
  const buttonStyle = computed(() => {
28
33
  return options.value.buttonStyle
@@ -22,7 +22,7 @@ const getComponent = (variant: CardVariant) => {
22
22
  spotlight: DesignSystemOrganismsSpotlightCard,
23
23
  fact: DesignSystemOrganismsFactCard,
24
24
  }
25
- return components[variant] || FeatureCard
25
+ return components[variant] || DesignSystemOrganismsFeatureCard
26
26
  }
27
27
 
28
28
  const variant = inject<CardVariant>('variant')
@@ -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
  >
@@ -19,12 +19,12 @@
19
19
  >
20
20
  <DesignSystemOrganismsContentBlock
21
21
  :pretitle="pretitle"
22
- :pretitleClass="themedClass('pretitleClass')"
22
+ :pretitle-class="themedClass('pretitleClass')"
23
23
  :title="title"
24
- :titleClass="themedClass('titleClass', 'mb-0')"
25
- :titleWeight="titleWeight"
26
- :titleStyle="titleStyle"
27
- :colorScheme="colorScheme"
24
+ :title-class="themedClass('titleClass', 'mb-0')"
25
+ :title-weight="titleWeight"
26
+ :title-style="titleStyle"
27
+ :color-scheme="colorScheme"
28
28
  />
29
29
  </div>
30
30
  <div
@@ -38,10 +38,10 @@
38
38
  >
39
39
  <DesignSystemOrganismsContentBlock
40
40
  :subtitle="subtitle"
41
- :subtitleClass="themedClass('subtitleClass')"
41
+ :subtitle-class="themedClass('subtitleClass')"
42
42
  :text="text"
43
- :textClass="themedClass('textClass')"
44
- :colorScheme="colorScheme"
43
+ :text-class="themedClass('textClass')"
44
+ :color-scheme="colorScheme"
45
45
  />
46
46
  </div>
47
47
  </DesignSystemAtomsGridRow>
@@ -2,7 +2,7 @@
2
2
  <div v-if="isEditing" :class="title({ colorScheme })">
3
3
  <DesignSystemOrganismsContentBlock
4
4
  :text="collapsibleTitle"
5
- :textClass="themedClass('textClass', 'text-sm mb-3')"
5
+ :text-class="themedClass('textClass', 'text-sm mb-3')"
6
6
  />
7
7
  <BlokkliField
8
8
  name="field_icms_collapsible_layouts"
@@ -38,7 +38,7 @@ import type { ParagraphIcmsCollapsibleElementFragment } from '#graphql-operation
38
38
 
39
39
  defineProps<ParagraphIcmsCollapsibleElementFragment>()
40
40
 
41
- const { options, isEditing } = useAttrs().blokkliData as any
41
+ const { isEditing } = useAttrs().blokkliData as any
42
42
 
43
43
  const themedClass = useTheme('ParagraphCollapsibleElement')
44
44
 
@@ -1,7 +1,7 @@
1
1
  <template>
2
2
  <DesignSystemAtomsContainer
3
3
  :spacing="spacing"
4
- :colorScheme="colorScheme"
4
+ :color-scheme="colorScheme"
5
5
  :class="themedClass('containerClass')"
6
6
  >
7
7
  <DesignSystemAtomsGridRow :class="themedClass('firstRowClass')">
@@ -17,9 +17,9 @@
17
17
  <DesignSystemOrganismsContentBlock
18
18
  :pretitle="pretitle"
19
19
  :title="title"
20
- :titleWeight="titleWeight"
21
- :titleStyle="titleStyle"
22
- :titleClass="themedClass('titleClass')"
20
+ :title-weight="titleWeight"
21
+ :title-style="titleStyle"
22
+ :title-class="themedClass('titleClass')"
23
23
  :subtitle="subtitle"
24
24
  :text="text"
25
25
  />
@@ -32,7 +32,7 @@
32
32
  })
33
33
  "
34
34
  >
35
- <div class="relative col-span-12" v-if="isEditing">
35
+ <div v-if="isEditing" class="relative col-span-12">
36
36
  <BlokkliField
37
37
  name="field_icms_collapsible_elements"
38
38
  :list="collapsibleElements"
@@ -41,15 +41,15 @@
41
41
  </div>
42
42
 
43
43
  <div
44
- :class="themedClass('secondRowColumnClass', 'relative col-span-12')"
45
44
  v-else
45
+ :class="themedClass('secondRowColumnClass', 'relative col-span-12')"
46
46
  >
47
47
  <template v-if="collapsibleVariant === 'tabs'">
48
48
  <UTabs
49
49
  v-if="!isMobile"
50
50
  :items="tabItems"
51
51
  :default-index="0"
52
- :unmountOnHide="false"
52
+ :unmount-on-hide="false"
53
53
  variant="link"
54
54
  :ui="{
55
55
  root: themedClass('tabsClass', 'gap-0'),
@@ -58,7 +58,7 @@
58
58
  indicator: tabIndicator({ colorScheme }),
59
59
  }"
60
60
  >
61
- <template #content="{ item, index }">
61
+ <template #content="{ item }">
62
62
  <div :class="themedClass('tabsContentInnerClass', 'py-6')">
63
63
  <BlokkliField
64
64
  name="field_icms_collapsible_elements"
@@ -91,9 +91,9 @@
91
91
  content: themedClass('accordionContentClass'),
92
92
  trailingIcon: accordionTrailingIcon({ colorScheme }),
93
93
  }"
94
- @update:modelValue="handleAccordionClick"
94
+ @update:model-value="handleAccordionClick"
95
95
  >
96
- <template #content="{ item, index, open }">
96
+ <template #content="{ item }">
97
97
  <div :class="themedClass('accordionContentInnerClass', 'py-6')">
98
98
  <BlokkliField
99
99
  name="field_icms_collapsible_elements"
@@ -127,9 +127,9 @@
127
127
  content: accordionContent({ colorScheme }),
128
128
  trailingIcon: accordionTrailingIcon({ colorScheme }),
129
129
  }"
130
- @update:modelValue="handleAccordionClick"
130
+ @update:model-value="handleAccordionClick"
131
131
  >
132
- <template #content="{ item, index, open }">
132
+ <template #content="{ item }">
133
133
  <div :class="themedClass('accordionContentInnerClass', 'py-6')">
134
134
  <BlokkliField
135
135
  name="field_icms_collapsible_elements"
@@ -154,12 +154,11 @@
154
154
  <template v-if="collapsibleVariant === 'carousel'">
155
155
  <div :class="carousel({ colorScheme })">
156
156
  <UCarousel
157
- v-model="carouselIndex"
158
157
  :items="collapsibleElements || []"
159
158
  :arrows="(collapsibleElements?.length || 0) > 1"
160
159
  :watch-drag="(collapsibleElements?.length || 0) > 1"
161
- nextIcon="i-lucide-chevron-right"
162
- prevIcon="i-lucide-chevron-left"
160
+ next-icon="i-lucide-chevron-right"
161
+ prev-icon="i-lucide-chevron-left"
163
162
  :class="themedClass('carouselClass')"
164
163
  :ui="{
165
164
  viewport: themedClass('carouselViewportClass', 'pt-8'),
@@ -188,7 +187,11 @@
188
187
  :list="[item]"
189
188
  proxy-mode
190
189
  >
191
- <BlokkliItem v-for="fieldItem in items" v-bind="fieldItem" />
190
+ <BlokkliItem
191
+ v-for="fieldItem in items"
192
+ :key="fieldItem.uuid"
193
+ v-bind="fieldItem"
194
+ />
192
195
  </BlokkliField>
193
196
  </template>
194
197
  </UCarousel>
@@ -238,8 +241,6 @@ const accordionItems = computed(() => {
238
241
  const accordionRef = ref()
239
242
  const { scrollToTrigger } = useAccordionScroll()
240
243
 
241
- const carouselIndex = ref(0)
242
-
243
244
  const handleAccordionClick = (
244
245
  payload: string | string[] | number | undefined,
245
246
  ): void => {