@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.
- package/README.md +86 -0
- package/__tests__/anchors.spec.ts +43 -0
- package/__tests__/contract/blokkliComponents.spec.ts +59 -0
- package/__tests__/contract/componentFragments.spec.ts +24 -0
- package/__tests__/contract/languages.spec.ts +63 -0
- package/__tests__/contract/project.ts +144 -0
- package/__tests__/hreflang.spec.ts +39 -0
- package/{tests → __tests__}/pages.spec.ts +14 -5
- package/app.config.ts +15 -0
- package/app.vue +3 -21
- package/assets/css/backend.css +56 -0
- package/assets/css/base/base.css +2 -2
- package/assets/css/base/typography.css +2 -2
- package/assets/css/ckeditor.css +3 -4
- package/assets/css/components/decorations.css +1 -1
- package/assets/css/components/editor.css +1 -1
- package/assets/css/components/grid-container.css +19 -19
- package/assets/css/fe-fixes.css +6 -12
- package/assets/css/theme/colors/color-primary.css +33 -11
- package/assets/css/theme/colors/color-secondary.css +33 -11
- package/assets/css/utilities/utilities.css +1 -1
- package/assets/symbols/anchor.svg +3 -0
- package/blokkli.editAdapter.ts +56 -0
- package/bundles.ts +6 -6
- package/cache/__tests__/pageCache.spec.ts +540 -0
- package/cache/cacheTagRegistry.ts +124 -0
- package/cache/enabledForRequest.ts +21 -0
- package/cache/multiCache.serverOptions.ts +58 -0
- package/cache/pageCache.ts +360 -0
- package/components/Blokkli/Anchor/icon.svg +3 -0
- package/components/Blokkli/Anchor/index.vue +13 -0
- package/components/Blokkli/AnchorNavigation/icon.svg +8 -0
- package/components/Blokkli/AnchorNavigation/index.vue +9 -0
- package/components/Blokkli/Card/Element/index.vue +0 -1
- package/components/Blokkli/Collapsible/Element/index.vue +5 -0
- package/components/Blokkli/FAQ/Element/index.vue +0 -1
- package/components/Blokkli/Fragment/Separator/icon.svg +4 -0
- package/components/Blokkli/Fragment/Separator/index.vue +87 -0
- package/components/Blokkli/Gallery/index.vue +1 -3
- package/components/Blokkli/IconText/Element/index.vue +0 -1
- package/components/Blokkli/Introduction/index.vue +0 -1
- package/components/Blokkli/Listing/icon.svg +9 -0
- package/components/Blokkli/Listing/index.vue +24 -0
- package/components/Blokkli/Logo/Element/index.vue +0 -1
- package/components/Blokkli/Table/index.vue +0 -1
- package/components/Blokkli/Team/Element/index.vue +0 -1
- package/components/Blokkli/Team/Layout/index.vue +0 -1
- package/components/Blokkli/Testimonial/Element/index.vue +0 -1
- package/components/Blokkli/Text/index.vue +0 -1
- package/components/DesignSystem/Atoms/Button.vue +3 -0
- package/components/DesignSystem/Atoms/GridRow.vue +3 -1
- package/components/DesignSystem/Molecules/ButtonGroup.vue +5 -4
- package/components/DesignSystem/Molecules/IconText.vue +6 -6
- package/components/DesignSystem/Molecules/MediaLightbox.vue +8 -8
- package/components/DesignSystem/Molecules/MediaSlider.vue +9 -11
- package/components/DesignSystem/Molecules/Partnership.vue +7 -4
- package/components/DesignSystem/Molecules/Table.vue +35 -19
- package/components/DesignSystem/Molecules/TitleComposition.vue +46 -15
- package/components/DesignSystem/Molecules/iFrame.vue +2 -2
- package/components/DesignSystem/Organisms/CallToAction.vue +8 -7
- package/components/DesignSystem/Organisms/CallToActionFeature.vue +8 -7
- package/components/DesignSystem/Organisms/CallToActionFullwidth.vue +9 -8
- package/components/DesignSystem/Organisms/ContentBlock.vue +26 -16
- package/components/DesignSystem/Organisms/FacetedListing.vue +38 -17
- package/components/DesignSystem/Organisms/FactCard.vue +6 -7
- package/components/DesignSystem/Organisms/FeatureCard.vue +14 -14
- package/components/DesignSystem/Organisms/FlipTeaser.vue +19 -19
- package/components/DesignSystem/Organisms/HeaderHero.vue +11 -11
- package/components/DesignSystem/Organisms/HeaderTextImage.vue +10 -10
- package/components/DesignSystem/Organisms/LinkList.vue +20 -12
- package/components/DesignSystem/Organisms/LogoElement.vue +7 -4
- package/components/DesignSystem/Organisms/Map.vue +1 -1
- package/components/DesignSystem/Organisms/SearchResults.vue +15 -6
- package/components/DesignSystem/Organisms/SpotlightCard.vue +18 -18
- package/components/DesignSystem/Organisms/TeamElement.vue +19 -18
- package/components/DesignSystem/Organisms/TeaserCard.vue +16 -17
- package/components/DesignSystem/Organisms/TeaserSimple.vue +4 -31
- package/components/DesignSystem/Organisms/Testimonial.vue +13 -13
- package/components/DesignSystem/Organisms/TextCard.vue +10 -10
- package/components/DesignSystem/Organisms/TimelineElement.vue +20 -19
- package/components/DesignSystem/Organisms/TwoColumnTextElement.vue +5 -5
- package/components/DesignSystem/README.md +59 -40
- package/components/Drupal/Breadcrumb/index.vue +25 -12
- package/components/Drupal/Messages/index.vue +9 -4
- package/components/Drupal/Webform/index.vue +271 -51
- package/components/Drupal/Webform/mutation.graphql +17 -1
- package/components/Form/HiddenInput.vue +1 -4
- package/components/Form/SelectInput.vue +1 -1
- package/components/Form/SubmitButton.vue +2 -2
- package/components/Form/TextAreaInput.vue +3 -3
- package/components/Form/TextEmailInput.vue +1 -1
- package/components/Form/TextInput.vue +1 -1
- package/components/Form/TextPhoneInput.vue +1 -1
- package/components/Form/WebformMarkup.vue +1 -1
- package/components/Form/composables/useFormDefaultValue.ts +9 -13
- package/components/Form/composables/useFormElement.ts +5 -3
- package/components/Form/utils/types.ts +3 -3
- package/components/Media/Image/fragment.graphql +4 -0
- package/components/Media/Image/index.vue +27 -17
- package/components/Media/RemoteVideo/index.vue +3 -3
- package/components/Media/Video/index.vue +1 -1
- package/components/Node/Icms/Page/SearchResult/fragment.graphql +1 -1
- package/components/Node/Icms/Page/SearchResult/index.vue +2 -2
- package/components/Node/Icms/Page/Teaser/index.vue +1 -1
- package/components/Node/Icms/Page/index.vue +8 -2
- package/components/Paragraph/Anchor/fragment.graphql +4 -0
- package/components/Paragraph/Anchor/index.vue +48 -0
- package/components/Paragraph/AnchorNavigation/fragment.graphql +3 -0
- package/components/Paragraph/AnchorNavigation/index.vue +154 -0
- package/components/Paragraph/BlokkliFragment/fragment.graphql +5 -0
- package/components/Paragraph/Button/Element/fragment.graphql +1 -1
- package/components/Paragraph/Button/Element/index.vue +13 -8
- package/components/Paragraph/Card/Element/index.vue +1 -1
- package/components/Paragraph/Card/Layout/index.vue +9 -9
- package/components/Paragraph/Collapsible/Element/index.vue +2 -2
- package/components/Paragraph/Collapsible/Layout/index.vue +19 -18
- package/components/Paragraph/Contact/index.vue +17 -17
- package/components/Paragraph/Downloads/fragment.graphql +1 -1
- package/components/Paragraph/Downloads/index.vue +13 -13
- package/components/Paragraph/FAQ/Element/index.vue +4 -3
- package/components/Paragraph/FAQ/Layout/index.vue +42 -21
- package/components/Paragraph/FromLibrary/fragment.graphql +17 -0
- package/components/Paragraph/Gallery/index.vue +14 -14
- package/components/Paragraph/HeroImage/index.vue +26 -19
- package/components/Paragraph/IconText/Element/index.vue +4 -4
- package/components/Paragraph/IconText/Layout/index.vue +12 -12
- package/components/Paragraph/Introduction/index.vue +17 -17
- package/components/Paragraph/LinkLists/Element/fragment.graphql +1 -1
- package/components/Paragraph/LinkLists/Element/index.vue +5 -5
- package/components/Paragraph/LinkLists/Layout/index.vue +9 -9
- package/components/Paragraph/Listing/fragment.graphql +14 -0
- package/components/Paragraph/Listing/index.vue +91 -0
- package/components/Paragraph/Logo/Element/fragment.graphql +1 -1
- package/components/Paragraph/Logo/Element/index.vue +7 -4
- package/components/Paragraph/Media/index.vue +23 -21
- package/components/Paragraph/Partnerships/Element/fragment.graphql +1 -1
- package/components/Paragraph/Partnerships/Element/index.vue +10 -7
- package/components/Paragraph/Partnerships/Layout/index.vue +13 -13
- package/components/Paragraph/SingleTestimonial/index.vue +13 -13
- package/components/Paragraph/Table/index.vue +12 -12
- package/components/Paragraph/Team/Element/index.vue +16 -14
- package/components/Paragraph/Team/Layout/index.vue +9 -9
- package/components/Paragraph/Teasers/index.vue +14 -17
- package/components/Paragraph/Testimonial/Element/index.vue +19 -18
- package/components/Paragraph/Testimonial/Layout/index.vue +24 -18
- package/components/Paragraph/Text/fragment.graphql +1 -1
- package/components/Paragraph/Text/index.vue +8 -8
- package/components/Paragraph/TextMedia/index.vue +22 -22
- package/components/Paragraph/Timeline/Element/index.vue +19 -19
- package/components/Paragraph/Timeline/Layout/index.vue +51 -20
- package/components/Paragraph/TwoColumnText/Element/index.vue +13 -13
- package/components/Paragraph/TwoColumnText/Layout/index.vue +1 -1
- package/components/Paragraph/iFrame/index.vue +8 -8
- package/components/Search/Facets/FacetDropdownUI.vue +25 -31
- package/components/Search/Facets/FacetedSearchUI.vue +72 -48
- package/components/Search/Facets/fragment.graphql +20 -15
- package/components/Section/Footer/index.vue +55 -32
- package/components/Section/Header/Buttons.vue +4 -5
- package/components/Section/Header/DesktopMainMenu.vue +5 -4
- package/components/Section/Header/DesktopMainMenuDropdown.vue +5 -5
- package/components/Section/Header/LanguageDropdown.vue +2 -3
- package/components/Section/Header/Logo.vue +3 -3
- package/components/Section/Header/MainButtons.vue +4 -5
- package/components/Section/Header/MetaButtons.vue +5 -6
- package/components/Section/Header/MetaMenu.vue +1 -1
- package/components/Section/Header/MobileMainMenu.vue +1 -1
- package/components/Section/Header/MobileMainMenuItem.vue +2 -2
- package/components/Section/Header/index.vue +6 -13
- package/composables/__tests__/useImageSources.spec.ts +91 -0
- package/composables/breadcrumb.ts +12 -7
- package/composables/useAccordionScroll.ts +12 -4
- package/composables/useAllowedFragments.ts +14 -0
- package/composables/useAnchors.ts +48 -0
- package/composables/useChildTitleWeight.ts +1 -1
- package/composables/useCurrentLangcodeEnum.ts +1 -1
- package/composables/useCurrentNodeId.ts +8 -0
- package/composables/useDrupalUrl.ts +15 -0
- package/composables/useEnrichFacetLabels.ts +8 -6
- package/composables/useEntityRegistry.ts +3 -3
- package/composables/useGlobalConfig.ts +1 -2
- package/composables/useGraphqlCacheability.ts +12 -1
- package/composables/useHreflangLinks.ts +54 -0
- package/composables/useImageSources.ts +70 -28
- package/composables/useInitData.ts +1 -2
- package/composables/useIsHomepage.ts +1 -1
- package/composables/useLinkUrl.ts +30 -0
- package/composables/useMediaLightbox.ts +62 -50
- package/composables/useMenu.ts +1 -3
- package/composables/useMessageBus.ts +14 -7
- package/composables/useMobileMenuOverlayBus.ts +1 -1
- package/composables/useNavigationOffset.ts +49 -6
- package/composables/usePageCache.ts +33 -0
- package/composables/useSiteShell.ts +11 -0
- package/composables/useTheme.ts +17 -13
- package/config/__tests__/stubs/nuxt-multi-cache-config.ts +2 -0
- package/config/blokkli.ts +7 -13
- package/config/easyTexts.ts +1 -1
- package/config/graphqlMiddleware.ts +11 -4
- package/config/index.ts +5 -2
- package/config/languageNegotiation.ts +1 -1
- package/config/multiCache.ts +6 -1
- package/config/postcss-drupal/postcss.config.cjs +2 -2
- package/config/runtimeConfig.ts +7 -2
- package/config/svgIconSprite.ts +1 -2
- package/config/vuepal.ts +16 -18
- package/error.vue +1 -22
- package/layouts/default.vue +2 -6
- package/layouts/error.vue +7 -15
- package/modules/blokkliFragments.ts +76 -0
- package/modules/dynamicEntityComponentResolver.ts +47 -47
- package/modules/dynamicParagraphResolver.ts +43 -8
- package/modules/pageCache.ts +106 -0
- package/nuxt.config.ts +62 -33
- package/package.json +23 -22
- package/pages/[...slug]/index.vue +25 -4
- package/pages/index.vue +3 -3
- package/pages/node/[nid]/index.vue +2 -3
- package/patches/@blokkli+editor+2.0.0-alpha.13.patch +15 -1
- package/plugins/currentNode.ts +17 -0
- package/plugins/graphqlMiddleware.ts +10 -2
- package/plugins/initData.ts +15 -7
- package/plugins/registerNodeIcmsPageComponents.ts +6 -2
- package/plugins/siteShell.client.ts +94 -0
- package/queries/facetTermLabels.graphql +1 -5
- package/queries/fragment.linkUrl.graphql +12 -0
- package/queries/query.contentSearch.graphql +17 -2
- package/queries/query.initData.graphql +1 -1
- package/scripts/create-shade.ts +13 -4
- package/scripts/update-icms.ts +3 -1
- package/server/__tests__/graphqlMiddleware.serverOptions.spec.ts +2 -2
- package/server/__tests__/turnstile.spec.ts +119 -0
- package/server/api/icms/blokkli/last-changed.get.ts +24 -0
- package/server/api/icon/[...params].ts +3 -9
- package/server/graphqlMiddleware.serverOptions.ts +9 -8
- package/server/helpers/index.ts +0 -1
- package/server/middleware/turnstile.ts +51 -0
- package/server/plugins/frameAncestors.ts +20 -0
- package/server/plugins/pageCache.ts +59 -0
- package/server/utils/__tests__/accessDeniedHandover.spec.ts +49 -0
- package/server/utils/__tests__/backendRequestHeaders.spec.ts +115 -0
- package/server/utils/accessDeniedHandover.ts +48 -0
- package/server/utils/backendRequestHeaders.ts +51 -0
- package/server/utils/cacheability.ts +4 -4
- package/shared/__tests__/blokkliState.spec.ts +54 -0
- package/shared/__tests__/csp.spec.ts +53 -0
- package/shared/__tests__/drupalUrl.spec.ts +50 -0
- package/shared/blokkliState.ts +40 -0
- package/shared/csp.ts +47 -0
- package/shared/drupalUrl.ts +30 -0
- package/tailwind.config.ts +0 -5
- package/types/breadcrumb.ts +22 -0
- package/types/global.ts +15 -14
- package/types/langcode.ts +16 -0
- package/types/layerConfig.ts +84 -0
- package/utils/pageCache.ts +95 -0
- package/utils/turnstile.ts +35 -0
- package/composables/useWebformEnrichment.ts +0 -68
- package/server/__tests__/graphqlMiddleware.ts +0 -41
- package/server/multiCache.serverOptions.ts +0 -32
- 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>
|
|
@@ -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
|
})
|
|
@@ -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>
|
|
@@ -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>
|