@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
@@ -0,0 +1,58 @@
1
+ import { defineMultiCacheOptions } from 'nuxt-multi-cache/server-options'
2
+ import lruCacheDriver from 'unstorage/drivers/lru-cache'
3
+ import { useRuntimeConfig } from 'nitropack/runtime'
4
+ import {
5
+ alterCachedHeaders,
6
+ boundedLruOptions,
7
+ buildRouteCacheKey,
8
+ containerMemoryLimit,
9
+ isBlokkliRequest,
10
+ resolvePageCacheConfig,
11
+ resolveRouteMaxSize,
12
+ routeCacheDriver,
13
+ } from './pageCache'
14
+ import { LruCacheTagRegistry } from './cacheTagRegistry'
15
+ import { enabledForRequest } from './enabledForRequest'
16
+
17
+ export default defineMultiCacheOptions(() => {
18
+ const config = resolvePageCacheConfig(useRuntimeConfig().pageCache)
19
+
20
+ const dataDriver = lruCacheDriver(
21
+ boundedLruOptions(config.dataMaxSize, (key) =>
22
+ cacheTagRegistry.forget('data', key),
23
+ ),
24
+ )
25
+ const routeDriver = routeCacheDriver(
26
+ resolveRouteMaxSize(config, containerMemoryLimit()),
27
+ config.routeCompression,
28
+ (key) => cacheTagRegistry.forget('route', key),
29
+ )
30
+ const drivers = { data: dataDriver, route: routeDriver }
31
+ const cacheTagRegistry = new LruCacheTagRegistry(
32
+ (cacheType, key) =>
33
+ cacheType !== 'component' && !!drivers[cacheType].hasItem(key, {}),
34
+ )
35
+
36
+ return {
37
+ api: {},
38
+ // Purging looks the tags up instead of decoding every cached entry.
39
+ cacheTagRegistry,
40
+ data: {
41
+ storage: {
42
+ driver: dataDriver,
43
+ },
44
+ },
45
+ // Requests with a Drupal session bypass all caches.
46
+ enabledForRequest,
47
+ route: {
48
+ storage: {
49
+ driver: routeDriver,
50
+ },
51
+ // Pages opt in via usePageCache(); this only rules out requests that
52
+ // must never be stored.
53
+ applies: (path: string) => !isBlokkliRequest(path),
54
+ buildCacheKey: buildRouteCacheKey,
55
+ alterCachedHeaders,
56
+ },
57
+ }
58
+ })
@@ -0,0 +1,360 @@
1
+ import type { H3Event } from 'h3'
2
+ import {
3
+ getRequestHeader,
4
+ getResponseHeader,
5
+ getResponseStatus,
6
+ removeResponseHeader,
7
+ setResponseHeader,
8
+ } from 'h3'
9
+ import { readFileSync } from 'node:fs'
10
+ import { promisify } from 'node:util'
11
+ import { gunzip, gzip } from 'node:zlib'
12
+ import lruCacheDriver from 'unstorage/drivers/lru-cache'
13
+ import type { PageCacheConfig } from '../utils/pageCache'
14
+ import { hasDrupalSession } from './enabledForRequest'
15
+
16
+ export {
17
+ PAGE_CACHE_DEFAULTS,
18
+ pageCacheDefaults,
19
+ resolvePageCacheConfig,
20
+ } from '../utils/pageCache'
21
+ export type { PageCacheConfig, PageCacheMode } from '../utils/pageCache'
22
+
23
+ export { hasDrupalSession }
24
+
25
+ /**
26
+ * Whether the request is a blökkli editor or preview request.
27
+ */
28
+ export function isBlokkliRequest(pathWithQuery: string) {
29
+ const [path = '', query = ''] = pathWithQuery.split('?', 2)
30
+ return (
31
+ path.startsWith('/blokkli') ||
32
+ /(?:^|&)blokkli(?:Editing|Preview)(?:[=&]|$)/.test(query)
33
+ )
34
+ }
35
+
36
+ /**
37
+ * Query parameters that never change the rendered page: analytics and ad
38
+ * click identifiers. They are dropped from the route cache key so that
39
+ * campaign links don't split the cache.
40
+ */
41
+ const IGNORED_QUERY_PARAMS =
42
+ /^(?:utm_\w+|gclid|gclsrc|gbraid|wbraid|dclid|fbclid|msclkid|twclid|li_fat_id|igshid|yclid|srsltid|mc_cid|mc_eid|_ga|_gl|_hsenc|_hsmi|mkt_tok)$/i
43
+
44
+ /**
45
+ * The route cache key: scheme + host + path + the relevant query parameters,
46
+ * sorted. Cookies are deliberately not part of it — requests with a Drupal
47
+ * session never reach the route cache.
48
+ *
49
+ * Both Host and X-Forwarded-Host are part of the key: whichever of them the
50
+ * app ends up rendering absolute URLs from, a spoofed header only ever lands
51
+ * in its own cache entry.
52
+ *
53
+ * The key is URI-encoded because unstorage drops everything after a "?" and
54
+ * treats "/" and ":" as path separators when normalizing keys.
55
+ */
56
+ export function buildRouteCacheKey(event: H3Event): string {
57
+ const url = new URL(event.path || '/', 'http://localhost')
58
+ const params = [...url.searchParams]
59
+ .filter(([name]) => !IGNORED_QUERY_PARAMS.test(name))
60
+ .sort(([a, av], [b, bv]) => (a === b ? cmp(av, bv) : cmp(a, b)))
61
+ const query = new URLSearchParams(params).toString()
62
+
63
+ const proto = getRequestHeader(event, 'x-forwarded-proto') || ''
64
+ const forwardedHost = getRequestHeader(event, 'x-forwarded-host') || ''
65
+ const host = getRequestHeader(event, 'host') || ''
66
+
67
+ return encodeURIComponent(
68
+ [proto, forwardedHost, host].join('|').toLowerCase() +
69
+ url.pathname +
70
+ (query ? '?' + query : ''),
71
+ )
72
+ }
73
+
74
+ function cmp(a: string, b: string) {
75
+ return a < b ? -1 : a > b ? 1 : 0
76
+ }
77
+
78
+ /**
79
+ * Approximate memory footprint of a cached value in bytes.
80
+ *
81
+ * JS engines (JavaScriptCore in Bun, V8 in Node) store a string with one
82
+ * byte per character as long as it only contains Latin-1 characters, and
83
+ * two bytes per character otherwise. Buffer.byteLength() (UTF-8) would
84
+ * undercount the second case by up to half.
85
+ */
86
+ export function cacheEntrySize(value: unknown, key: string): number {
87
+ if (value instanceof Uint8Array) {
88
+ return value.byteLength + key.length * 2
89
+ }
90
+ const string =
91
+ typeof value === 'string' ? value : (JSON.stringify(value) ?? '')
92
+ // eslint-disable-next-line no-control-regex
93
+ const perChar = /[^\u0000-\u00ff]/.test(string) ? 2 : 1
94
+ return string.length * perChar + key.length * 2
95
+ }
96
+
97
+ /**
98
+ * Options for the unstorage lru-cache driver that bound it by bytes.
99
+ *
100
+ * A single entry may use at most a quarter of the budget, so one huge page
101
+ * can't flush the whole cache. `max` is only a safety net for the entry count.
102
+ */
103
+ export function boundedLruOptions(
104
+ maxSize: number,
105
+ onRemove?: (key: string) => void,
106
+ ) {
107
+ return {
108
+ max: 5000,
109
+ maxSize: Math.max(maxSize, 1),
110
+ maxEntrySize: Math.max(Math.floor(maxSize / 4), 1),
111
+ sizeCalculation: (value: unknown, key: string) =>
112
+ Math.max(cacheEntrySize(value, key), 1),
113
+ // Called when an entry is evicted, deleted or replaced.
114
+ dispose: onRemove
115
+ ? (_value: unknown, key: string) => onRemove(key)
116
+ : undefined,
117
+ }
118
+ }
119
+
120
+ /**
121
+ * The memory limit of the container in bytes, if known.
122
+ *
123
+ * Platform.sh/Upsun containers expose their size in /run/config.json
124
+ * (`info.limits.memory`, in MB); their cgroup files are not readable and
125
+ * process.constrainedMemory() reports the memory of the host. Elsewhere the
126
+ * cgroup limit is used.
127
+ */
128
+ export function containerMemoryLimit(
129
+ readFile: (path: string) => string = (path) => readFileSync(path, 'utf8'),
130
+ ): number | null {
131
+ const read = (path: string) => {
132
+ try {
133
+ return readFile(path).trim()
134
+ } catch {
135
+ return null
136
+ }
137
+ }
138
+
139
+ const platformConfig = read('/run/config.json')
140
+ if (platformConfig) {
141
+ try {
142
+ const memory = Number(JSON.parse(platformConfig)?.info?.limits?.memory)
143
+ if (memory > 0) {
144
+ return memory * 1024 * 1024
145
+ }
146
+ } catch {
147
+ // Not the expected format, try the cgroup limit.
148
+ }
149
+ }
150
+
151
+ for (const path of [
152
+ '/sys/fs/cgroup/memory.max',
153
+ '/sys/fs/cgroup/memory/memory.limit_in_bytes',
154
+ ]) {
155
+ const limit = Number(read(path))
156
+ // "max" (v2) or a huge number (v1) means unlimited.
157
+ if (limit > 0 && limit < 2 ** 50) {
158
+ return limit
159
+ }
160
+ }
161
+ return null
162
+ }
163
+
164
+ const MB = 1024 * 1024
165
+
166
+ export const AUTO_ROUTE_MAX_SIZE = Object.freeze({
167
+ share: 0.03,
168
+ min: 8 * MB,
169
+ max: 64 * MB,
170
+ fallback: 24 * MB,
171
+ })
172
+
173
+ /**
174
+ * The route cache budget: the configured one, or for `auto` a share of the
175
+ * container memory (see the package README for the measurements behind it).
176
+ */
177
+ export function resolveRouteMaxSize(
178
+ config: Pick<PageCacheConfig, 'routeMaxSize'>,
179
+ memoryLimit: number | null,
180
+ ): number {
181
+ if (config.routeMaxSize !== 'auto') {
182
+ return config.routeMaxSize
183
+ }
184
+ if (!memoryLimit) {
185
+ return AUTO_ROUTE_MAX_SIZE.fallback
186
+ }
187
+ return Math.min(
188
+ Math.max(
189
+ Math.floor(memoryLimit * AUTO_ROUTE_MAX_SIZE.share),
190
+ AUTO_ROUTE_MAX_SIZE.min,
191
+ ),
192
+ AUTO_ROUTE_MAX_SIZE.max,
193
+ )
194
+ }
195
+
196
+ const gzipAsync = promisify(gzip)
197
+ const gunzipAsync = promisify(gunzip)
198
+
199
+ /**
200
+ * The route cache storage driver: a bounded LRU that stores each page as a
201
+ * UTF-8 buffer, gzipped by default.
202
+ *
203
+ * A JS string takes two bytes per character as soon as a page has a single
204
+ * character outside Latin-1 (a typographic apostrophe is enough); UTF-8 takes
205
+ * one for markup. gzip shrinks a page about 8× further. nuxt-multi-cache
206
+ * accepts a Buffer from getItemRaw(). For a 400 KB page compression costs
207
+ * about 3 ms when storing and 4 ms per hit (5 ms instead of 1 ms); purging
208
+ * decompresses nothing thanks to the cache tag registry.
209
+ */
210
+ export function routeCacheDriver(
211
+ maxSize: number,
212
+ compress: boolean,
213
+ onRemove?: (key: string) => void,
214
+ ) {
215
+ const driver = lruCacheDriver(boundedLruOptions(maxSize, onRemove))
216
+ const encode = (value: string) =>
217
+ compress ? gzipAsync(value) : Promise.resolve(Buffer.from(value))
218
+ const decode = async (value: unknown) =>
219
+ compress && value instanceof Uint8Array ? gunzipAsync(value) : value
220
+ return {
221
+ ...driver,
222
+ async setItemRaw(key: string, value: unknown) {
223
+ driver.setItemRaw!(
224
+ key,
225
+ typeof value === 'string' ? await encode(value) : value,
226
+ {},
227
+ )
228
+ },
229
+ async getItemRaw(key: string) {
230
+ return (await decode(driver.getItemRaw!(key, {}))) ?? null
231
+ },
232
+ // Used by the stats and inspect API endpoints.
233
+ async getItem(key: string) {
234
+ const value = await decode(driver.getItem(key, {}))
235
+ return value instanceof Uint8Array
236
+ ? Buffer.from(value).toString()
237
+ : (value ?? null)
238
+ },
239
+ }
240
+ }
241
+
242
+ type RouteCacheHelperLike = {
243
+ cacheable: boolean | null
244
+ maxAge: number
245
+ setUncacheable(): unknown
246
+ }
247
+
248
+ type MultiCacheContextLike = {
249
+ cdn?: unknown
250
+ route?: RouteCacheHelperLike
251
+ routeServedFromCache?: boolean
252
+ }
253
+
254
+ function getMultiCacheContext(event: H3Event): MultiCacheContextLike {
255
+ return (event.context.multiCache || {}) as MultiCacheContextLike
256
+ }
257
+
258
+ /**
259
+ * Finalize the route cache decision just before the response is sent (the
260
+ * route cache stores it in the afterResponse hook, which runs later).
261
+ *
262
+ * - Never store a response that sets a cookie.
263
+ * - Clamp the max age: nuxt-multi-cache treats "permanent" (-1) as the
264
+ * lowest value when merging, so a permanent data cache item could
265
+ * otherwise make the whole page permanent.
266
+ */
267
+ export function finalizeRouteCache(event: H3Event, config: PageCacheConfig) {
268
+ const route = getMultiCacheContext(event).route
269
+ if (!route?.cacheable) {
270
+ return
271
+ }
272
+
273
+ if (getResponseHeader(event, 'set-cookie')) {
274
+ route.setUncacheable()
275
+ return
276
+ }
277
+
278
+ if (route.maxAge < 0 || route.maxAge > config.routeMaxAge) {
279
+ route.maxAge = config.routeMaxAge
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Rewrite the shared-cache headers collected by the nuxt-multi-cache CDN
285
+ * helper into browser-only headers when no shared cache is in front of Nuxt.
286
+ *
287
+ * - Cache-Tag only serves a shared cache; it is always dropped. (Error pages
288
+ * render in a sub-request whose headers are copied over, so it can also
289
+ * show up without collected cacheability.)
290
+ * - Responses for a Drupal session are never cached anywhere. Logged-in users
291
+ * get the SPA shell without SSR, so there is no collected cacheability.
292
+ * - Otherwise only responses whose cacheability was collected by the CDN
293
+ * helper (SSR pages, GraphQL API routes) are rewritten. Static assets keep
294
+ * the headers of their route rules.
295
+ */
296
+ export function applyCacheControlHeaders(
297
+ event: H3Event,
298
+ config: PageCacheConfig,
299
+ ) {
300
+ if (config.mode === 'varnish') {
301
+ return
302
+ }
303
+
304
+ removeResponseHeader(event, 'cache-tag')
305
+
306
+ const hasCacheability = !!getMultiCacheContext(event).cdn
307
+ const isHtml = /^text\/html\b/.test(
308
+ String(getResponseHeader(event, 'content-type') || ''),
309
+ )
310
+ const hasSession = hasDrupalSession(getRequestHeader(event, 'cookie'))
311
+
312
+ if (hasSession && (hasCacheability || isHtml)) {
313
+ setResponseHeader(event, 'cache-control', 'private, no-store')
314
+ return
315
+ }
316
+
317
+ if (!hasCacheability) {
318
+ return
319
+ }
320
+
321
+ const cacheControl = String(getResponseHeader(event, 'cache-control') || '')
322
+ const isCacheable =
323
+ getResponseStatus(event) < 500 &&
324
+ /\bpublic\b/.test(cacheControl) &&
325
+ !/\b(?:private|no-store|no-cache)\b/.test(cacheControl)
326
+
327
+ setResponseHeader(
328
+ event,
329
+ 'cache-control',
330
+ isCacheable && config.browserMaxAge > 0
331
+ ? `public, max-age=${config.browserMaxAge}`
332
+ : 'private, no-store',
333
+ )
334
+ }
335
+
336
+ /**
337
+ * Expose whether the route cache served the response, for monitoring.
338
+ */
339
+ export function setRouteCacheStatusHeader(event: H3Event) {
340
+ const context = getMultiCacheContext(event)
341
+ if (context.routeServedFromCache) {
342
+ setResponseHeader(event, 'x-icms-page-cache', 'HIT')
343
+ } else if (context.route?.cacheable) {
344
+ setResponseHeader(event, 'x-icms-page-cache', 'MISS')
345
+ }
346
+ }
347
+
348
+ /**
349
+ * Headers that must never be replayed from the route cache.
350
+ */
351
+ export function alterCachedHeaders(
352
+ headers: Record<string, string | string[] | number | undefined>,
353
+ ) {
354
+ const {
355
+ 'set-cookie': _cookie,
356
+ 'x-icms-page-cache': _status,
357
+ ...rest
358
+ } = headers
359
+ return rest
360
+ }
@@ -0,0 +1,3 @@
1
+ <svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M15 3C17.7614 3 20 5.23858 20 8C20 10.0502 18.7657 11.8114 17 12.583V21.3447L21.5 18.7471C21.8093 18.5685 21.9999 18.238 22 17.8809V16C22 14.8954 22.8954 14 24 14C25.1046 14 26 14.8954 26 16V20.1904C25.9999 20.5476 25.8093 20.878 25.5 21.0566L17 25.9639L15.46 26.7617C15.1715 26.9111 14.8285 26.9111 14.54 26.7617L13 25.9639L4.5 21.0566C4.19066 20.878 4.00007 20.5476 4 20.1904V16C4 14.8954 4.89543 14 6 14C7.10457 14 8 14.8954 8 16V17.8809C8.00006 18.238 8.19067 18.5685 8.5 18.7471L13 21.3447V12.583C11.2343 11.8114 10 10.0502 10 8C10 5.23858 12.2386 3 15 3ZM15 6C13.8954 6 13 6.89543 13 8C13 9.10457 13.8954 10 15 10C16.1046 10 17 9.10457 17 8C17 6.89543 16.1046 6 15 6Z" fill="currentColor"/>
3
+ </svg>
@@ -0,0 +1,13 @@
1
+ <template>
2
+ <ParagraphAnchor v-bind="props" :blokkliData="blokkliData" />
3
+ </template>
4
+
5
+ <script lang="ts" setup>
6
+ import type { ParagraphIcmsLayoutAnchorFragment } from '#graphql-operations'
7
+
8
+ const props = defineProps<ParagraphIcmsLayoutAnchorFragment>()
9
+
10
+ const blokkliData = defineBlokkli({
11
+ bundle: 'icms_layout_anchor',
12
+ })
13
+ </script>
@@ -0,0 +1,8 @@
1
+ <svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect x="12" y="8" width="14" height="2" rx="0.5" fill="currentColor"/>
3
+ <rect x="12" y="20" width="14" height="2" rx="0.5" fill="currentColor"/>
4
+ <rect x="12" y="14" width="14" height="2" rx="0.5" fill="currentColor"/>
5
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M6.5 6C7.1276 6 7.63636 6.46884 7.63636 7.04717C7.63636 7.47655 7.35585 7.84541 6.95455 8.00702V9.84202L7.97727 9.29798C8.04757 9.26058 8.0909 9.19138 8.09091 9.11657V8.72265C8.09091 8.49132 8.29442 8.30378 8.54545 8.30378C8.79649 8.30378 9 8.49132 9 8.72265V9.60027C8.99998 9.67508 8.95667 9.74428 8.88636 9.78169L6.95455 10.8094L6.60454 10.9765C6.53898 11.0078 6.46102 11.0078 6.39546 10.9765L6.04545 10.8094L4.11364 9.78169C4.04333 9.74428 4.00002 9.67508 4 9.60027V8.72265C4 8.49132 4.20351 8.30378 4.45455 8.30378C4.70558 8.30378 4.90909 8.49132 4.90909 8.72265V9.11657C4.9091 9.19138 4.95243 9.26058 5.02273 9.29798L6.04545 9.84202V8.00702C5.64415 7.84541 5.36364 7.47655 5.36364 7.04717C5.36364 6.46884 5.8724 6 6.5 6ZM6.5 6.6283C6.24896 6.6283 6.04545 6.81584 6.04545 7.04717C6.04545 7.27851 6.24896 7.46604 6.5 7.46604C6.75104 7.46604 6.95455 7.27851 6.95455 7.04717C6.95455 6.81584 6.75104 6.6283 6.5 6.6283Z" fill="currentColor"/>
6
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M6.5 12C7.1276 12 7.63636 12.4688 7.63636 13.0472C7.63636 13.4766 7.35585 13.8454 6.95455 14.007V15.842L7.97727 15.298C8.04757 15.2606 8.0909 15.1914 8.09091 15.1166V14.7227C8.09091 14.4913 8.29442 14.3038 8.54545 14.3038C8.79649 14.3038 9 14.4913 9 14.7227V15.6003C8.99998 15.6751 8.95667 15.7443 8.88636 15.7817L6.95455 16.8094L6.60454 16.9765C6.53898 17.0078 6.46102 17.0078 6.39546 16.9765L6.04545 16.8094L4.11364 15.7817C4.04333 15.7443 4.00002 15.6751 4 15.6003V14.7227C4 14.4913 4.20351 14.3038 4.45455 14.3038C4.70558 14.3038 4.90909 14.4913 4.90909 14.7227V15.1166C4.9091 15.1914 4.95243 15.2606 5.02273 15.298L6.04545 15.842V14.007C5.64415 13.8454 5.36364 13.4766 5.36364 13.0472C5.36364 12.4688 5.8724 12 6.5 12ZM6.5 12.6283C6.24896 12.6283 6.04545 12.8158 6.04545 13.0472C6.04545 13.2785 6.24896 13.466 6.5 13.466C6.75104 13.466 6.95455 13.2785 6.95455 13.0472C6.95455 12.8158 6.75104 12.6283 6.5 12.6283Z" fill="currentColor"/>
7
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M6.5 18C7.1276 18 7.63636 18.4688 7.63636 19.0472C7.63636 19.4766 7.35585 19.8454 6.95455 20.007V21.842L7.97727 21.298C8.04757 21.2606 8.0909 21.1914 8.09091 21.1166V20.7227C8.09091 20.4913 8.29442 20.3038 8.54545 20.3038C8.79649 20.3038 9 20.4913 9 20.7227V21.6003C8.99998 21.6751 8.95667 21.7443 8.88636 21.7817L6.95455 22.8094L6.60454 22.9765C6.53898 23.0078 6.46102 23.0078 6.39546 22.9765L6.04545 22.8094L4.11364 21.7817C4.04333 21.7443 4.00002 21.6751 4 21.6003V20.7227C4 20.4913 4.20351 20.3038 4.45455 20.3038C4.70558 20.3038 4.90909 20.4913 4.90909 20.7227V21.1166C4.9091 21.1914 4.95243 21.2606 5.02273 21.298L6.04545 21.842V20.007C5.64415 19.8454 5.36364 19.4766 5.36364 19.0472C5.36364 18.4688 5.8724 18 6.5 18ZM6.5 18.6283C6.24896 18.6283 6.04545 18.8158 6.04545 19.0472C6.04545 19.2785 6.24896 19.466 6.5 19.466C6.75104 19.466 6.95455 19.2785 6.95455 19.0472C6.95455 18.8158 6.75104 18.6283 6.5 18.6283Z" fill="currentColor"/>
8
+ </svg>
@@ -0,0 +1,9 @@
1
+ <template>
2
+ <ParagraphAnchorNavigation :blokkliData="blokkliData" />
3
+ </template>
4
+
5
+ <script lang="ts" setup>
6
+ const blokkliData = defineBlokkli({
7
+ bundle: 'icms_layout_anchor_navigation',
8
+ })
9
+ </script>
@@ -10,5 +10,4 @@ const props = defineProps<ParagraphIcmsCardElementFragment>()
10
10
  const blokkliData = defineBlokkli({
11
11
  bundle: 'icms_card_element',
12
12
  })
13
-
14
13
  </script>
@@ -7,6 +7,11 @@ import type { ParagraphIcmsCollapsibleElementFragment } from '#graphql-operation
7
7
 
8
8
  const props = defineProps<ParagraphIcmsCollapsibleElementFragment>()
9
9
 
10
+ // This bundle holds nested blocks: ParagraphCollapsibleElement renders
11
+ // `<BlokkliField name="field_icms_collapsible_layouts">`. blokkli detects
12
+ // nested bundles by scanning the block component itself for that tag, so
13
+ // naming it here is what puts the bundle into `BlockBundleWithNested` and
14
+ // makes it usable as a `renderFor.parentBundle`.
10
15
  const blokkliData = defineBlokkli({
11
16
  bundle: 'icms_collapsible_element',
12
17
  })
@@ -10,5 +10,4 @@ const props = defineProps<ParagraphIcmsFaqElementFragment>()
10
10
  const blokkliData = defineBlokkli({
11
11
  bundle: 'icms_faq_element',
12
12
  })
13
-
14
13
  </script>
@@ -0,0 +1,4 @@
1
+ <svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M4 15H26" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
3
+ <path d="M7 8H23M7 22H23" stroke="currentColor" stroke-width="2" stroke-linecap="round" opacity="0.3"/>
4
+ </svg>
@@ -0,0 +1,87 @@
1
+ <template>
2
+ <DesignSystemAtomsContainer
3
+ :color-scheme="parentColorScheme"
4
+ :spacing="spacing"
5
+ :class="themedClass('containerClass', minSpacingClassList)"
6
+ >
7
+ <DesignSystemAtomsGridRow>
8
+ <hr
9
+ :class="
10
+ themedClass(
11
+ 'hrClass',
12
+ cn('col-span-12 border-0 border-t', lineColorClass),
13
+ )
14
+ "
15
+ />
16
+ </DesignSystemAtomsGridRow>
17
+ </DesignSystemAtomsContainer>
18
+ </template>
19
+
20
+ <script lang="ts" setup>
21
+ import type { ColorScheme } from '@iqual/nuxt-icms/types/global'
22
+ import cn from '@iqual/nuxt-icms/composables/useCn'
23
+
24
+ /**
25
+ * A horizontal rule between blocks.
26
+ *
27
+ * Fragments are blocks without backend data: editors place them, the frontend
28
+ * decides what they render. Any component under `components/Blokkli/Fragment`
29
+ * calling `defineBlokkliFragment()` is discovered at build time and offered in
30
+ * the editor (see modules/blokkliFragments.ts).
31
+ *
32
+ * Unlike a layout block, the colour option colours the line instead of the
33
+ * background — a separator has nothing else to colour. The background stays
34
+ * whatever the surrounding section uses.
35
+ */
36
+ const { options } = defineBlokkliFragment({
37
+ name: 'separator',
38
+ label: 'Separator',
39
+ description: 'A horizontal line between two blocks.',
40
+ globalOptions: ['spacingTop', 'spacingBottom', 'colorScheme'],
41
+ })
42
+
43
+ const themedClass = useTheme('BlokkliFragmentSeparator')
44
+
45
+ const parentColorScheme = inject<ColorScheme>('colorScheme', 'none')
46
+
47
+ /**
48
+ * The line takes the colour of the chosen scheme. "Light" resolves to the same
49
+ * `gray-100` as the default, because that is the design system's border colour.
50
+ */
51
+ const lineColorClass = computed(() => {
52
+ switch (options.value.colorScheme) {
53
+ case 'dark':
54
+ return themedClass('lineColorDarkClass', 'border-gray-950')
55
+ case 'accent':
56
+ return themedClass('lineColorAccentClass', 'border-primary-600')
57
+ default:
58
+ return themedClass('lineColorClass', 'border-gray-100')
59
+ }
60
+ })
61
+
62
+ // The global options carry defaults, but a fragment placed before this
63
+ // component existed has no stored options at all — fall back to the same
64
+ // values the option definitions declare.
65
+ const spacing = computed(() => ({
66
+ top: options.value.spacingTop ?? 'small',
67
+ bottom: options.value.spacingBottom ?? 'small',
68
+ }))
69
+
70
+ /**
71
+ * "None" would leave the block one pixel high, which makes it impossible to
72
+ * grab in the editor — half of the small spacing instead. The classes are
73
+ * merged after the container's own, so they win.
74
+ */
75
+ const minSpacingClassList = computed(() =>
76
+ [
77
+ spacing.value.top === 'none'
78
+ ? themedClass('spacingTopNoneClass', 'pt-8')
79
+ : '',
80
+ spacing.value.bottom === 'none'
81
+ ? themedClass('spacingBottomNoneClass', 'pb-8')
82
+ : '',
83
+ ]
84
+ .filter(Boolean)
85
+ .join(' '),
86
+ )
87
+ </script>
@@ -3,7 +3,6 @@
3
3
  </template>
4
4
 
5
5
  <script lang="ts" setup>
6
-
7
6
  import type { ParagraphIcmsLayoutGalleryFragment } from '#graphql-operations'
8
7
 
9
8
  const props = defineProps<ParagraphIcmsLayoutGalleryFragment>()
@@ -14,8 +13,7 @@ const blokkliData = defineBlokkli({
14
13
  'spacingTop',
15
14
  'spacingBottom',
16
15
  'alignHorizontalFull',
17
- 'colorScheme'
16
+ 'colorScheme',
18
17
  ],
19
18
  })
20
-
21
19
  </script>
@@ -10,5 +10,4 @@ const props = defineProps<ParagraphIcmsIconTextElementFragment>()
10
10
  const blokkliData = defineBlokkli({
11
11
  bundle: 'icms_icon_text_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>
@@ -0,0 +1,9 @@
1
+ <svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect x="16" y="6" width="12" height="2" rx="0.5" fill="currentColor"/>
3
+ <rect x="16" y="18" width="12" height="2" rx="0.5" fill="currentColor"/>
4
+ <rect x="16" y="10" width="6" height="2" rx="0.5" fill="currentColor"/>
5
+ <rect x="16" y="22" width="6" height="2" rx="0.5" fill="currentColor"/>
6
+ <path d="M4.5 3H10.5C10.7761 3 11 3.22386 11 3.5V4H5.5C4.67157 4 4 4.67157 4 5.5V11H3.5C3.22386 11 3 10.7761 3 10.5V4.5C3 3.67157 3.67157 3 4.5 3Z" fill="currentColor"/>
7
+ <path d="M6.5 5H12.5C12.7761 5 13 5.22386 13 5.5V6H7.5C6.67157 6 6 6.67157 6 7.5V13H5.5C5.22386 13 5 12.7761 5 12.5V6.5C5 5.67157 5.67157 5 6.5 5Z" fill="currentColor"/>
8
+ <path d="M8.5 7H13.5C13.7761 7 14 7.22386 14 7.5V14.5C14 14.7761 13.7761 15 13.5 15H8.5C7.67157 15 7 14.3284 7 13.5V8.5C7 7.67157 7.67157 7 8.5 7ZM8.5 12C8.22386 12 8 12.2239 8 12.5C8 12.7761 8.22386 13 8.5 13H12.5C12.7761 13 13 12.7761 13 12.5C13 12.2239 12.7761 12 12.5 12H8.5ZM8.5 10C8.22386 10 8 10.2239 8 10.5C8 10.7761 8.22386 11 8.5 11H12.5C12.7761 11 13 10.7761 13 10.5C13 10.2239 12.7761 10 12.5 10H8.5Z" fill="currentColor"/>
9
+ </svg>
@@ -0,0 +1,24 @@
1
+ <template>
2
+ <div>
3
+ <ParagraphListing v-bind="props" :blokkliData="blokkliData" />
4
+ </div>
5
+ </template>
6
+
7
+ <script lang="ts" setup>
8
+ import type { ParagraphIcmsLayoutListingFragment } from '#graphql-operations'
9
+
10
+ const props = defineProps<ParagraphIcmsLayoutListingFragment>()
11
+
12
+ const blokkliData = defineBlokkli({
13
+ bundle: 'icms_layout_listing',
14
+ globalOptions: [
15
+ 'teaserVariant',
16
+ 'spacingTop',
17
+ 'spacingBottom',
18
+ 'alignHorizontalFull',
19
+ 'slider',
20
+ 'sliderColumns',
21
+ 'colorScheme',
22
+ ],
23
+ })
24
+ </script>
@@ -10,5 +10,4 @@ const props = defineProps<ParagraphIcmsLogoElementFragment>()
10
10
  const blokkliData = defineBlokkli({
11
11
  bundle: 'icms_logo_element',
12
12
  })
13
-
14
13
  </script>
@@ -16,5 +16,4 @@ const blokkliData = defineBlokkli({
16
16
  'colorScheme',
17
17
  ],
18
18
  })
19
-
20
19
  </script>
@@ -10,5 +10,4 @@ const props = defineProps<ParagraphIcmsTeamElementFragment>()
10
10
  const blokkliData = defineBlokkli({
11
11
  bundle: 'icms_team_element',
12
12
  })
13
-
14
13
  </script>
@@ -16,5 +16,4 @@ const blokkliData = defineBlokkli({
16
16
  'colorScheme',
17
17
  ],
18
18
  })
19
-
20
19
  </script>