@iqual/nuxt-icms 1.6.0-rc.5 → 1.6.2-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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__/multiCache.spec.ts +67 -0
- package/config/__tests__/stubs/nuxt-multi-cache-config.ts +8 -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 +7 -2
- 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 +20 -19
- 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 +31 -7
- package/server/__tests__/graphqlMiddleware.serverOptions.spec.ts +170 -0
- 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 +35 -18
- 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
package/README.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# @iqual/nuxt-icms
|
|
2
|
+
|
|
3
|
+
The Nuxt layer of ICMS: design system, blökkli components and the GraphQL
|
|
4
|
+
integration with the Drupal backend. Client projects get it through
|
|
5
|
+
`@iqual/create-nuxt-icms`.
|
|
6
|
+
|
|
7
|
+
## Page cache
|
|
8
|
+
|
|
9
|
+
Anonymous HTML is cached in the memory of the Nuxt process (nuxt-multi-cache's
|
|
10
|
+
route cache) and purged by Drupal cache tags through the `nuxt_multi_cache`
|
|
11
|
+
purger, with a 5 s batching delay. Code: `cache/`, `utils/pageCache.ts`,
|
|
12
|
+
`modules/pageCache.ts`, `server/plugins/pageCache.ts`.
|
|
13
|
+
|
|
14
|
+
- **Stored:** pages that opt in with `usePageCache()` (the catch-all page and
|
|
15
|
+
the homepage do), rendered with status 200, from cacheable GraphQL responses
|
|
16
|
+
only, without setting a cookie. Requests with a Drupal session
|
|
17
|
+
(`SSESS*`/`SESS*`) and blökkli editor/preview requests bypass it.
|
|
18
|
+
- **Never stored:** redirects, 4xx and 5xx. There is no grace: a purged page is
|
|
19
|
+
re-rendered on the next request, or fails if Drupal is down.
|
|
20
|
+
- **Key:** scheme, host, path and the sorted query, without cookies and
|
|
21
|
+
analytics/click IDs (`utm_*`, `gclid`, `fbclid`, …).
|
|
22
|
+
- **Budget:** LRU by bytes, pages stored gzipped. `routeMaxSize: 'auto'` uses 3%
|
|
23
|
+
of the container memory (8–64 MiB; 24 MiB if unknown; off in `nuxi dev`,
|
|
24
|
+
`NUXT_PAGE_CACHE_ROUTE_MAX_SIZE=auto` turns it on). The data cache has 8 MiB.
|
|
25
|
+
Under heavy churn the process grows by several times the budget (8 MiB: +40–50
|
|
26
|
+
MB, 24 MiB: +60–90 MB RSS).
|
|
27
|
+
- **Headers:** cacheable anonymous responses get
|
|
28
|
+
`Cache-Control: public, max-age=300` without `s-maxage`/`Cache-Tag`; sessions
|
|
29
|
+
and 5xx get `private, no-store`. `X-Icms-Page-Cache: HIT|MISS` shows the
|
|
30
|
+
outcome.
|
|
31
|
+
- **`varnish` mode:** behind Varnish (`routing.cache: varnish`) the responses
|
|
32
|
+
keep `s-maxage` and `Cache-Tag` and the route cache is off.
|
|
33
|
+
`NUXT_PAGE_CACHE_MODE=varnish` is set by `frontend/.environment`; an
|
|
34
|
+
explicitly set variable wins.
|
|
35
|
+
|
|
36
|
+
Configure it in `frontend/nuxt.config.ts` or with the environment variables:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
runtimeConfig: {
|
|
40
|
+
pageCache: {
|
|
41
|
+
mode: 'varnish', // NUXT_PAGE_CACHE_MODE
|
|
42
|
+
browserMaxAge: 300, // NUXT_PAGE_CACHE_BROWSER_MAX_AGE
|
|
43
|
+
routeMaxSize: 32 * 1024 * 1024, // NUXT_PAGE_CACHE_ROUTE_MAX_SIZE, 'auto', 0 = off
|
|
44
|
+
routeCompression: true, // NUXT_PAGE_CACHE_ROUTE_COMPRESSION
|
|
45
|
+
routeMaxAge: 604_800, // NUXT_PAGE_CACHE_ROUTE_MAX_AGE (7 days)
|
|
46
|
+
dataMaxSize: 8 * 1024 * 1024, // NUXT_PAGE_CACHE_DATA_MAX_SIZE
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Projects need no `server/multiCache.serverOptions.ts`. One that has its own file
|
|
52
|
+
must extend the ICMS options, or the page cache stays off (the server logs
|
|
53
|
+
`[icms] Page cache disabled`):
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
// frontend/server/multiCache.serverOptions.ts
|
|
57
|
+
import defineIcmsOptions from '@iqual/nuxt-icms/cache/multiCache.serverOptions'
|
|
58
|
+
import { defineMultiCacheOptions } from 'nuxt-multi-cache/server-options'
|
|
59
|
+
|
|
60
|
+
export default defineMultiCacheOptions(() => {
|
|
61
|
+
const options = defineIcmsOptions()
|
|
62
|
+
return { ...options /* , project overrides */ }
|
|
63
|
+
})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Webform spam protection (Turnstile)
|
|
67
|
+
|
|
68
|
+
Webforms are submitted through GraphQL, which bypasses Drupal's Form API, so
|
|
69
|
+
Drupal spam modules (`honeypot`, `captcha`, `antibot`, Webform's honeypot) never
|
|
70
|
+
run on them. Use Cloudflare Turnstile instead: the widget renders above the
|
|
71
|
+
submit button, and a Nitro middleware verifies its token before the
|
|
72
|
+
`submitWebform` mutation reaches Drupal.
|
|
73
|
+
|
|
74
|
+
It is off until both keys are set on the **frontend** application (Drupal needs
|
|
75
|
+
neither):
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
NUXT_PUBLIC_TURNSTILE_SITE_KEY=0x4AAAAAAA… # public
|
|
79
|
+
NUXT_TURNSTILE_SECRET_KEY=0x4AAAAAAA… # sensitive
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Cloudflare's test keys for staging: `1x00000000000000000000AA` /
|
|
83
|
+
`1x0000000000000000000000000000000AA` always pass, `2x00000000000000000000AB` /
|
|
84
|
+
`2x0000000000000000000000000000000AA` always fail. The rejection message is the
|
|
85
|
+
text `webform.spam_protection_error`; the widget is themed via `turnstileClass`
|
|
86
|
+
of `DrupalWebform`.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { anchorId, resolveAnchorId } from '../composables/useAnchors'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The anchor block and the anchor navigation derive the same id from the same
|
|
6
|
+
* fields — one on the block that carries the id, one on the links pointing at
|
|
7
|
+
* it. A drift between the two leaves links pointing nowhere, so the
|
|
8
|
+
* derivation is pinned here.
|
|
9
|
+
*/
|
|
10
|
+
describe('anchorId', () => {
|
|
11
|
+
it('lowercases, strips accents and special characters, dashes the spaces', () => {
|
|
12
|
+
expect(anchorId('Öffnungszeiten & Kontakt')).toBe('offnungszeiten-kontakt')
|
|
13
|
+
expect(anchorId(' Über uns ')).toBe('uber-uns')
|
|
14
|
+
expect(anchorId('FAQ (2026)')).toBe('faq-2026')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('is empty when nothing usable is left', () => {
|
|
18
|
+
expect(anchorId('')).toBe('')
|
|
19
|
+
expect(anchorId('***')).toBe('')
|
|
20
|
+
})
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('resolveAnchorId', () => {
|
|
24
|
+
it('generates the id from the label when the anchor is empty', () => {
|
|
25
|
+
expect(resolveAnchorId('Unsere Standorte', null)).toBe('unsere-standorte')
|
|
26
|
+
expect(resolveAnchorId('Unsere Standorte', ' ')).toBe('unsere-standorte')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('prefers the anchor field and keeps its casing', () => {
|
|
30
|
+
expect(resolveAnchorId('Unsere Standorte', 'standorte')).toBe('standorte')
|
|
31
|
+
expect(resolveAnchorId('Unsere Standorte', 'Standorte')).toBe('Standorte')
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('still strips what is not valid in a fragment from the anchor field', () => {
|
|
35
|
+
expect(resolveAnchorId('Unsere Standorte', '#Standorte')).toBe('Standorte')
|
|
36
|
+
expect(resolveAnchorId('Kontakt', 'Büro Zürich')).toBe('Buro-Zurich')
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('handles missing values', () => {
|
|
40
|
+
expect(resolveAnchorId(undefined, undefined)).toBe('')
|
|
41
|
+
expect(resolveAnchorId(null, 'kontakt')).toBe('kontakt')
|
|
42
|
+
})
|
|
43
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
blokkliBundles,
|
|
4
|
+
hasSchema,
|
|
5
|
+
pascalCase,
|
|
6
|
+
readSchema,
|
|
7
|
+
resolveProject,
|
|
8
|
+
schemaTypeNames,
|
|
9
|
+
} from './project'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Guards the contract between the blökkli components and the Drupal schema.
|
|
13
|
+
*
|
|
14
|
+
* Every `defineBlokkli({ bundle })` component — in the base layer, the
|
|
15
|
+
* installed bundle layers and the project's own layers — renders a paragraph
|
|
16
|
+
* type; the editor lists it and the frontend resolves it through the
|
|
17
|
+
* `Paragraph<Bundle>` GraphQL type. A component whose paragraph type no longer
|
|
18
|
+
* exists in the schema is a dead block (editor error, unresolvable fragment);
|
|
19
|
+
* a layout paragraph type without a component renders nothing. Both sides are
|
|
20
|
+
* discovered, so project blocks and bundles are covered wherever they are
|
|
21
|
+
* installed.
|
|
22
|
+
*/
|
|
23
|
+
describe.skipIf(!hasSchema())(
|
|
24
|
+
'blökkli components and the GraphQL schema',
|
|
25
|
+
() => {
|
|
26
|
+
it('every blökkli component maps to a paragraph type in the schema', async () => {
|
|
27
|
+
const { layerDirs } = await resolveProject()
|
|
28
|
+
const types = schemaTypeNames(readSchema())
|
|
29
|
+
const bundles = blokkliBundles(layerDirs)
|
|
30
|
+
expect(bundles.size).toBeGreaterThan(0)
|
|
31
|
+
|
|
32
|
+
const missing = [...bundles]
|
|
33
|
+
.filter(([bundle]) => !types.has(`Paragraph${pascalCase(bundle)}`))
|
|
34
|
+
.map(([bundle, file]) => `${bundle} (${file})`)
|
|
35
|
+
expect(
|
|
36
|
+
missing,
|
|
37
|
+
'components without a paragraph type in schema.graphql',
|
|
38
|
+
).toEqual([])
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('every ICMS layout paragraph type in the schema has a blökkli component', async () => {
|
|
42
|
+
const { layerDirs } = await resolveProject()
|
|
43
|
+
const bundles = blokkliBundles(layerDirs)
|
|
44
|
+
const componentTypes = new Set(
|
|
45
|
+
[...bundles.keys()].map((bundle) => `Paragraph${pascalCase(bundle)}`),
|
|
46
|
+
)
|
|
47
|
+
// Layout paragraphs are always top-level blocks; element paragraphs may
|
|
48
|
+
// be nested-only or data-only (e.g. event occurrences), so only layouts
|
|
49
|
+
// are required to have a component.
|
|
50
|
+
const layoutTypes = [...schemaTypeNames(readSchema())].filter((type) =>
|
|
51
|
+
/^ParagraphIcmsLayout[A-Z0-9]/.test(type),
|
|
52
|
+
)
|
|
53
|
+
expect(layoutTypes.length).toBeGreaterThan(0)
|
|
54
|
+
|
|
55
|
+
const missing = layoutTypes.filter((type) => !componentTypes.has(type))
|
|
56
|
+
expect(missing, 'layout paragraph types without a component').toEqual([])
|
|
57
|
+
})
|
|
58
|
+
},
|
|
59
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { definedFragments, resolveProject } from './project'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Guards the fragment composition the dynamic paragraph resolver relies on.
|
|
6
|
+
*
|
|
7
|
+
* Every layer registers the fragments of its blökkli components in
|
|
8
|
+
* `icms.components.fragments` (and `wrapperFragments` for paragraphs that
|
|
9
|
+
* embed another one); `dynamicParagraphResolver` spreads them into the
|
|
10
|
+
* generated `paragraph` fragment. A registered name without a definition
|
|
11
|
+
* — a renamed fragment file, a bundle layer installed without its documents,
|
|
12
|
+
* a project layer that forgot the file — only fails at `nuxt prepare`, with a
|
|
13
|
+
* generic GraphQL error far from its cause.
|
|
14
|
+
*/
|
|
15
|
+
describe('icms.components.fragments', () => {
|
|
16
|
+
it('every registered fragment name is defined in a GraphQL document', async () => {
|
|
17
|
+
const { layerDirs, componentFragments } = await resolveProject()
|
|
18
|
+
expect(componentFragments.length).toBeGreaterThan(0)
|
|
19
|
+
|
|
20
|
+
const defined = definedFragments(layerDirs)
|
|
21
|
+
const missing = componentFragments.filter((name) => !defined.has(name))
|
|
22
|
+
expect(missing, 'registered fragments without a definition').toEqual([])
|
|
23
|
+
})
|
|
24
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { describe, it, expect } from 'vitest'
|
|
4
|
+
import {
|
|
5
|
+
hasSchema,
|
|
6
|
+
projectRoot,
|
|
7
|
+
readSchema,
|
|
8
|
+
resolveProject,
|
|
9
|
+
schemaEnumValues,
|
|
10
|
+
} from './project'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Guards the language contract between the frontend, Drupal and the routing.
|
|
14
|
+
*
|
|
15
|
+
* The frontend serves exactly `languageNegotiation.languages`, asking Drupal
|
|
16
|
+
* for each via `/{lang}/graphql`; Drupal exposes its languages as the
|
|
17
|
+
* `Langcode` enum; the nginx/VCL split (scaffolded from `routing.languages`
|
|
18
|
+
* in drupal/composer.json) decides which prefixes reach the frontend at all.
|
|
19
|
+
* A frontend language Drupal doesn't know renders empty; one the routing
|
|
20
|
+
* doesn't know is unreachable. Both regress silently on config changes.
|
|
21
|
+
*/
|
|
22
|
+
describe.skipIf(!hasSchema())('site languages', () => {
|
|
23
|
+
it('every frontend language exists in Drupal (Langcode enum)', async () => {
|
|
24
|
+
const { languages } = await resolveProject()
|
|
25
|
+
expect(languages.length).toBeGreaterThan(0)
|
|
26
|
+
|
|
27
|
+
const drupal = schemaEnumValues(readSchema(), 'Langcode')
|
|
28
|
+
expect(drupal.length).toBeGreaterThan(0)
|
|
29
|
+
|
|
30
|
+
const unknown = languages.filter(
|
|
31
|
+
(lang) => !drupal.includes(lang.toUpperCase()),
|
|
32
|
+
)
|
|
33
|
+
expect(unknown, 'frontend languages missing in the Drupal schema').toEqual(
|
|
34
|
+
[],
|
|
35
|
+
)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('every frontend language is routed', async () => {
|
|
39
|
+
const composerPath = resolve(projectRoot, '../drupal/composer.json')
|
|
40
|
+
const routing = existsSync(composerPath)
|
|
41
|
+
? (
|
|
42
|
+
JSON.parse(readFileSync(composerPath, 'utf8')) as {
|
|
43
|
+
extra?: {
|
|
44
|
+
'project-scaffold'?: { routing?: { languages?: string[] } }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
).extra?.['project-scaffold']?.routing?.languages
|
|
48
|
+
: undefined
|
|
49
|
+
if (!routing?.length) {
|
|
50
|
+
// Not scaffolded (yet): the routing falls back to the platform default.
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { languages } = await resolveProject()
|
|
55
|
+
// The first routing entry is the fallback target of the root redirect,
|
|
56
|
+
// not necessarily the frontend's default language — only membership is
|
|
57
|
+
// the contract.
|
|
58
|
+
const unrouted = languages.filter((lang) => !routing.includes(lang))
|
|
59
|
+
expect(unrouted, 'frontend languages missing in routing.languages').toEqual(
|
|
60
|
+
[],
|
|
61
|
+
)
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join, resolve } from 'node:path'
|
|
3
|
+
import { loadNuxtConfig } from '@nuxt/kit'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Reads the project the specs run in — the monorepo's frontend or a client's
|
|
7
|
+
* — from the working directory: its committed schema.graphql, the resolved
|
|
8
|
+
* Nuxt config with every layer (base, bundles, project layers) and the files
|
|
9
|
+
* of all of them. Nothing here assumes the starterkit's set of languages,
|
|
10
|
+
* bundles or components; the specs compare what the project declares with
|
|
11
|
+
* what its Drupal schema exposes.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export const projectRoot = process.cwd()
|
|
15
|
+
export const schemaPath = resolve(projectRoot, 'schema.graphql')
|
|
16
|
+
|
|
17
|
+
/** Skip guard: the committed schema is the contract these specs read. */
|
|
18
|
+
export const hasSchema = () => existsSync(schemaPath)
|
|
19
|
+
|
|
20
|
+
export function readSchema(): string {
|
|
21
|
+
return readFileSync(schemaPath, 'utf8')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** All object type names of the schema (`type Foo ...`). */
|
|
25
|
+
export function schemaTypeNames(schema: string): Set<string> {
|
|
26
|
+
return new Set(
|
|
27
|
+
[...schema.matchAll(/^type\s+([A-Za-z0-9_]+)/gm)].map((m) => m[1]!),
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The values of a schema enum, e.g. `Langcode` → `['DE', 'EN']`. */
|
|
32
|
+
export function schemaEnumValues(schema: string, name: string): string[] {
|
|
33
|
+
const match = schema.match(
|
|
34
|
+
new RegExp(`^enum\\s+${name}\\s*\\{([^}]*)\\}`, 'm'),
|
|
35
|
+
)
|
|
36
|
+
if (!match) return []
|
|
37
|
+
return match[1]!
|
|
38
|
+
.split('\n')
|
|
39
|
+
.map((line) => line.trim())
|
|
40
|
+
.filter((line) => /^[A-Z][A-Z0-9_]*$/.test(line))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* `icms_layout_text` → `IcmsLayoutText`, the way graphql_core_schema names
|
|
45
|
+
* the GraphQL type of a bundle (`ParagraphIcmsLayoutText`).
|
|
46
|
+
*/
|
|
47
|
+
export function pascalCase(machineName: string): string {
|
|
48
|
+
return machineName
|
|
49
|
+
.split('_')
|
|
50
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
51
|
+
.join('')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ResolvedProject {
|
|
55
|
+
/** Layer directories, base layer first, the project's own last. */
|
|
56
|
+
layerDirs: string[]
|
|
57
|
+
languages: string[]
|
|
58
|
+
componentFragments: string[]
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let resolved: Promise<ResolvedProject> | undefined
|
|
62
|
+
|
|
63
|
+
/** Resolves the Nuxt config (all layers merged) once per run. */
|
|
64
|
+
export function resolveProject(): Promise<ResolvedProject> {
|
|
65
|
+
resolved ??= loadNuxtConfig({ cwd: projectRoot }).then((config) => {
|
|
66
|
+
const layerDirs = [...(config._layers ?? [])]
|
|
67
|
+
.map((layer) => layer.cwd)
|
|
68
|
+
.filter((dir): dir is string => typeof dir === 'string')
|
|
69
|
+
.reverse()
|
|
70
|
+
const icms = (
|
|
71
|
+
config as {
|
|
72
|
+
icms?: {
|
|
73
|
+
components?: { fragments?: string[]; wrapperFragments?: string[] }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
).icms
|
|
77
|
+
return {
|
|
78
|
+
layerDirs,
|
|
79
|
+
languages: [
|
|
80
|
+
...((config.languageNegotiation as { languages?: string[] })
|
|
81
|
+
?.languages ?? []),
|
|
82
|
+
],
|
|
83
|
+
componentFragments: [
|
|
84
|
+
...new Set([
|
|
85
|
+
...(icms?.components?.fragments ?? []),
|
|
86
|
+
...(icms?.components?.wrapperFragments ?? []),
|
|
87
|
+
]),
|
|
88
|
+
],
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
return resolved
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Recursively lists files under a directory (skipping node_modules). */
|
|
95
|
+
export function listFiles(dir: string, extension: string): string[] {
|
|
96
|
+
if (!existsSync(dir)) return []
|
|
97
|
+
const files: string[] = []
|
|
98
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
99
|
+
if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue
|
|
100
|
+
const path = join(dir, entry.name)
|
|
101
|
+
if (entry.isDirectory()) files.push(...listFiles(path, extension))
|
|
102
|
+
else if (entry.name.endsWith(extension)) files.push(path)
|
|
103
|
+
}
|
|
104
|
+
return files
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Directories a layer keeps its source in: the layer root and, for the Nuxt 4
|
|
109
|
+
* app structure, its `app/` directory.
|
|
110
|
+
*/
|
|
111
|
+
export function layerSourceDirs(layerDir: string): string[] {
|
|
112
|
+
return [layerDir, join(layerDir, 'app')].filter((dir) => existsSync(dir))
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The paragraph bundles of all `defineBlokkli({ bundle })` components. */
|
|
116
|
+
export function blokkliBundles(layerDirs: string[]): Map<string, string> {
|
|
117
|
+
const bundles = new Map<string, string>()
|
|
118
|
+
for (const layerDir of layerDirs) {
|
|
119
|
+
for (const sourceDir of layerSourceDirs(layerDir)) {
|
|
120
|
+
for (const file of listFiles(join(sourceDir, 'components'), '.vue')) {
|
|
121
|
+
const match = readFileSync(file, 'utf8').match(
|
|
122
|
+
/defineBlokkli\(\s*\{[\s\S]*?\bbundle:\s*['"]([a-z0-9_]+)['"]/,
|
|
123
|
+
)
|
|
124
|
+
if (match) bundles.set(match[1]!, file)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return bundles
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Names of all fragments defined in the project's and layers' documents. */
|
|
132
|
+
export function definedFragments(layerDirs: string[]): Set<string> {
|
|
133
|
+
const names = new Set<string>()
|
|
134
|
+
for (const dir of new Set([projectRoot, ...layerDirs])) {
|
|
135
|
+
for (const file of listFiles(dir, '.graphql')) {
|
|
136
|
+
for (const match of readFileSync(file, 'utf8').matchAll(
|
|
137
|
+
/^\s*fragment\s+([A-Za-z0-9_]+)\s+on\b/gm,
|
|
138
|
+
)) {
|
|
139
|
+
names.add(match[1]!)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return names
|
|
144
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { buildHreflangLinks } from '../composables/useHreflangLinks'
|
|
3
|
+
|
|
4
|
+
const ORIGIN = 'https://www.example.com'
|
|
5
|
+
|
|
6
|
+
describe('buildHreflangLinks', () => {
|
|
7
|
+
it('lists every translation, including the current one', () => {
|
|
8
|
+
expect(
|
|
9
|
+
buildHreflangLinks(
|
|
10
|
+
[
|
|
11
|
+
{ code: 'de', path: '/de/ueber-uns' },
|
|
12
|
+
{ code: 'fr', path: '/fr/a-propos' },
|
|
13
|
+
],
|
|
14
|
+
ORIGIN,
|
|
15
|
+
),
|
|
16
|
+
).toEqual([
|
|
17
|
+
{ rel: 'alternate', hreflang: 'de', href: `${ORIGIN}/de/ueber-uns` },
|
|
18
|
+
{ rel: 'alternate', hreflang: 'fr', href: `${ORIGIN}/fr/a-propos` },
|
|
19
|
+
])
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('emits nothing for a page that exists in one language only', () => {
|
|
23
|
+
expect(
|
|
24
|
+
buildHreflangLinks([{ code: 'de', path: '/de/ueber-uns' }], ORIGIN),
|
|
25
|
+
).toEqual([])
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('emits nothing without a known origin, rather than relative hrefs', () => {
|
|
29
|
+
expect(
|
|
30
|
+
buildHreflangLinks(
|
|
31
|
+
[
|
|
32
|
+
{ code: 'de', path: '/de/ueber-uns' },
|
|
33
|
+
{ code: 'fr', path: '/fr/a-propos' },
|
|
34
|
+
],
|
|
35
|
+
'',
|
|
36
|
+
),
|
|
37
|
+
).toEqual([])
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -8,6 +8,7 @@ import { parse as parseSfc } from '@vue/compiler-sfc'
|
|
|
8
8
|
async function findVueFiles(
|
|
9
9
|
dir: string,
|
|
10
10
|
ignorePatterns: string[] = [],
|
|
11
|
+
rootDir: string = dir,
|
|
11
12
|
): Promise<string[]> {
|
|
12
13
|
const entries = await fsp.readdir(dir, { withFileTypes: true })
|
|
13
14
|
const files: string[] = []
|
|
@@ -15,13 +16,16 @@ async function findVueFiles(
|
|
|
15
16
|
for (const entry of entries) {
|
|
16
17
|
const fullPath = path.join(dir, entry.name)
|
|
17
18
|
|
|
18
|
-
// Skip if path matches any ignore pattern
|
|
19
|
-
|
|
19
|
+
// Skip if the path below the root matches any ignore pattern. Matching the
|
|
20
|
+
// absolute path would also match the checkout directory, which on CI is
|
|
21
|
+
// named after the repository — and that name contains "blokkli".
|
|
22
|
+
const relativePath = path.relative(rootDir, fullPath)
|
|
23
|
+
if (ignorePatterns.some((pattern) => relativePath.includes(pattern))) {
|
|
20
24
|
continue
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
if (entry.isDirectory()) {
|
|
24
|
-
const subFiles = await findVueFiles(fullPath, ignorePatterns)
|
|
28
|
+
const subFiles = await findVueFiles(fullPath, ignorePatterns, rootDir)
|
|
25
29
|
files.push(...subFiles)
|
|
26
30
|
} else if (entry.name.endsWith('.vue')) {
|
|
27
31
|
files.push(fullPath)
|
|
@@ -69,11 +73,10 @@ function functionIsCalledAtLeastOnce(
|
|
|
69
73
|
}
|
|
70
74
|
|
|
71
75
|
describe('The Nuxt page component', async () => {
|
|
72
|
-
const rootDir = path.resolve(__dirname, './../
|
|
76
|
+
const rootDir = path.resolve(__dirname, './../pages')
|
|
73
77
|
const ignorePatterns = ['blokkli']
|
|
74
78
|
|
|
75
79
|
const pageFilePaths = await findVueFiles(rootDir, ignorePatterns)
|
|
76
|
-
console.log(pageFilePaths)
|
|
77
80
|
|
|
78
81
|
const pageComponentFiles = await Promise.all(
|
|
79
82
|
pageFilePaths.map((filePath) => {
|
|
@@ -86,6 +89,12 @@ describe('The Nuxt page component', async () => {
|
|
|
86
89
|
}),
|
|
87
90
|
)
|
|
88
91
|
|
|
92
|
+
// Without this the suite would silently pass as empty if the discovery ever
|
|
93
|
+
// stops finding the page components.
|
|
94
|
+
it('is discovered by the test suite', () => {
|
|
95
|
+
expect(pageComponentFiles.length).toBeGreaterThan(0)
|
|
96
|
+
})
|
|
97
|
+
|
|
89
98
|
pageComponentFiles.forEach(({ filePath, content }) => {
|
|
90
99
|
it(`"${filePath}" should call useDrupalRouteQuery()`, () => {
|
|
91
100
|
expect(
|
package/app.config.ts
CHANGED
|
@@ -1,2 +1,17 @@
|
|
|
1
1
|
export default defineAppConfig({
|
|
2
|
+
ui: {
|
|
3
|
+
button: {
|
|
4
|
+
// The design system styles buttons entirely through the `ui.base` slot,
|
|
5
|
+
// so it opts out of @nuxt/ui's own colour and variant classes by
|
|
6
|
+
// selecting these empty ones.
|
|
7
|
+
variants: {
|
|
8
|
+
color: {
|
|
9
|
+
none: '',
|
|
10
|
+
},
|
|
11
|
+
variant: {
|
|
12
|
+
none: '',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
2
17
|
})
|
package/app.vue
CHANGED
|
@@ -1,19 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<UApp>
|
|
3
|
+
<NuxtLoadingIndicator color="var(--primary-500)" :height="3" />
|
|
3
4
|
<div class="bk-main-canvas relative">
|
|
4
|
-
<ClientOnly>
|
|
5
|
-
<div
|
|
6
|
-
v-if="drupalUser.accessToolbar && !isEditing"
|
|
7
|
-
class="hidden border-b border-b-gray-100 bg-white 2xl:block"
|
|
8
|
-
>
|
|
9
|
-
<VuepalAdminToolbar :key="language" />
|
|
10
|
-
<div class="flex">
|
|
11
|
-
<div class="mx-auto w-auto bg-white py-8 xl:min-w-[1174px]">
|
|
12
|
-
<VuepalLocalTasks />
|
|
13
|
-
</div>
|
|
14
|
-
</div>
|
|
15
|
-
</div>
|
|
16
|
-
</ClientOnly>
|
|
17
5
|
<NuxtLayout>
|
|
18
6
|
<NuxtPage />
|
|
19
7
|
</NuxtLayout>
|
|
@@ -22,16 +10,10 @@
|
|
|
22
10
|
</template>
|
|
23
11
|
|
|
24
12
|
<script setup lang="ts">
|
|
25
|
-
|
|
26
|
-
|
|
13
|
+
// The admin UI (toolbar, local tasks) is Drupal's: editors browse the
|
|
14
|
+
// frontend inside the backend's site shell (see docs/routing.md).
|
|
27
15
|
const language = useCurrentLanguage()
|
|
28
16
|
|
|
29
|
-
const isEditing = computed(
|
|
30
|
-
() =>
|
|
31
|
-
!!(route.query.blokkliEditing || route.query.blokkliPreview) &&
|
|
32
|
-
drupalUser.value.accessToolbar,
|
|
33
|
-
)
|
|
34
|
-
|
|
35
17
|
useHead({
|
|
36
18
|
htmlAttrs: {
|
|
37
19
|
lang: language.value,
|
package/assets/css/backend.css
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
text-transform: uppercase;
|
|
10
10
|
list-style-type: none !important;
|
|
11
11
|
margin: 0 !important;
|
|
12
|
+
|
|
12
13
|
a,
|
|
13
14
|
div {
|
|
14
15
|
display: block;
|
|
@@ -26,10 +27,12 @@
|
|
|
26
27
|
color: #aaa;
|
|
27
28
|
font-weight: 500;
|
|
28
29
|
}
|
|
30
|
+
|
|
29
31
|
&.is-source {
|
|
30
32
|
border-color: #a7b3ce;
|
|
31
33
|
}
|
|
32
34
|
}
|
|
35
|
+
|
|
33
36
|
a {
|
|
34
37
|
&:hover {
|
|
35
38
|
background: #dbe1ea;
|
|
@@ -46,9 +49,14 @@
|
|
|
46
49
|
position: relative;
|
|
47
50
|
display: inline-block;
|
|
48
51
|
margin-right: 0.25em;
|
|
52
|
+
|
|
49
53
|
&.is-published {
|
|
50
54
|
background: var(--gin-color-green);
|
|
51
55
|
}
|
|
56
|
+
|
|
57
|
+
&.is-draft {
|
|
58
|
+
background: var(--gin-color-warning);
|
|
59
|
+
}
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
table.permissions thead tr {
|
|
@@ -80,3 +88,51 @@ table.permissions thead tr {
|
|
|
80
88
|
bottom: 0;
|
|
81
89
|
z-index: 20000;
|
|
82
90
|
}
|
|
91
|
+
|
|
92
|
+
.meta-sidebar__trigger {
|
|
93
|
+
&.trigger {
|
|
94
|
+
cursor: pointer;
|
|
95
|
+
background: transparent;
|
|
96
|
+
display: block;
|
|
97
|
+
height: 32px;
|
|
98
|
+
width: 32px;
|
|
99
|
+
margin-inline: calc(var(--gin-spacing-xxs) * -1);
|
|
100
|
+
border-radius: var(--gin-border-m);
|
|
101
|
+
|
|
102
|
+
&:hover {
|
|
103
|
+
background-color: var(--gin-color-primary-light);
|
|
104
|
+
|
|
105
|
+
&::before {
|
|
106
|
+
background-color: var(--gin-color-primary);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
&::before {
|
|
111
|
+
display: inline-block;
|
|
112
|
+
width: 100%;
|
|
113
|
+
height: 100%;
|
|
114
|
+
content: '';
|
|
115
|
+
-webkit-mask-repeat: no-repeat;
|
|
116
|
+
mask-repeat: no-repeat;
|
|
117
|
+
-webkit-mask-position: center center;
|
|
118
|
+
mask-position: center center;
|
|
119
|
+
background-color: var(--gin-icon-color);
|
|
120
|
+
-webkit-mask-image: url('/themes/contrib/gin/dist/media/sprite.svg#sidebar-view');
|
|
121
|
+
mask-image: url('/themes/contrib/gin/dist/media/sprite.svg#sidebar-view');
|
|
122
|
+
-webkit-mask-size: var(--gin-icon-size-toolbar-secondary)
|
|
123
|
+
var(--gin-icon-size-toolbar-secondary);
|
|
124
|
+
mask-size: var(--gin-icon-size-toolbar-secondary)
|
|
125
|
+
var(--gin-icon-size-toolbar-secondary);
|
|
126
|
+
border-radius: 50%;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
&.is-active,
|
|
130
|
+
&.is-active:hover {
|
|
131
|
+
background-color: var(--gin-bg-item-hover);
|
|
132
|
+
|
|
133
|
+
&::before {
|
|
134
|
+
background-color: var(--gin-color-primary);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
package/assets/css/base/base.css
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
html {
|
|
2
|
-
@apply scroll-smooth
|
|
3
|
-
}
|
|
2
|
+
@apply scroll-smooth bg-white font-sans hyphens-auto text-black;
|
|
3
|
+
}
|