srcdev-nuxt-components 9.5.6 → 9.5.7
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/.claude/skills/component-compliance-checklist.md +6 -2
- package/.claude/skills/components/navigation-items.md +1 -0
- package/.claude/skills/components/opening-hours.md +12 -0
- package/.claude/skills/components/price-list.md +10 -1
- package/.claude/skills/components/responsive-header.md +25 -0
- package/.claude/skills/components/site-header.md +2 -0
- package/.claude/skills/storybook-add-story.md +14 -0
- package/.vscode/srcdev-component-responsive-header.code-snippets +3 -1
- package/.vscode/srcdev-component-site-header.code-snippets +3 -1
- package/app/components/02.molecules/opening-hours/CONSUMER-STYLING.md +11 -1
- package/app/components/02.molecules/opening-hours/OpeningHours.vue +71 -43
- package/app/components/02.molecules/opening-hours/stories/OpeningHours.stories.ts +138 -6
- package/app/components/02.molecules/opening-hours/tests/OpeningHours.spec.ts +53 -0
- package/app/components/02.molecules/opening-hours/tests/__snapshots__/OpeningHours.spec.ts.snap +1 -1
- package/app/components/02.molecules/opening-hours/tests/buildOpeningHoursSpecification.spec.ts +37 -1
- package/app/components/02.molecules/price-list/CONSUMER-STYLING.md +17 -5
- package/app/components/02.molecules/price-list/PriceList.vue +32 -9
- package/app/components/02.molecules/price-list/stories/PriceList.stories.ts +125 -29
- package/app/components/02.molecules/price-list/tests/PriceList.spec.ts +20 -0
- package/app/components/03.organisms/google-reviews/stories/GoogleReviews.stories.ts +9 -4
- package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +14 -0
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +23 -14
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +55 -27
- package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +149 -18
- package/app/components/03.organisms/responsive-header/tests/.nuxt/app.config.mjs +21 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/components.d.ts +66 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/imports.d.ts +30 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/manifest/meta/test.json +1 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/app.config.d.ts +35 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/build.d.ts +23 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/builder-env.d.ts +1 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/components.d.ts +71 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/imports.d.ts +386 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/middleware.d.ts +11 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/modules.d.ts +79 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-layouts.d.ts +17 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-nuxt.d.ts +64 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/plugins.d.ts +29 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/runtime-config.d.ts +32 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/shared-imports.d.ts +10 -0
- package/app/components/03.organisms/responsive-header/tests/.nuxt/types/vue-shim.d.ts +0 -0
- package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +16 -0
- package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +91 -6
- package/app/components/03.organisms/responsive-header/tests/__snapshots__/ResponsiveHeader.spec.ts.snap +9 -9
- package/app/components/03.organisms/responsive-header/tests/node_modules/.cache/nuxt/chrome-workspace.json +1 -0
- package/app/components/03.organisms/site-header/SiteHeader.vue +8 -0
- package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +1 -1
- package/app/utils/opening-hours.ts +23 -2
- package/package.json +1 -1
|
@@ -27,8 +27,7 @@
|
|
|
27
27
|
'is-active': isActiveNavItem(link),
|
|
28
28
|
}"
|
|
29
29
|
:style="{
|
|
30
|
-
'--_main-navigation-item-width':
|
|
31
|
-
mainNavigationState.clonedNavLinks?.[groupKey]?.[localIndex]?.config?.width + 'px',
|
|
30
|
+
'--_main-navigation-item-width': itemWidth(mainNavigationState.clonedNavLinks?.[groupKey]?.[localIndex]),
|
|
32
31
|
}"
|
|
33
32
|
:data-group-key="groupKey"
|
|
34
33
|
:data-local-index="localIndex"
|
|
@@ -48,8 +47,7 @@
|
|
|
48
47
|
'is-active': isActiveNavItem(link),
|
|
49
48
|
}"
|
|
50
49
|
:style="{
|
|
51
|
-
'--_main-navigation-item-width':
|
|
52
|
-
mainNavigationState.clonedNavLinks?.[groupKey]?.[localIndex]?.config?.width + 'px',
|
|
50
|
+
'--_main-navigation-item-width': itemWidth(mainNavigationState.clonedNavLinks?.[groupKey]?.[localIndex]),
|
|
53
51
|
}"
|
|
54
52
|
:data-group-key="groupKey"
|
|
55
53
|
:data-local-index="localIndex"
|
|
@@ -66,11 +64,11 @@
|
|
|
66
64
|
<template #summary>
|
|
67
65
|
<span
|
|
68
66
|
class="overflow-navigation-text"
|
|
69
|
-
:aria-label="
|
|
67
|
+
:aria-label="submenuLabel(link)"
|
|
70
68
|
role="menuitem"
|
|
71
69
|
:aria-haspopup="true"
|
|
72
70
|
>
|
|
73
|
-
{{ link.childLinksTitle }}
|
|
71
|
+
{{ link.childLinksTitle ?? link.name }}
|
|
74
72
|
</span>
|
|
75
73
|
</template>
|
|
76
74
|
<template #icon>
|
|
@@ -81,7 +79,7 @@
|
|
|
81
79
|
<ul class="overflow-navigation-sub-nav-list" @mouseleave="hoveredChildKey = null">
|
|
82
80
|
<li
|
|
83
81
|
v-for="(childLink, childIndex) in link.childLinks"
|
|
84
|
-
:key="
|
|
82
|
+
:key="childIndex"
|
|
85
83
|
class="overflow-navigation-sub-nav-item"
|
|
86
84
|
:class="{
|
|
87
85
|
'is-hovered': hoveredChildKey === `${String(groupKey)}-${localIndex}-${childIndex}`,
|
|
@@ -117,6 +115,8 @@ interface Props {
|
|
|
117
115
|
styleClassPassthrough?: string | string[];
|
|
118
116
|
/** aria-label on the overflow menu — override for localisation. */
|
|
119
117
|
ariaLabel?: string;
|
|
118
|
+
/** aria-label on each submenu summary; {title} is replaced with the item's title. */
|
|
119
|
+
submenuAriaLabel?: string;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -127,8 +127,12 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
127
127
|
panelVariant: "classic",
|
|
128
128
|
styleClassPassthrough: () => [],
|
|
129
129
|
ariaLabel: "Overflow navigation menu",
|
|
130
|
+
submenuAriaLabel: "{title} submenu",
|
|
130
131
|
});
|
|
131
132
|
|
|
133
|
+
const submenuLabel = (link: ResponsiveHeaderNavItem) =>
|
|
134
|
+
props.submenuAriaLabel.replace("{title}", link.childLinksTitle ?? link.name);
|
|
135
|
+
|
|
132
136
|
const panelComponent = computed(() => (props.panelVariant === "modern" ? ExpandingPanel : ExpandingPanelClassic));
|
|
133
137
|
|
|
134
138
|
const hoveredItemKey = ref<string | null>(null);
|
|
@@ -178,6 +182,9 @@ const widestNavLinkWidthInMainNavigationState = computed(() => {
|
|
|
178
182
|
}, 0);
|
|
179
183
|
});
|
|
180
184
|
|
|
185
|
+
const itemWidth = (link?: ResponsiveHeaderNavItem) =>
|
|
186
|
+
link?.config?.width === undefined ? undefined : `${link.config.width}px`;
|
|
187
|
+
|
|
181
188
|
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
182
189
|
|
|
183
190
|
// Performance: Use immediate flag and more efficient watching
|
|
@@ -199,6 +206,7 @@ watch(
|
|
|
199
206
|
on this element to avoid cascade conflicts with ancestor overrides.
|
|
200
207
|
|
|
201
208
|
--overflow-nav-padding-inline (default: 0.8rem)
|
|
209
|
+
--overflow-nav-max-inline-size (default: calc(100vw - 3.2rem))
|
|
202
210
|
--overflow-nav-items-gap (default: 0px)
|
|
203
211
|
--overflow-nav-items-padding-block (default: 0.8rem)
|
|
204
212
|
|
|
@@ -212,10 +220,15 @@ watch(
|
|
|
212
220
|
--overflow-nav-decorator-hovered-bg (default: oklch(100% 0 0 / 6%))
|
|
213
221
|
──────────────────────────────────────────────────────────────────────── */
|
|
214
222
|
|
|
223
|
+
--_max-inline-size: var(--overflow-nav-max-inline-size, calc(100vw - 3.2rem));
|
|
224
|
+
|
|
215
225
|
display: flex;
|
|
216
226
|
flex-direction: column;
|
|
217
227
|
gap: var(--overflow-nav-items-gap, 0px);
|
|
218
228
|
position: relative;
|
|
229
|
+
inline-size: max-content;
|
|
230
|
+
max-inline-size: var(--_max-inline-size);
|
|
231
|
+
overflow-wrap: anywhere;
|
|
219
232
|
|
|
220
233
|
.overflow-navigation-list {
|
|
221
234
|
display: none;
|
|
@@ -224,7 +237,7 @@ watch(
|
|
|
224
237
|
display: flex;
|
|
225
238
|
flex-direction: column;
|
|
226
239
|
gap: var(--overflow-nav-items-gap, 0px);
|
|
227
|
-
min-width: var(--_overflow-navigation-list-min-width,
|
|
240
|
+
min-width: min(var(--_overflow-navigation-list-min-width, 0px), var(--_max-inline-size));
|
|
228
241
|
}
|
|
229
242
|
|
|
230
243
|
.overflow-navigation-item {
|
|
@@ -262,9 +275,7 @@ watch(
|
|
|
262
275
|
border-bottom: 0.1rem solid var(--overflow-nav-link-border-color, #efefef75);
|
|
263
276
|
|
|
264
277
|
.label-wrapper {
|
|
265
|
-
|
|
266
|
-
white-space: nowrap;
|
|
267
|
-
}
|
|
278
|
+
min-inline-size: 0;
|
|
268
279
|
}
|
|
269
280
|
.icon-wrapper {
|
|
270
281
|
padding: 0;
|
|
@@ -325,9 +336,7 @@ watch(
|
|
|
325
336
|
border-bottom: 0.1rem solid var(--overflow-nav-link-border-color, #efefef75);
|
|
326
337
|
|
|
327
338
|
.label-wrapper {
|
|
328
|
-
|
|
329
|
-
white-space: nowrap;
|
|
330
|
-
}
|
|
339
|
+
min-inline-size: 0;
|
|
331
340
|
}
|
|
332
341
|
.icon-wrapper {
|
|
333
342
|
padding: 0;
|
|
@@ -32,8 +32,7 @@
|
|
|
32
32
|
'is-active': isActiveNavItem(link),
|
|
33
33
|
}"
|
|
34
34
|
:style="{
|
|
35
|
-
'--_main-navigation-item-width':
|
|
36
|
-
mainNavigationState.clonedNavLinks?.[groupKey]?.[localIndex]?.config?.width + 'px',
|
|
35
|
+
'--_main-navigation-item-width': itemWidth(mainNavigationState.clonedNavLinks?.[groupKey]?.[localIndex]),
|
|
37
36
|
}"
|
|
38
37
|
:data-group-key="groupKey"
|
|
39
38
|
:data-local-index="localIndex"
|
|
@@ -52,7 +51,7 @@
|
|
|
52
51
|
<details v-else ref="navigationDetails" class="main-navigation-details" name="navigation-group">
|
|
53
52
|
<summary
|
|
54
53
|
class="main-navigation-details-summary has-toggle-icon"
|
|
55
|
-
:aria-label="
|
|
54
|
+
:aria-label="submenuLabel(link)"
|
|
56
55
|
@mouseenter="handleSummaryHover($event)"
|
|
57
56
|
@focusin="handleSummaryHover($event)"
|
|
58
57
|
@click.prevent="handleSummaryAction($event)"
|
|
@@ -61,11 +60,15 @@
|
|
|
61
60
|
<Icon name="mdi:chevron-down" class="icon" :aria-hidden="true" />
|
|
62
61
|
<Icon v-if="link.iconName" :name="link.iconName" class="decorator-icon" aria-hidden="true" />
|
|
63
62
|
|
|
64
|
-
{{ link.childLinksTitle }}
|
|
63
|
+
{{ link.childLinksTitle ?? link.name }}
|
|
65
64
|
</summary>
|
|
66
65
|
<div class="main-navigation-sub-nav" role="menu" @mouseenter="handleSubNavHover">
|
|
67
66
|
<ul class="main-navigation-sub-nav-list">
|
|
68
|
-
<li
|
|
67
|
+
<li
|
|
68
|
+
v-for="(childLink, childIndex) in link.childLinks"
|
|
69
|
+
:key="childIndex"
|
|
70
|
+
class="main-navigation-sub-nav-item"
|
|
71
|
+
>
|
|
69
72
|
<NuxtLink :to="childLink.path" class="main-navigation-sub-nav-link" role="menuitem">
|
|
70
73
|
{{ childLink.name }}
|
|
71
74
|
</NuxtLink>
|
|
@@ -85,7 +88,7 @@
|
|
|
85
88
|
:class="overflowDetailsClass ? [overflowDetailsClass] : []"
|
|
86
89
|
name="overflow-group"
|
|
87
90
|
>
|
|
88
|
-
<summary class="overflow-details-summary has-toggle-icon">
|
|
91
|
+
<summary class="overflow-details-summary has-toggle-icon" :aria-label="overflowButtonLabel">
|
|
89
92
|
<Icon
|
|
90
93
|
:name="overflowDetailsSummaryIcons.more ?? 'gravity-ui:ellipsis'"
|
|
91
94
|
class="icon"
|
|
@@ -104,6 +107,7 @@
|
|
|
104
107
|
:main-navigation-state="mainNavigationState"
|
|
105
108
|
:panel-variant="panelVariant"
|
|
106
109
|
:aria-label="overflowMenuAriaLabel"
|
|
110
|
+
:submenu-aria-label="submenuAriaLabel"
|
|
107
111
|
/>
|
|
108
112
|
</div>
|
|
109
113
|
</details>
|
|
@@ -136,6 +140,10 @@ interface Props {
|
|
|
136
140
|
secondaryNavAriaLabel?: string;
|
|
137
141
|
/** aria-label on the overflow menu itself (forwarded to NavigationItems) — override for localisation. */
|
|
138
142
|
overflowMenuAriaLabel?: string;
|
|
143
|
+
/** Accessible name of the overflow/burger button, which shows only icons. */
|
|
144
|
+
overflowButtonLabel?: string;
|
|
145
|
+
/** aria-label on each dropdown summary; {title} is replaced with the item's title. Forwarded to NavigationItems. */
|
|
146
|
+
submenuAriaLabel?: string;
|
|
139
147
|
}
|
|
140
148
|
|
|
141
149
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -155,8 +163,13 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
155
163
|
mainNavAriaLabel: "Main navigation",
|
|
156
164
|
secondaryNavAriaLabel: "Secondary navigation",
|
|
157
165
|
overflowMenuAriaLabel: "Overflow navigation menu",
|
|
166
|
+
overflowButtonLabel: "More navigation",
|
|
167
|
+
submenuAriaLabel: "{title} submenu",
|
|
158
168
|
});
|
|
159
169
|
|
|
170
|
+
const submenuLabel = (link: ResponsiveHeaderNavItem) =>
|
|
171
|
+
props.submenuAriaLabel.replace("{title}", link.childLinksTitle ?? link.name);
|
|
172
|
+
|
|
160
173
|
const collapseNavigationBelowWidth = computed(
|
|
161
174
|
() => props.collapseBreakpoint !== null || props.collapseAtMainNavIntersection
|
|
162
175
|
);
|
|
@@ -173,6 +186,7 @@ const {
|
|
|
173
186
|
const slots = useSlots();
|
|
174
187
|
|
|
175
188
|
const navigationWrapperRef = useTemplateRef<HTMLDivElement>("navigationWrapper");
|
|
189
|
+
const mainNavRef = useTemplateRef<HTMLElement>("mainNav");
|
|
176
190
|
|
|
177
191
|
const closeAllNavigationDetails = () => {
|
|
178
192
|
navigationDetailsRefs.value?.forEach((element) => {
|
|
@@ -361,7 +375,10 @@ const mainNavigationState = ref<ResponsiveHeaderState>({
|
|
|
361
375
|
firstNav: false,
|
|
362
376
|
secondNav: false,
|
|
363
377
|
},
|
|
364
|
-
|
|
378
|
+
// Copied so the geometry pass's config writes never land in the consumer's own nav data
|
|
379
|
+
clonedNavLinks: Object.fromEntries(
|
|
380
|
+
Object.entries(props.responsiveNavLinks).map(([groupKey, group]) => [groupKey, group.map((link) => ({ ...link }))])
|
|
381
|
+
),
|
|
365
382
|
hasSecondNav: Object.keys(props.responsiveNavLinks).length > 1,
|
|
366
383
|
});
|
|
367
384
|
|
|
@@ -398,12 +415,13 @@ const navigationDetailsRefs = useTemplateRef<HTMLElement[]>("navigationDetails")
|
|
|
398
415
|
|
|
399
416
|
const overflowDetailsRef = useTemplateRef<HTMLDetailsElement>("overflowDetails");
|
|
400
417
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
418
|
+
// Item-based rather than reading navListVisibility.firstNav/secondNav, so any group key works and
|
|
419
|
+
// empty nav data never shows a burger with nothing in it
|
|
420
|
+
const showOverflowDetails = computed(() =>
|
|
421
|
+
Object.values(mainNavigationState.value.clonedNavLinks ?? {}).some((group) =>
|
|
422
|
+
group.some((link) => link.config?.visible === false)
|
|
423
|
+
)
|
|
424
|
+
);
|
|
407
425
|
|
|
408
426
|
const mainNavigationMarginBlockEnd = computed(() => {
|
|
409
427
|
return secondaryNavRects.value ? secondaryNavRects.value.width + props.gapBetweenFirstAndSecondNav : 0;
|
|
@@ -462,20 +480,18 @@ const determineNavigationItemVisibility = (rect: DOMRect) => {
|
|
|
462
480
|
return false;
|
|
463
481
|
}
|
|
464
482
|
|
|
465
|
-
//
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
);
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Default to visible
|
|
474
|
-
return true;
|
|
483
|
+
// An item fits if it ends within the main nav, whose margin-inline-end already reserves the
|
|
484
|
+
// secondary nav plus the gap. Measured live with the item so both read the same layout.
|
|
485
|
+
const mainNavRect = mainNavRef.value?.getBoundingClientRect();
|
|
486
|
+
if (!mainNavRect) return true;
|
|
487
|
+
return Math.floor(rect.right) <= Math.floor(mainNavRect.right);
|
|
475
488
|
};
|
|
476
489
|
|
|
477
490
|
const initMainNavigationState = () => {
|
|
478
|
-
if (!mainNavigationItemsRefs.value)
|
|
491
|
+
if (!mainNavigationItemsRefs.value?.length) {
|
|
492
|
+
isGeometryReady.value = true;
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
479
495
|
|
|
480
496
|
mainNavigationItemsRefs.value.forEach(async (item) => {
|
|
481
497
|
// await nextTick()
|
|
@@ -642,6 +658,9 @@ onMounted(async () => {
|
|
|
642
658
|
|
|
643
659
|
useResizeObserver(navigationWrapperRef, runGeometryPass);
|
|
644
660
|
|
|
661
|
+
const itemWidth = (link?: ResponsiveHeaderNavItem) =>
|
|
662
|
+
link?.config?.width === undefined ? undefined : `${link.config.width}px`;
|
|
663
|
+
|
|
645
664
|
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
646
665
|
|
|
647
666
|
watch(
|
|
@@ -690,6 +709,8 @@ watch(
|
|
|
690
709
|
--responsive-header-sub-nav-border (default: 1px solid #efefef75)
|
|
691
710
|
--responsive-header-sub-nav-border-radius (default: 8px)
|
|
692
711
|
--responsive-header-sub-nav-padding (default: 12px)
|
|
712
|
+
--responsive-header-sub-nav-max-inline-size (default: min(48rem, calc(100vw - 3.2rem)))
|
|
713
|
+
--responsive-header-sub-nav-max-block-size (default: 70vh)
|
|
693
714
|
|
|
694
715
|
--responsive-header-overflow-btn-bg (default: Canvas)
|
|
695
716
|
--responsive-header-overflow-btn-size (default: 20px)
|
|
@@ -702,12 +723,14 @@ watch(
|
|
|
702
723
|
--responsive-header-overflow-nav-border (default: 1px solid #ffffff90)
|
|
703
724
|
--responsive-header-overflow-nav-border-radius (default: 8px)
|
|
704
725
|
--responsive-header-overflow-nav-padding-block (default: 12px)
|
|
726
|
+
--responsive-header-overflow-nav-max-block-size (default: 70vh)
|
|
705
727
|
|
|
706
728
|
--responsive-nav-decorator-indicator-color (default: currentColor)
|
|
707
729
|
--responsive-nav-decorator-hovered-indicator-color (default: inherits --responsive-nav-decorator-indicator-color)
|
|
708
730
|
--responsive-nav-decorator-hovered-bg (default: oklch(100% 0 0 / 8%))
|
|
709
731
|
|
|
710
732
|
--responsive-header-link-font-size (default: inherit)
|
|
733
|
+
--responsive-header-main-nav-justify-content (default: space-between)
|
|
711
734
|
──────────────────────────────────────────────────────────────────────── */
|
|
712
735
|
|
|
713
736
|
/* `inherit` means a viewport-relative (vw/clamp) ancestor font-size silently
|
|
@@ -749,7 +772,7 @@ watch(
|
|
|
749
772
|
display: flex;
|
|
750
773
|
flex-wrap: nowrap;
|
|
751
774
|
flex-grow: 1;
|
|
752
|
-
justify-content: space-between;
|
|
775
|
+
justify-content: var(--responsive-header-main-nav-justify-content, space-between);
|
|
753
776
|
gap: 60px;
|
|
754
777
|
overflow-x: hidden;
|
|
755
778
|
margin-inline-end: v-bind(mainNavigationMarginBlockEndStr);
|
|
@@ -868,6 +891,9 @@ watch(
|
|
|
868
891
|
translate: 0 12px;
|
|
869
892
|
|
|
870
893
|
min-width: var(--_main-navigation-item-width);
|
|
894
|
+
max-inline-size: var(--responsive-header-sub-nav-max-inline-size, min(48rem, calc(100vw - 3.2rem)));
|
|
895
|
+
max-block-size: var(--responsive-header-sub-nav-max-block-size, 70vh);
|
|
896
|
+
overflow-y: auto;
|
|
871
897
|
|
|
872
898
|
.main-navigation-sub-nav-list {
|
|
873
899
|
display: grid;
|
|
@@ -883,7 +909,7 @@ watch(
|
|
|
883
909
|
|
|
884
910
|
.main-navigation-sub-nav-link {
|
|
885
911
|
display: block;
|
|
886
|
-
|
|
912
|
+
overflow-wrap: anywhere;
|
|
887
913
|
text-decoration: none;
|
|
888
914
|
color: var(--responsive-header-link-color, inherit);
|
|
889
915
|
}
|
|
@@ -1048,7 +1074,9 @@ watch(
|
|
|
1048
1074
|
padding-block: var(--responsive-header-overflow-nav-padding-block, 12px);
|
|
1049
1075
|
margin: 0;
|
|
1050
1076
|
z-index: 999;
|
|
1051
|
-
min-width:
|
|
1077
|
+
min-width: fit-content;
|
|
1078
|
+
max-block-size: var(--responsive-header-overflow-nav-max-block-size, 70vh);
|
|
1079
|
+
overflow-y: auto;
|
|
1052
1080
|
|
|
1053
1081
|
display: grid;
|
|
1054
1082
|
grid-auto-flow: row;
|
|
@@ -1,10 +1,50 @@
|
|
|
1
|
+
import { computed, ref } from "vue";
|
|
1
2
|
import ResponsiveHeader from "../ResponsiveHeader.vue";
|
|
3
|
+
import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
2
4
|
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
5
|
+
import type { MediaCanvas, ResponsiveHeaderProp } from "~/types/components";
|
|
3
6
|
|
|
4
|
-
|
|
7
|
+
type MainNavJustify = "space-between" | "flex-start" | "safe center" | "safe end";
|
|
8
|
+
|
|
9
|
+
// Story-only arg: drives a CSS token, not a ResponsiveHeader prop. See the argTypes entry.
|
|
10
|
+
type StoryArgs = InstanceType<typeof ResponsiveHeader>["$props"] & {
|
|
11
|
+
mainNavJustifyContent: MainNavJustify;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function useStorySetup(args: StoryArgs) {
|
|
15
|
+
const componentArgs = computed(() => {
|
|
16
|
+
const { mainNavJustifyContent: _mainNavJustifyContent, ...rest } = args;
|
|
17
|
+
return rest;
|
|
18
|
+
});
|
|
19
|
+
const tokenStyles = computed(() => ({
|
|
20
|
+
"--responsive-header-main-nav-justify-content": args.mainNavJustifyContent,
|
|
21
|
+
}));
|
|
22
|
+
return { componentArgs, tokenStyles };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const renderWith = (template: string) => (args: StoryArgs) => ({
|
|
26
|
+
components: { ResponsiveHeader },
|
|
27
|
+
setup() {
|
|
28
|
+
return useStorySetup(args);
|
|
29
|
+
},
|
|
30
|
+
template: `<div :style="tokenStyles">${template}</div>`,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const render = renderWith(`<ResponsiveHeader v-bind="componentArgs" />`);
|
|
34
|
+
|
|
35
|
+
const meta: Meta<StoryArgs> = {
|
|
5
36
|
title: "Organisms/Responsive Header",
|
|
6
37
|
component: ResponsiveHeader,
|
|
7
38
|
argTypes: {
|
|
39
|
+
mainNavJustifyContent: {
|
|
40
|
+
control: "select",
|
|
41
|
+
options: ["space-between", "flex-start", "safe center", "safe end"] satisfies MainNavJustify[],
|
|
42
|
+
description:
|
|
43
|
+
"**Story control, not a prop.** Sets the `--responsive-header-main-nav-justify-content` CSS token on a wrapper. " +
|
|
44
|
+
"To use it in an app, set the token in your own CSS, e.g. `.site-nav { --responsive-header-main-nav-justify-content: flex-start; }`. " +
|
|
45
|
+
"Use `safe end` / `safe center` rather than plain `end` / `center`, so items that don't fit aren't clipped off the start edge.",
|
|
46
|
+
table: { category: "CSS tokens (story only, set in your CSS)", defaultValue: { summary: "space-between" } },
|
|
47
|
+
},
|
|
8
48
|
responsiveNavLinks: {
|
|
9
49
|
control: "object",
|
|
10
50
|
description: "Nav groups keyed by name — each item is either a link (`path`) or a dropdown (`childLinks`)",
|
|
@@ -29,12 +69,47 @@ const meta: Meta<typeof ResponsiveHeader> = {
|
|
|
29
69
|
control: "boolean",
|
|
30
70
|
description: "Allow a dropdown to open on hover/focus, not just click",
|
|
31
71
|
},
|
|
72
|
+
mainNavAriaLabel: { control: "text", table: { category: "Labels", defaultValue: { summary: "Main navigation" } } },
|
|
73
|
+
secondaryNavAriaLabel: { control: "text", table: { category: "Labels", defaultValue: { summary: "Secondary navigation" } } },
|
|
74
|
+
overflowMenuAriaLabel: { control: "text", table: { category: "Labels", defaultValue: { summary: "Overflow navigation menu" } } },
|
|
75
|
+
overflowButtonLabel: {
|
|
76
|
+
control: "text",
|
|
77
|
+
description: "Accessible name of the icon-only overflow/burger button",
|
|
78
|
+
table: { category: "Labels", defaultValue: { summary: "More navigation" } },
|
|
79
|
+
},
|
|
80
|
+
submenuAriaLabel: {
|
|
81
|
+
control: "text",
|
|
82
|
+
description: "aria-label on each dropdown summary; {title} is replaced with the item's title",
|
|
83
|
+
table: { category: "Labels", defaultValue: { summary: "{title} submenu" } },
|
|
84
|
+
},
|
|
32
85
|
styleClassPassthrough: {
|
|
33
86
|
control: "object",
|
|
34
87
|
description: "Extra CSS classes applied to the root element",
|
|
35
88
|
},
|
|
36
89
|
},
|
|
90
|
+
decorators: [
|
|
91
|
+
(story, context) => ({
|
|
92
|
+
components: { story, CanvasSwitcher },
|
|
93
|
+
setup() {
|
|
94
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
95
|
+
return { canvasName };
|
|
96
|
+
},
|
|
97
|
+
template: `
|
|
98
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
99
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
100
|
+
</div>
|
|
101
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
102
|
+
<story />
|
|
103
|
+
</div>
|
|
104
|
+
`,
|
|
105
|
+
}),
|
|
106
|
+
],
|
|
107
|
+
parameters: {
|
|
108
|
+
layout: "fullscreen",
|
|
109
|
+
},
|
|
110
|
+
render,
|
|
37
111
|
args: {
|
|
112
|
+
mainNavJustifyContent: "space-between",
|
|
38
113
|
gapBetweenFirstAndSecondNav: 12,
|
|
39
114
|
collapseBreakpoint: null,
|
|
40
115
|
collapseAtMainNavIntersection: false,
|
|
@@ -44,7 +119,8 @@ const meta: Meta<typeof ResponsiveHeader> = {
|
|
|
44
119
|
};
|
|
45
120
|
|
|
46
121
|
export default meta;
|
|
47
|
-
type Story = StoryObj<
|
|
122
|
+
type Story = StoryObj<StoryArgs>;
|
|
123
|
+
|
|
48
124
|
|
|
49
125
|
const responsiveNavLinks = {
|
|
50
126
|
firstNav: [
|
|
@@ -73,13 +149,7 @@ export const Default: Story = {
|
|
|
73
149
|
export const ConstrainedWidth: Story = {
|
|
74
150
|
name: "Constrained Width (items overflow)",
|
|
75
151
|
args: { responsiveNavLinks },
|
|
76
|
-
render: (
|
|
77
|
-
components: { ResponsiveHeader },
|
|
78
|
-
setup() {
|
|
79
|
-
return { args };
|
|
80
|
-
},
|
|
81
|
-
template: `<div style="max-width: 420px"><ResponsiveHeader v-bind="args" /></div>`,
|
|
82
|
-
}),
|
|
152
|
+
render: renderWith(`<div style="max-width: 420px"><ResponsiveHeader v-bind="componentArgs" /></div>`),
|
|
83
153
|
};
|
|
84
154
|
|
|
85
155
|
export const CustomOverflowIcons: Story = {
|
|
@@ -92,19 +162,13 @@ export const CustomOverflowIcons: Story = {
|
|
|
92
162
|
export const WithSecondaryNavigationSlot: Story = {
|
|
93
163
|
name: "With secondaryNavigation slot",
|
|
94
164
|
args: { responsiveNavLinks },
|
|
95
|
-
render: (
|
|
96
|
-
|
|
97
|
-
setup() {
|
|
98
|
-
return { args };
|
|
99
|
-
},
|
|
100
|
-
template: `
|
|
101
|
-
<ResponsiveHeader v-bind="args">
|
|
165
|
+
render: renderWith(`
|
|
166
|
+
<ResponsiveHeader v-bind="componentArgs">
|
|
102
167
|
<template #secondaryNavigation>
|
|
103
168
|
<a href="/settings" aria-label="Settings">⚙</a>
|
|
104
169
|
</template>
|
|
105
170
|
</ResponsiveHeader>
|
|
106
|
-
|
|
107
|
-
}),
|
|
171
|
+
`),
|
|
108
172
|
};
|
|
109
173
|
|
|
110
174
|
export const SingleFlatGroup: Story = {
|
|
@@ -120,3 +184,70 @@ export const SingleFlatGroup: Story = {
|
|
|
120
184
|
},
|
|
121
185
|
},
|
|
122
186
|
};
|
|
187
|
+
|
|
188
|
+
export const MainNavAlignment: Story = {
|
|
189
|
+
name: "Main Nav Alignment",
|
|
190
|
+
args: { responsiveNavLinks, mainNavJustifyContent: "flex-start" },
|
|
191
|
+
parameters: {
|
|
192
|
+
docs: {
|
|
193
|
+
description: {
|
|
194
|
+
story:
|
|
195
|
+
"The main nav's two groups are spread with `space-between` by default. Use the Main nav justify content control " +
|
|
196
|
+
"(a CSS token set by the story, not a prop) to try `flex-start`, `safe center` and `safe end`. " +
|
|
197
|
+
"Narrow the canvas to check items still collapse into the overflow menu from the end, whatever the alignment.",
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
// ─── Stress test ──────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
const stressNavLinks: ResponsiveHeaderProp = {
|
|
206
|
+
primary: [
|
|
207
|
+
{ name: "Startseite", path: "/" },
|
|
208
|
+
{ name: "Pneumonoultramicroscopicsilicovolcanoconiosisbehandlungohneleerzeichen", path: "/long" },
|
|
209
|
+
{
|
|
210
|
+
name: "Leistungen und Behandlungen",
|
|
211
|
+
iconName: "lucide:this-icon-does-not-exist",
|
|
212
|
+
childLinks: [
|
|
213
|
+
...Array.from({ length: 26 }, (_, i) => ({ name: `Behandlung ${i + 1}`, path: `/treatment-${i + 1}` })),
|
|
214
|
+
{ name: "Same name", path: "/same-a" },
|
|
215
|
+
{ name: "Same name", path: "/same-b" },
|
|
216
|
+
{ name: "Kopfhautbehandlungmitspeziellenätherischenölenohneleerzeichendurchgehend", path: "/unbroken" },
|
|
217
|
+
{ name: "💇♀️ Emoji first", path: "/emoji" },
|
|
218
|
+
],
|
|
219
|
+
},
|
|
220
|
+
{ name: "One child", childLinksTitle: "Only one", childLinks: [{ name: "The one", path: "/one" }] },
|
|
221
|
+
{ name: "<b>not bold</b>", path: "/html" },
|
|
222
|
+
{ name: "من نحن", path: "/rtl" },
|
|
223
|
+
...Array.from({ length: 8 }, (_, i) => ({ name: `Item ${i + 1}`, path: `/item-${i + 1}` })),
|
|
224
|
+
],
|
|
225
|
+
secondary: [{ name: "Kontakt und Terminvereinbarung", path: "/contact" }],
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
export const StressTest: Story = {
|
|
229
|
+
name: "Stress Test (Worst-Case Data)",
|
|
230
|
+
args: {
|
|
231
|
+
responsiveNavLinks: stressNavLinks,
|
|
232
|
+
mainNavAriaLabel: "Hauptnavigation",
|
|
233
|
+
secondaryNavAriaLabel: "Sekundäre Navigation",
|
|
234
|
+
overflowMenuAriaLabel: "Weitere Navigationspunkte",
|
|
235
|
+
overflowButtonLabel: "Weitere Navigationspunkte anzeigen",
|
|
236
|
+
submenuAriaLabel: "Untermenü {title}",
|
|
237
|
+
},
|
|
238
|
+
parameters: {
|
|
239
|
+
initialCanvas: "laptopCanvas",
|
|
240
|
+
docs: {
|
|
241
|
+
description: {
|
|
242
|
+
story:
|
|
243
|
+
"Deliberately hostile data to find breakage: 16 top-level items in group keys other than firstNav/secondNav, " +
|
|
244
|
+
"a 70-character unbroken item name, a dropdown with no childLinksTitle (its name is shown instead) and 30 children " +
|
|
245
|
+
"including duplicate names, an unbroken name and emoji, a dropdown with one child, a broken icon name, HTML-like text " +
|
|
246
|
+
"(must render as text), right-to-left Arabic and German labels throughout. Check at every canvas width: items that " +
|
|
247
|
+
"don't fit collapse into the overflow menu, the burger only appears when something has collapsed, the top-bar dropdown " +
|
|
248
|
+
"and the overflow panel never run off-screen (long names wrap, 30 children scroll inside the panel), and nothing " +
|
|
249
|
+
"shows 'undefined'. Resize the canvas back and forth to check items return to the bar.",
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
|
|
2
|
+
import { defuFn } from 'defu'
|
|
3
|
+
|
|
4
|
+
const inlineConfig = {
|
|
5
|
+
"nuxt": {}
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** client **/
|
|
9
|
+
import { _replaceAppConfig } from '#app/config'
|
|
10
|
+
|
|
11
|
+
// Vite - webpack is handled directly in #app/config
|
|
12
|
+
if (import.meta.dev && !import.meta.nitro && import.meta.hot) {
|
|
13
|
+
import.meta.hot.accept((newModule) => {
|
|
14
|
+
_replaceAppConfig(newModule.default)
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
/** client-end **/
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
export default /*@__PURE__*/ defuFn(inlineConfig)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
|
|
2
|
+
import type { DefineComponent, SlotsType } from 'vue'
|
|
3
|
+
type IslandComponent<T> = DefineComponent<{}, {refresh: () => Promise<void>}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, SlotsType<{ fallback: { error: unknown } }>> & T
|
|
4
|
+
|
|
5
|
+
type HydrationStrategies = {
|
|
6
|
+
hydrateOnVisible?: IntersectionObserverInit | true
|
|
7
|
+
hydrateOnIdle?: number | true
|
|
8
|
+
hydrateOnInteraction?: keyof HTMLElementEventMap | Array<keyof HTMLElementEventMap> | true
|
|
9
|
+
hydrateOnMediaQuery?: string
|
|
10
|
+
hydrateAfter?: number
|
|
11
|
+
hydrateWhen?: boolean
|
|
12
|
+
hydrateNever?: true
|
|
13
|
+
}
|
|
14
|
+
type LazyComponent<T> = DefineComponent<HydrationStrategies, {}, {}, {}, {}, {}, {}, { hydrated: () => void }> & T
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
export const NuxtWelcome: typeof import("../../../../../../node_modules/nuxt/dist/app/components/welcome.vue")['default']
|
|
18
|
+
export const NuxtLayout: typeof import("../../../../../../node_modules/nuxt/dist/app/components/nuxt-layout")['default']
|
|
19
|
+
export const NuxtErrorBoundary: typeof import("../../../../../../node_modules/nuxt/dist/app/components/nuxt-error-boundary.vue")['default']
|
|
20
|
+
export const ClientOnly: typeof import("../../../../../../node_modules/nuxt/dist/app/components/client-only")['default']
|
|
21
|
+
export const DevOnly: typeof import("../../../../../../node_modules/nuxt/dist/app/components/dev-only")['default']
|
|
22
|
+
export const ServerPlaceholder: typeof import("../../../../../../node_modules/nuxt/dist/app/components/server-placeholder")['default']
|
|
23
|
+
export const NuxtLink: typeof import("../../../../../../node_modules/nuxt/dist/app/components/nuxt-link")['default']
|
|
24
|
+
export const NuxtLoadingIndicator: typeof import("../../../../../../node_modules/nuxt/dist/app/components/nuxt-loading-indicator")['default']
|
|
25
|
+
export const NuxtTime: typeof import("../../../../../../node_modules/nuxt/dist/app/components/nuxt-time.vue")['default']
|
|
26
|
+
export const NuxtRouteAnnouncer: typeof import("../../../../../../node_modules/nuxt/dist/app/components/nuxt-route-announcer")['default']
|
|
27
|
+
export const NuxtAnnouncer: typeof import("../../../../../../node_modules/nuxt/dist/app/components/nuxt-announcer")['default']
|
|
28
|
+
export const NuxtImg: typeof import("../../../../../../node_modules/nuxt/dist/app/components/nuxt-stubs")['NuxtImg']
|
|
29
|
+
export const NuxtPicture: typeof import("../../../../../../node_modules/nuxt/dist/app/components/nuxt-stubs")['NuxtPicture']
|
|
30
|
+
export const NuxtPage: typeof import("../../../../../../node_modules/nuxt/dist/pages/runtime/page-placeholder")['default']
|
|
31
|
+
export const NoScript: typeof import("../../../../../../node_modules/nuxt/dist/head/runtime/components")['NoScript']
|
|
32
|
+
export const Link: typeof import("../../../../../../node_modules/nuxt/dist/head/runtime/components")['Link']
|
|
33
|
+
export const Base: typeof import("../../../../../../node_modules/nuxt/dist/head/runtime/components")['Base']
|
|
34
|
+
export const Title: typeof import("../../../../../../node_modules/nuxt/dist/head/runtime/components")['Title']
|
|
35
|
+
export const Meta: typeof import("../../../../../../node_modules/nuxt/dist/head/runtime/components")['Meta']
|
|
36
|
+
export const Style: typeof import("../../../../../../node_modules/nuxt/dist/head/runtime/components")['Style']
|
|
37
|
+
export const Head: typeof import("../../../../../../node_modules/nuxt/dist/head/runtime/components")['Head']
|
|
38
|
+
export const Html: typeof import("../../../../../../node_modules/nuxt/dist/head/runtime/components")['Html']
|
|
39
|
+
export const Body: typeof import("../../../../../../node_modules/nuxt/dist/head/runtime/components")['Body']
|
|
40
|
+
export const NuxtIsland: typeof import("../../../../../../node_modules/nuxt/dist/app/components/nuxt-island")['default']
|
|
41
|
+
export const LazyNuxtWelcome: LazyComponent<typeof import("../../../../../../node_modules/nuxt/dist/app/components/welcome.vue")['default']>
|
|
42
|
+
export const LazyNuxtLayout: LazyComponent<typeof import("../../../../../../node_modules/nuxt/dist/app/components/nuxt-layout")['default']>
|
|
43
|
+
export const LazyNuxtErrorBoundary: LazyComponent<typeof import("../../../../../../node_modules/nuxt/dist/app/components/nuxt-error-boundary.vue")['default']>
|
|
44
|
+
export const LazyClientOnly: LazyComponent<typeof import("../../../../../../node_modules/nuxt/dist/app/components/client-only")['default']>
|
|
45
|
+
export const LazyDevOnly: LazyComponent<typeof import("../../../../../../node_modules/nuxt/dist/app/components/dev-only")['default']>
|
|
46
|
+
export const LazyServerPlaceholder: LazyComponent<typeof import("../../../../../../node_modules/nuxt/dist/app/components/server-placeholder")['default']>
|
|
47
|
+
export const LazyNuxtLink: LazyComponent<typeof import("../../../../../../node_modules/nuxt/dist/app/components/nuxt-link")['default']>
|
|
48
|
+
export const LazyNuxtLoadingIndicator: LazyComponent<typeof import("../../../../../../node_modules/nuxt/dist/app/components/nuxt-loading-indicator")['default']>
|
|
49
|
+
export const LazyNuxtTime: LazyComponent<typeof import("../../../../../../node_modules/nuxt/dist/app/components/nuxt-time.vue")['default']>
|
|
50
|
+
export const LazyNuxtRouteAnnouncer: LazyComponent<typeof import("../../../../../../node_modules/nuxt/dist/app/components/nuxt-route-announcer")['default']>
|
|
51
|
+
export const LazyNuxtAnnouncer: LazyComponent<typeof import("../../../../../../node_modules/nuxt/dist/app/components/nuxt-announcer")['default']>
|
|
52
|
+
export const LazyNuxtImg: LazyComponent<typeof import("../../../../../../node_modules/nuxt/dist/app/components/nuxt-stubs")['NuxtImg']>
|
|
53
|
+
export const LazyNuxtPicture: LazyComponent<typeof import("../../../../../../node_modules/nuxt/dist/app/components/nuxt-stubs")['NuxtPicture']>
|
|
54
|
+
export const LazyNuxtPage: LazyComponent<typeof import("../../../../../../node_modules/nuxt/dist/pages/runtime/page-placeholder")['default']>
|
|
55
|
+
export const LazyNoScript: LazyComponent<typeof import("../../../../../../node_modules/nuxt/dist/head/runtime/components")['NoScript']>
|
|
56
|
+
export const LazyLink: LazyComponent<typeof import("../../../../../../node_modules/nuxt/dist/head/runtime/components")['Link']>
|
|
57
|
+
export const LazyBase: LazyComponent<typeof import("../../../../../../node_modules/nuxt/dist/head/runtime/components")['Base']>
|
|
58
|
+
export const LazyTitle: LazyComponent<typeof import("../../../../../../node_modules/nuxt/dist/head/runtime/components")['Title']>
|
|
59
|
+
export const LazyMeta: LazyComponent<typeof import("../../../../../../node_modules/nuxt/dist/head/runtime/components")['Meta']>
|
|
60
|
+
export const LazyStyle: LazyComponent<typeof import("../../../../../../node_modules/nuxt/dist/head/runtime/components")['Style']>
|
|
61
|
+
export const LazyHead: LazyComponent<typeof import("../../../../../../node_modules/nuxt/dist/head/runtime/components")['Head']>
|
|
62
|
+
export const LazyHtml: LazyComponent<typeof import("../../../../../../node_modules/nuxt/dist/head/runtime/components")['Html']>
|
|
63
|
+
export const LazyBody: LazyComponent<typeof import("../../../../../../node_modules/nuxt/dist/head/runtime/components")['Body']>
|
|
64
|
+
export const LazyNuxtIsland: LazyComponent<typeof import("../../../../../../node_modules/nuxt/dist/app/components/nuxt-island")['default']>
|
|
65
|
+
|
|
66
|
+
export const componentNames: string[]
|