srcdev-nuxt-components 9.5.6 → 9.6.0
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/breadcrumb.md +5 -0
- package/.claude/skills/components/display-pill.md +2 -0
- package/.claude/skills/components/input-button.md +3 -0
- 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 +74 -0
- package/.claude/skills/components/service-detail.md +32 -0
- package/.claude/skills/components/service-summary.md +8 -0
- package/.claude/skills/components/services-card-grid.md +5 -3
- package/.claude/skills/components/services-card.md +22 -12
- package/.claude/skills/components/site-header.md +2 -0
- package/.claude/skills/components/slider-gallery.md +10 -4
- package/.claude/skills/components/tab-navigation.md +25 -9
- package/.claude/skills/index.md +1 -2
- package/.claude/skills/storybook-add-story.md +14 -0
- package/.vscode/srcdev-component-responsive-header.code-snippets +13 -1
- package/.vscode/srcdev-component-service-detail.code-snippets +1 -0
- package/.vscode/srcdev-component-service-summary.code-snippets +3 -1
- package/.vscode/srcdev-component-services-card.code-snippets +38 -36
- package/.vscode/srcdev-component-services-grids.code-snippets +2 -1
- package/.vscode/srcdev-component-site-header.code-snippets +3 -1
- package/.vscode/srcdev-component-tab-navigation.code-snippets +1 -1
- package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +3 -0
- package/app/components/01.atoms/display-pill/DisplayPill.vue +5 -2
- package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +16 -4
- package/app/components/01.atoms/navigation/breadcrumb/CONSUMER-STYLING.md +28 -5
- package/app/components/01.atoms/navigation/breadcrumb/stories/Breadcrumb.stories.ts +94 -0
- package/app/components/01.atoms/navigation/breadcrumb/tests/Breadcrumb.spec.ts +27 -0
- package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +52 -11
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +55 -23
- package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +176 -257
- package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +42 -2
- 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/image-galleries/slider-gallery/CONSUMER-STYLING.md +19 -1
- package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +65 -7
- package/app/components/03.organisms/image-galleries/slider-gallery/stories/SliderGallery.stories.ts +201 -63
- package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +41 -2
- package/app/components/03.organisms/image-galleries/slider-gallery/tests/__snapshots__/SliderGallery.spec.ts.snap +6 -6
- package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +18 -0
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +56 -18
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +106 -31
- package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +201 -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 +36 -0
- package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +131 -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/services/service-detail/CONSUMER-STYLING.md +92 -41
- package/app/components/03.organisms/services/service-detail/ServiceDetail.vue +198 -73
- package/app/components/03.organisms/services/service-detail/stories/ServiceDetail.stories.ts +157 -9
- package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +58 -0
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +4 -3
- package/app/components/03.organisms/services/service-summary/CONSUMER-STYLING.md +30 -0
- package/app/components/03.organisms/services/service-summary/ServiceSummary.vue +32 -8
- package/app/components/03.organisms/services/service-summary/stories/ServiceSummary.stories.ts +147 -0
- package/app/components/03.organisms/services/service-summary/tests/ServiceSummary.spec.ts +26 -0
- package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +1 -1
- package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +104 -63
- package/app/components/03.organisms/services/services-card/ServicesCard.vue +92 -66
- package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +194 -27
- package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +34 -8
- package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +8 -8
- package/app/components/03.organisms/services/services-grids/CONSUMER-STYLING.md +9 -0
- package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +2 -1
- package/app/components/03.organisms/services/services-grids/stories/ServiceSummaryGrid.stories.ts +46 -6
- package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +89 -11
- package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +8 -0
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +3 -3
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +24 -24
- 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/components/05.forms/input-button/CONSUMER-STYLING.md +8 -0
- package/app/components/05.forms/input-button/InputButton.vue +3 -1
- package/app/composables/tests/useAnchorScroll.spec.ts +8 -0
- package/app/composables/useAnchorScroll.ts +1 -1
- package/app/utils/opening-hours.ts +23 -2
- package/nuxt.config.ts +1 -1
- package/package.json +1 -1
- package/.claude/skills/components/site-navigation.md +0 -145
- package/.vscode/srcdev-component-site-navigation.code-snippets +0 -38
- package/app/components/02.molecules/navigation/site-navigation/CONSUMER-STYLING.md +0 -109
- package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +0 -751
- package/app/components/02.molecules/navigation/site-navigation/stories/SiteNavigation.stories.ts +0 -356
- package/app/components/02.molecules/navigation/site-navigation/tests/SiteNavigation.spec.ts +0 -367
- package/app/components/02.molecules/navigation/site-navigation/tests/__snapshots__/SiteNavigation.spec.ts.snap +0 -30
package/app/components/03.organisms/image-galleries/slider-gallery/stories/SliderGallery.stories.ts
CHANGED
|
@@ -1,11 +1,54 @@
|
|
|
1
|
+
import { computed, ref } from "vue";
|
|
1
2
|
import SliderGallery from "../SliderGallery.vue";
|
|
3
|
+
import CanvasSwitcher from "../../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
2
4
|
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
-
import type { IGalleryData } from "~/types/components";
|
|
5
|
+
import type { IGalleryData, MediaCanvas } from "~/types/components";
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
type LineClamp = "none" | "1" | "2" | "3" | "4";
|
|
8
|
+
|
|
9
|
+
// Story-only args: each drives a CSS token, not a SliderGallery prop. See the argTypes entries.
|
|
10
|
+
const lineClampTokens = {
|
|
11
|
+
authorLineClamp: { token: "--slider-gallery-author-line-clamp", label: "Author (stylist)", fallback: "none" },
|
|
12
|
+
titleLineClamp: { token: "--slider-gallery-title-line-clamp", label: "Title", fallback: "none" },
|
|
13
|
+
topicLineClamp: { token: "--slider-gallery-topic-line-clamp", label: "Topic (category)", fallback: "none" },
|
|
14
|
+
descriptionLineClamp: { token: "--slider-gallery-description-line-clamp", label: "Description", fallback: "none" },
|
|
15
|
+
thumbnailTitleLineClamp: { token: "--slider-gallery-thumbnail-title-line-clamp", label: "Thumbnail title", fallback: "2" },
|
|
16
|
+
thumbnailDescriptionLineClamp: {
|
|
17
|
+
token: "--slider-gallery-thumbnail-description-line-clamp",
|
|
18
|
+
label: "Thumbnail description",
|
|
19
|
+
fallback: "2",
|
|
20
|
+
},
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
type LineClampArg = keyof typeof lineClampTokens;
|
|
24
|
+
|
|
25
|
+
type StoryArgs = InstanceType<typeof SliderGallery>["$props"] & Record<LineClampArg, LineClamp>;
|
|
26
|
+
|
|
27
|
+
const lineClampArgTypes = Object.fromEntries(
|
|
28
|
+
Object.entries(lineClampTokens).map(([arg, { token, label, fallback }]) => [
|
|
29
|
+
arg,
|
|
30
|
+
{
|
|
31
|
+
name: `${label} line clamp`,
|
|
32
|
+
control: "select",
|
|
33
|
+
options: ["none", "1", "2", "3", "4"] satisfies LineClamp[],
|
|
34
|
+
description:
|
|
35
|
+
`**Story control, not a prop.** Sets the \`${token}\` CSS token on a wrapper so you can try it here. ` +
|
|
36
|
+
`To use it in an app, set the token in your own CSS, e.g. \`.hero { ${token}: 2; }\`. ` +
|
|
37
|
+
`\`1\` is single-line ellipsis, \`none\` shows everything.`,
|
|
38
|
+
table: { category: "CSS tokens (story only, set in your CSS)", defaultValue: { summary: fallback } },
|
|
39
|
+
},
|
|
40
|
+
])
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
const lineClampArgs = Object.fromEntries(
|
|
44
|
+
Object.entries(lineClampTokens).map(([arg, { fallback }]) => [arg, fallback])
|
|
45
|
+
) as Record<LineClampArg, LineClamp>;
|
|
46
|
+
|
|
47
|
+
const meta: Meta<StoryArgs> = {
|
|
6
48
|
title: "Organisms/Image Galleries/Slider Gallery",
|
|
7
49
|
component: SliderGallery,
|
|
8
50
|
argTypes: {
|
|
51
|
+
...lineClampArgTypes,
|
|
9
52
|
autoRun: {
|
|
10
53
|
control: { type: "boolean" },
|
|
11
54
|
description: "Automatically advance to the next slide",
|
|
@@ -40,6 +83,7 @@ const meta: Meta<typeof SliderGallery> = {
|
|
|
40
83
|
},
|
|
41
84
|
},
|
|
42
85
|
args: {
|
|
86
|
+
...lineClampArgs,
|
|
43
87
|
autoRun: true,
|
|
44
88
|
autoRunInterval: 7000,
|
|
45
89
|
animationDuration: 3000,
|
|
@@ -64,7 +108,7 @@ const meta: Meta<typeof SliderGallery> = {
|
|
|
64
108
|
};
|
|
65
109
|
|
|
66
110
|
export default meta;
|
|
67
|
-
type Story = StoryObj<
|
|
111
|
+
type Story = StoryObj<StoryArgs>;
|
|
68
112
|
|
|
69
113
|
// ─── Fixtures ─────────────────────────────────────────────────────────────────
|
|
70
114
|
|
|
@@ -123,17 +167,105 @@ const sampleSlides: IGalleryData[] = [
|
|
|
123
167
|
},
|
|
124
168
|
];
|
|
125
169
|
|
|
170
|
+
const longGerman =
|
|
171
|
+
"Donaudampfschifffahrtsgesellschaftskapitänsmützenbandherstellungsbetriebsgenossenschaftsvorsitzender";
|
|
172
|
+
const longUrl =
|
|
173
|
+
"https://example.com/a/very/long/path/that/never/breaks/because/it/has/no/spaces/at/all?utm_source=newsletter&utm_medium=email&utm_campaign=autumn-collection-2026-final-final-v3";
|
|
174
|
+
const longParagraph =
|
|
175
|
+
"Eine ausgesprochen lange Beschreibung, wie sie ein CMS liefert, wenn niemand die Zeichenanzahl prüft. ".repeat(8);
|
|
176
|
+
|
|
177
|
+
const stressSlides: IGalleryData[] = [
|
|
178
|
+
{
|
|
179
|
+
src: "https://picsum.photos/seed/stress1/1920/1080",
|
|
180
|
+
alt: "Long German copy in every field",
|
|
181
|
+
stylist: `STYLISTIN ${longGerman.toUpperCase()}`,
|
|
182
|
+
title: `${longGerman} und Freunde`,
|
|
183
|
+
category: "Haarverlängerungen, Strähnchen und vollständige Farbkorrekturen",
|
|
184
|
+
description: longParagraph,
|
|
185
|
+
thumbnail: { title: longGerman, description: longParagraph },
|
|
186
|
+
textBrightness: "light",
|
|
187
|
+
href: longUrl,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
src: "https://picsum.photos/seed/stress2/1920/1080",
|
|
191
|
+
alt: "Emoji and HTML-like text",
|
|
192
|
+
stylist: "💇♀️✨ SALON 🎉",
|
|
193
|
+
title: "<script>alert('xss')</script>",
|
|
194
|
+
category: "<b>not bold</b> & <tags>",
|
|
195
|
+
description: "🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈",
|
|
196
|
+
thumbnail: { title: "😀 Emoji first", description: "<img src=x onerror=alert(1)>" },
|
|
197
|
+
textBrightness: "dark",
|
|
198
|
+
href: "#emoji",
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
src: "https://picsum.photos/seed/stress3/1920/1080",
|
|
202
|
+
alt: "صورة لصالون تصفيف الشعر",
|
|
203
|
+
stylist: "مصففة الشعر",
|
|
204
|
+
title: "تسريحات شعر جديدة لموسم الخريف",
|
|
205
|
+
category: "تلوين الشعر",
|
|
206
|
+
description: "وصف طويل باللغة العربية يمتد عبر عدة أسطر للتحقق من أن النص من اليمين إلى اليسار يعرض بشكل صحيح.",
|
|
207
|
+
thumbnail: { title: "تسريحات", description: "تلوين الشعر" },
|
|
208
|
+
textBrightness: "light",
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
src: "https://example.invalid/this-image-does-not-exist.jpg",
|
|
212
|
+
alt: "Broken image URL: the alt text should show instead",
|
|
213
|
+
stylist: "A",
|
|
214
|
+
title: "B",
|
|
215
|
+
category: "C",
|
|
216
|
+
description: "",
|
|
217
|
+
thumbnail: { title: "", description: "" },
|
|
218
|
+
textBrightness: "dark",
|
|
219
|
+
href: "",
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
src: "https://picsum.photos/seed/stress5/1920/1080",
|
|
223
|
+
alt: "",
|
|
224
|
+
textBrightness: "light",
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
src: "https://picsum.photos/seed/stress6/1920/1080",
|
|
228
|
+
alt: "Unbroken URL as description",
|
|
229
|
+
title: "Link soup",
|
|
230
|
+
description: longUrl,
|
|
231
|
+
thumbnail: { title: longUrl, description: longUrl },
|
|
232
|
+
textBrightness: "light",
|
|
233
|
+
href: "#link-soup",
|
|
234
|
+
},
|
|
235
|
+
...Array.from({ length: 24 }, (_, index): IGalleryData => ({
|
|
236
|
+
src: `https://picsum.photos/seed/stress-many-${index}/1920/1080`,
|
|
237
|
+
alt: `Filler slide ${index + 7}`,
|
|
238
|
+
title: `Slide ${index + 7}`,
|
|
239
|
+
thumbnail: { title: `Slide ${index + 7}`, description: "Filler" },
|
|
240
|
+
textBrightness: "light",
|
|
241
|
+
})),
|
|
242
|
+
];
|
|
243
|
+
|
|
126
244
|
// ─── Stories ──────────────────────────────────────────────────────────────────
|
|
127
245
|
|
|
246
|
+
function useStorySetup(args: StoryArgs) {
|
|
247
|
+
const componentArgs = computed(() =>
|
|
248
|
+
Object.fromEntries(Object.entries(args).filter(([arg]) => !(arg in lineClampTokens)))
|
|
249
|
+
);
|
|
250
|
+
const tokenStyles = computed(() =>
|
|
251
|
+
Object.fromEntries(
|
|
252
|
+
Object.entries(lineClampTokens).map(([arg, { token }]) => [token, args[arg as LineClampArg]])
|
|
253
|
+
)
|
|
254
|
+
);
|
|
255
|
+
return { componentArgs, tokenStyles };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const renderWith = (slides: IGalleryData[]) => (args: StoryArgs) => ({
|
|
259
|
+
components: { SliderGallery },
|
|
260
|
+
setup() {
|
|
261
|
+
const galleryData = ref<IGalleryData[]>(slides);
|
|
262
|
+
return { ...useStorySetup(args), galleryData };
|
|
263
|
+
},
|
|
264
|
+
template: `<div :style="tokenStyles"><SliderGallery v-bind="componentArgs" v-model:gallery-data="galleryData" /></div>`,
|
|
265
|
+
});
|
|
266
|
+
|
|
128
267
|
export const Default: Story = {
|
|
129
|
-
render: (
|
|
130
|
-
components: { SliderGallery },
|
|
131
|
-
setup() {
|
|
132
|
-
const galleryData = ref<IGalleryData[]>(sampleSlides);
|
|
133
|
-
return { args, galleryData };
|
|
134
|
-
},
|
|
135
|
-
template: `<SliderGallery v-bind="args" v-model:gallery-data="galleryData" />`,
|
|
136
|
-
}),
|
|
268
|
+
render: renderWith(sampleSlides),
|
|
137
269
|
};
|
|
138
270
|
|
|
139
271
|
export const AutoRunDisabled: Story = {
|
|
@@ -141,14 +273,7 @@ export const AutoRunDisabled: Story = {
|
|
|
141
273
|
args: {
|
|
142
274
|
autoRun: false,
|
|
143
275
|
},
|
|
144
|
-
render: (
|
|
145
|
-
components: { SliderGallery },
|
|
146
|
-
setup() {
|
|
147
|
-
const galleryData = ref<IGalleryData[]>(sampleSlides);
|
|
148
|
-
return { args, galleryData };
|
|
149
|
-
},
|
|
150
|
-
template: `<SliderGallery v-bind="args" v-model:gallery-data="galleryData" />`,
|
|
151
|
-
}),
|
|
276
|
+
render: renderWith(sampleSlides),
|
|
152
277
|
parameters: {
|
|
153
278
|
docs: {
|
|
154
279
|
description: {
|
|
@@ -164,14 +289,7 @@ export const FastTransition: Story = {
|
|
|
164
289
|
animationDuration: 500,
|
|
165
290
|
autoRunInterval: 3000,
|
|
166
291
|
},
|
|
167
|
-
render: (
|
|
168
|
-
components: { SliderGallery },
|
|
169
|
-
setup() {
|
|
170
|
-
const galleryData = ref<IGalleryData[]>(sampleSlides);
|
|
171
|
-
return { args, galleryData };
|
|
172
|
-
},
|
|
173
|
-
template: `<SliderGallery v-bind="args" v-model:gallery-data="galleryData" />`,
|
|
174
|
-
}),
|
|
292
|
+
render: renderWith(sampleSlides),
|
|
175
293
|
};
|
|
176
294
|
|
|
177
295
|
export const SlowTransition: Story = {
|
|
@@ -180,14 +298,7 @@ export const SlowTransition: Story = {
|
|
|
180
298
|
animationDuration: 5000,
|
|
181
299
|
autoRunInterval: 10000,
|
|
182
300
|
},
|
|
183
|
-
render: (
|
|
184
|
-
components: { SliderGallery },
|
|
185
|
-
setup() {
|
|
186
|
-
const galleryData = ref<IGalleryData[]>(sampleSlides);
|
|
187
|
-
return { args, galleryData };
|
|
188
|
-
},
|
|
189
|
-
template: `<SliderGallery v-bind="args" v-model:gallery-data="galleryData" />`,
|
|
190
|
-
}),
|
|
301
|
+
render: renderWith(sampleSlides),
|
|
191
302
|
};
|
|
192
303
|
|
|
193
304
|
export const SingleSlide: Story = {
|
|
@@ -195,14 +306,7 @@ export const SingleSlide: Story = {
|
|
|
195
306
|
args: {
|
|
196
307
|
autoRun: false,
|
|
197
308
|
},
|
|
198
|
-
render: (
|
|
199
|
-
components: { SliderGallery },
|
|
200
|
-
setup() {
|
|
201
|
-
const galleryData = ref<IGalleryData[]>([sampleSlides[0]!]);
|
|
202
|
-
return { args, galleryData };
|
|
203
|
-
},
|
|
204
|
-
template: `<SliderGallery v-bind="args" v-model:gallery-data="galleryData" />`,
|
|
205
|
-
}),
|
|
309
|
+
render: renderWith([sampleSlides[0]!]),
|
|
206
310
|
parameters: {
|
|
207
311
|
docs: {
|
|
208
312
|
description: {
|
|
@@ -217,18 +321,11 @@ export const MinimalSlideData: Story = {
|
|
|
217
321
|
args: {
|
|
218
322
|
autoRun: false,
|
|
219
323
|
},
|
|
220
|
-
render: (
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
{ src: "https://picsum.photos/seed/min2/1920/1080", alt: "Image two", textBrightness: "dark" },
|
|
226
|
-
{ src: "https://picsum.photos/seed/min3/1920/1080", alt: "Image three", textBrightness: "light" },
|
|
227
|
-
]);
|
|
228
|
-
return { args, galleryData };
|
|
229
|
-
},
|
|
230
|
-
template: `<SliderGallery v-bind="args" v-model:gallery-data="galleryData" />`,
|
|
231
|
-
}),
|
|
324
|
+
render: renderWith([
|
|
325
|
+
{ src: "https://picsum.photos/seed/min1/1920/1080", alt: "Image one", textBrightness: "light" },
|
|
326
|
+
{ src: "https://picsum.photos/seed/min2/1920/1080", alt: "Image two", textBrightness: "dark" },
|
|
327
|
+
{ src: "https://picsum.photos/seed/min3/1920/1080", alt: "Image three", textBrightness: "light" },
|
|
328
|
+
]),
|
|
232
329
|
parameters: {
|
|
233
330
|
docs: {
|
|
234
331
|
description: {
|
|
@@ -240,14 +337,7 @@ export const MinimalSlideData: Story = {
|
|
|
240
337
|
|
|
241
338
|
export const EmptyGallery: Story = {
|
|
242
339
|
name: "Empty Gallery",
|
|
243
|
-
render: (
|
|
244
|
-
components: { SliderGallery },
|
|
245
|
-
setup() {
|
|
246
|
-
const galleryData = ref<IGalleryData[]>([]);
|
|
247
|
-
return { args, galleryData };
|
|
248
|
-
},
|
|
249
|
-
template: `<SliderGallery v-bind="args" v-model:gallery-data="galleryData" />`,
|
|
250
|
-
}),
|
|
340
|
+
render: renderWith([]),
|
|
251
341
|
parameters: {
|
|
252
342
|
docs: {
|
|
253
343
|
description: {
|
|
@@ -256,3 +346,51 @@ export const EmptyGallery: Story = {
|
|
|
256
346
|
},
|
|
257
347
|
},
|
|
258
348
|
};
|
|
349
|
+
|
|
350
|
+
export const StressTest: Story = {
|
|
351
|
+
name: "Stress Test (Worst-Case Data)",
|
|
352
|
+
args: {
|
|
353
|
+
autoRunInterval: 0,
|
|
354
|
+
loadingText: "Die Galerie wird geladen, bitte haben Sie einen Augenblick Geduld...",
|
|
355
|
+
seeMoreText: "WEITERE INFORMATIONEN ZU DIESEM BEITRAG ANZEIGEN",
|
|
356
|
+
ariaLabel: "",
|
|
357
|
+
},
|
|
358
|
+
render: renderWith(stressSlides),
|
|
359
|
+
decorators: [
|
|
360
|
+
(story, context) => ({
|
|
361
|
+
components: { story, CanvasSwitcher },
|
|
362
|
+
setup() {
|
|
363
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
|
|
364
|
+
return { canvasName };
|
|
365
|
+
},
|
|
366
|
+
template: `
|
|
367
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
368
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
369
|
+
</div>
|
|
370
|
+
<div
|
|
371
|
+
:class="canvasName"
|
|
372
|
+
style="position: relative; block-size: 80svh; margin-inline: auto; outline: 1px dashed currentColor; --slider-gallery-width: 100%; --slider-gallery-height: 100%;"
|
|
373
|
+
>
|
|
374
|
+
<story />
|
|
375
|
+
</div>
|
|
376
|
+
`,
|
|
377
|
+
}),
|
|
378
|
+
],
|
|
379
|
+
parameters: {
|
|
380
|
+
initialCanvas: "mobileCanvas",
|
|
381
|
+
docs: {
|
|
382
|
+
description: {
|
|
383
|
+
story:
|
|
384
|
+
"Deliberately hostile data to find breakage, sized to the canvas with `--slider-gallery-width: 100%` and " +
|
|
385
|
+
"`--slider-gallery-height: 100%`: long German copy and unbroken strings (a 100-character compound word, a long URL) " +
|
|
386
|
+
"in every field including the see-more and loading copy, emoji (including first), HTML-like text (must render as text), " +
|
|
387
|
+
"right-to-left Arabic, a broken image URL (its alt text shows), single characters, empty strings (no empty elements or " +
|
|
388
|
+
"empty link are rendered), a slide with an empty alt and no text, an empty carousel label, and 30 slides. " +
|
|
389
|
+
"`autoRunInterval` is 0: auto-advance is floored to the transition length (minimum 1s), so it keeps cycling rather than " +
|
|
390
|
+
"stalling; try a negative `animationDuration` too. Hover the gallery to pause it while you look. " +
|
|
391
|
+
"Check at every canvas width: no text runs off the slide, thumbnail text stays inside its card (2 lines each by default), " +
|
|
392
|
+
"and the arrows stay usable. Try the line clamp controls too: they set CSS tokens, they are not props.",
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
};
|
package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts
CHANGED
|
@@ -43,9 +43,13 @@ type MockImageInstance = {
|
|
|
43
43
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
44
44
|
|
|
45
45
|
/** Mount the gallery and simulate the first image loading successfully. */
|
|
46
|
-
async function mountAndLoad(
|
|
46
|
+
async function mountAndLoad(
|
|
47
|
+
galleryData: IGalleryData[],
|
|
48
|
+
mockImage: MockImageInstance,
|
|
49
|
+
props: Record<string, unknown> = {}
|
|
50
|
+
) {
|
|
47
51
|
const wrapper = await mountSuspended(SliderGallery, {
|
|
48
|
-
props: { galleryData },
|
|
52
|
+
props: { galleryData, ...props },
|
|
49
53
|
});
|
|
50
54
|
mockImage.onload?.();
|
|
51
55
|
await nextTick(); // let onMounted resume after Promise.race resolves
|
|
@@ -310,4 +314,39 @@ describe("SliderGallery", () => {
|
|
|
310
314
|
await nextTick();
|
|
311
315
|
expect(wrapper.classes()).not.toContain("is-next");
|
|
312
316
|
});
|
|
317
|
+
|
|
318
|
+
// ─── Worst-case numbers ─────────────────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
const activeAlt = (wrapper: Awaited<ReturnType<typeof mountAndLoad>>) =>
|
|
321
|
+
wrapper.find(".slider-gallery-list .slider-gallery-item img").attributes("alt");
|
|
322
|
+
|
|
323
|
+
// Mount with motion allowed, then let the load chain settle so the first auto-run timer is queued.
|
|
324
|
+
async function mountForAutoRun(props: Record<string, unknown>) {
|
|
325
|
+
vi.stubGlobal("matchMedia", vi.fn(() => ({ matches: false })));
|
|
326
|
+
const wrapper = await mountAndLoad(mockGalleryData, mockImage, props);
|
|
327
|
+
vi.advanceTimersByTime(1000);
|
|
328
|
+
await nextTick();
|
|
329
|
+
return wrapper;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
it("keeps auto-advancing when autoRunInterval is shorter than the transition", async () => {
|
|
333
|
+
const wrapper = await mountForAutoRun({ autoRunInterval: 0, animationDuration: 3000 });
|
|
334
|
+
expect(activeAlt(wrapper)).toBe("Slide 1");
|
|
335
|
+
vi.advanceTimersByTime(3000);
|
|
336
|
+
await nextTick();
|
|
337
|
+
expect(activeAlt(wrapper)).toBe("Slide 2");
|
|
338
|
+
vi.advanceTimersByTime(3000);
|
|
339
|
+
await nextTick();
|
|
340
|
+
expect(activeAlt(wrapper)).toBe("Slide 3");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("floors auto-advance at 1s when both timings are zero or negative", async () => {
|
|
344
|
+
const wrapper = await mountForAutoRun({ autoRunInterval: 0, animationDuration: -500 });
|
|
345
|
+
vi.advanceTimersByTime(999);
|
|
346
|
+
await nextTick();
|
|
347
|
+
expect(activeAlt(wrapper)).toBe("Slide 1");
|
|
348
|
+
vi.advanceTimersByTime(1);
|
|
349
|
+
await nextTick();
|
|
350
|
+
expect(activeAlt(wrapper)).toBe("Slide 2");
|
|
351
|
+
});
|
|
313
352
|
});
|
|
@@ -8,7 +8,7 @@ exports[`SliderGallery > renders correct HTML structure after load 1`] = `
|
|
|
8
8
|
</div>
|
|
9
9
|
<div class="slider-gallery-content">
|
|
10
10
|
<div class="slider-gallery-list">
|
|
11
|
-
<div class="slider-gallery-item" data-text-brightness="light" data-has-text=""><img data-nuxt-img="" srcset="/_ipx/
|
|
11
|
+
<div class="slider-gallery-item" data-text-brightness="light" data-has-text=""><img width="1920" height="1080" data-nuxt-img="" srcset="/_ipx/s_1920x1080/images/slide-1.jpg 1x, /_ipx/s_3840x2160/images/slide-1.jpg 2x" alt="Slide 1" src="/_ipx/s_1920x1080/images/slide-1.jpg">
|
|
12
12
|
<div class="slider-gallery-item-content light">
|
|
13
13
|
<div class="slider-gallery-author">Stylist One</div>
|
|
14
14
|
<div class="slider-gallery-title">First Slide</div>
|
|
@@ -17,7 +17,7 @@ exports[`SliderGallery > renders correct HTML structure after load 1`] = `
|
|
|
17
17
|
<!--v-if-->
|
|
18
18
|
</div>
|
|
19
19
|
</div>
|
|
20
|
-
<div class="slider-gallery-item" data-text-brightness="dark" data-has-text=""><img data-nuxt-img="" srcset="/_ipx/
|
|
20
|
+
<div class="slider-gallery-item" data-text-brightness="dark" data-has-text=""><img width="1920" height="1080" data-nuxt-img="" srcset="/_ipx/s_1920x1080/images/slide-2.jpg 1x, /_ipx/s_3840x2160/images/slide-2.jpg 2x" alt="Slide 2" src="/_ipx/s_1920x1080/images/slide-2.jpg">
|
|
21
21
|
<div class="slider-gallery-item-content dark">
|
|
22
22
|
<!--v-if-->
|
|
23
23
|
<div class="slider-gallery-title">Second Slide</div>
|
|
@@ -26,7 +26,7 @@ exports[`SliderGallery > renders correct HTML structure after load 1`] = `
|
|
|
26
26
|
<!--v-if-->
|
|
27
27
|
</div>
|
|
28
28
|
</div>
|
|
29
|
-
<div class="slider-gallery-item" data-text-brightness="light"><img data-nuxt-img="" srcset="/_ipx/
|
|
29
|
+
<div class="slider-gallery-item" data-text-brightness="light"><img width="1920" height="1080" data-nuxt-img="" srcset="/_ipx/s_1920x1080/images/slide-3.jpg 1x, /_ipx/s_3840x2160/images/slide-3.jpg 2x" alt="Slide 3" src="/_ipx/s_1920x1080/images/slide-3.jpg">
|
|
30
30
|
<div class="slider-gallery-item-content light">
|
|
31
31
|
<!--v-if-->
|
|
32
32
|
<!--v-if-->
|
|
@@ -38,7 +38,7 @@ exports[`SliderGallery > renders correct HTML structure after load 1`] = `
|
|
|
38
38
|
</div>
|
|
39
39
|
<div class="slider-gallery-thumbnails" aria-hidden="true">
|
|
40
40
|
<div class="slider-gallery-item">
|
|
41
|
-
<div class="slider-gallery-thumbnail-overlay"><img data-nuxt-img="" srcset="/_ipx/
|
|
41
|
+
<div class="slider-gallery-thumbnail-overlay"><img width="640" height="940" data-nuxt-img="" srcset="/_ipx/s_640x940/images/slide-1.jpg 1x, /_ipx/s_1280x1880/images/slide-1.jpg 2x" alt="" loading="lazy" src="/_ipx/s_640x940/images/slide-1.jpg">
|
|
42
42
|
<div class="slider-gallery-item-content light">
|
|
43
43
|
<div class="slider-gallery-title">Thumb 1</div>
|
|
44
44
|
<div class="slider-gallery-description">Thumb desc 1</div>
|
|
@@ -46,7 +46,7 @@ exports[`SliderGallery > renders correct HTML structure after load 1`] = `
|
|
|
46
46
|
</div>
|
|
47
47
|
</div>
|
|
48
48
|
<div class="slider-gallery-item">
|
|
49
|
-
<div class="slider-gallery-thumbnail-overlay"><img data-nuxt-img="" srcset="/_ipx/
|
|
49
|
+
<div class="slider-gallery-thumbnail-overlay"><img width="640" height="940" data-nuxt-img="" srcset="/_ipx/s_640x940/images/slide-2.jpg 1x, /_ipx/s_1280x1880/images/slide-2.jpg 2x" alt="" loading="lazy" src="/_ipx/s_640x940/images/slide-2.jpg">
|
|
50
50
|
<div class="slider-gallery-item-content dark">
|
|
51
51
|
<div class="slider-gallery-title">Thumb 2</div>
|
|
52
52
|
<div class="slider-gallery-description">Thumb desc 2</div>
|
|
@@ -54,7 +54,7 @@ exports[`SliderGallery > renders correct HTML structure after load 1`] = `
|
|
|
54
54
|
</div>
|
|
55
55
|
</div>
|
|
56
56
|
<div class="slider-gallery-item">
|
|
57
|
-
<div class="slider-gallery-thumbnail-overlay"><img data-nuxt-img="" srcset="/_ipx/
|
|
57
|
+
<div class="slider-gallery-thumbnail-overlay"><img width="640" height="940" data-nuxt-img="" srcset="/_ipx/s_640x940/images/slide-3.jpg 1x, /_ipx/s_1280x1880/images/slide-3.jpg 2x" alt="" loading="lazy" src="/_ipx/s_640x940/images/slide-3.jpg">
|
|
58
58
|
<div class="slider-gallery-item-content light">
|
|
59
59
|
<!--v-if-->
|
|
60
60
|
<!--v-if-->
|
|
@@ -11,6 +11,7 @@ are documented alongside it since that's where they're consumed.
|
|
|
11
11
|
|---|---|---|
|
|
12
12
|
| `--responsive-header-link-font-size` | `inherit` | Nav-link font-size. **Always set this to a fixed value.** Leaving it `inherit` means a fluid ancestor font-size (`clamp()`/`vw`) can drift nav-item text width without ever re-triggering the overflow-collapse measurement — see the component skill doc's "measurement pipeline" section for the full mechanism. |
|
|
13
13
|
| `--responsive-header-link-color` | `inherit` | Link/summary text colour. |
|
|
14
|
+
| `--responsive-header-main-nav-justify-content` | `space-between` | How the main nav's groups are spread along the bar. Use `flex-start`, `safe center` or `safe end`; plain `center`/`end` let items that don't fit spill off the start edge, where they're clipped. (Safari 16 ignores `safe` values and falls back to start alignment.) |
|
|
14
15
|
| `--responsive-header-color` | `inherit` | Root element text colour. |
|
|
15
16
|
| `--responsive-header-margin` | `0` | Root element margin. |
|
|
16
17
|
| `--responsive-header-bg` | `transparent` | Root element background. |
|
|
@@ -20,8 +21,21 @@ are documented alongside it since that's where they're consumed.
|
|
|
20
21
|
| `--responsive-header-max-height` | `none` | Root element max-height. |
|
|
21
22
|
| `--responsive-header-inline-size` | `100%` | Root element inline-size. |
|
|
22
23
|
| `--responsive-header-sub-nav-bg` / `-border` / `-border-radius` / `-padding` | `Canvas` / `1px solid #efefef75` / `8px` / `12px` | Top-bar dropdown panel (`.main-navigation-sub-nav`). |
|
|
24
|
+
| `--responsive-header-sub-nav-max-inline-size` | `min(48rem, calc(100vw - 3.2rem))` | Widest the top-bar dropdown panel gets; long child names wrap inside it. |
|
|
25
|
+
| `--responsive-header-sub-nav-max-block-size` | `70vh` | Tallest the top-bar dropdown panel gets before it scrolls, so a long child list stays reachable under a sticky header. |
|
|
23
26
|
| `--responsive-header-overflow-btn-bg` / `-size` / `-border` / `-outline` / `-icon-color` / `-hover-outline` | `Canvas` / `20px` / `1px solid #ffffff90` / `1px solid #ffffff10` / `inherit` / `1px solid #ffffff` | Overflow burger button. |
|
|
24
27
|
| `--responsive-header-overflow-nav-bg` / `-border` / `-border-radius` / `-padding-block` | `Canvas` / `1px solid #ffffff90` / `8px` / `12px` | Overflow panel container. |
|
|
28
|
+
| `--responsive-header-overflow-nav-max-block-size` | `70vh` | Tallest the overflow panel gets before it scrolls. |
|
|
29
|
+
| `--overflow-nav-max-inline-size` | `calc(100vw - 3.2rem)` | Widest the overflow panel's content gets (set on `NavigationItems`); long names wrap inside it. |
|
|
30
|
+
|
|
31
|
+
Link and dropdown text is never line-clamped: it names where the link goes. Long unbroken names
|
|
32
|
+
wrap inside the dropdown and overflow panels; in the top bar, an item too wide to fit moves to the
|
|
33
|
+
overflow menu.
|
|
34
|
+
|
|
35
|
+
> **Changed 2026-10-05**: the dropdown and overflow panels had no maximum size, so a long child
|
|
36
|
+
> name or a long child list ran off-screen. They are now capped by the four tokens above.
|
|
37
|
+
> `--_overflow-drop-down-width`, a private token nothing ever set, is gone (`min-width: fit-content`
|
|
38
|
+
> directly).
|
|
25
39
|
| `--responsive-nav-decorator-indicator-color` | `currentColor` | Active-item indicator bar/underline. |
|
|
26
40
|
| `--responsive-nav-decorator-hovered-indicator-color` | inherits the above | Indicator colour while hovering. |
|
|
27
41
|
| `--responsive-nav-decorator-hovered-bg` | `oklch(100% 0 0 / 8%)` | Hover highlight background behind the hovered item. |
|
|
@@ -66,6 +80,10 @@ Set the tokens above on an element you own (a page or section class, or a class
|
|
|
66
80
|
|
|
67
81
|
## Notes
|
|
68
82
|
|
|
83
|
+
- Same-page `#anchor` items (since 2026-10-06) render as plain `<a>` with the same classes as route
|
|
84
|
+
links (`.main-navigation-link`, `.overflow-navigation-link`, the sub-nav link classes), so every token
|
|
85
|
+
and selector here applies to them. Their `li.is-active` follows the current hash rather than the route.
|
|
86
|
+
|
|
69
87
|
- Anything with async-loaded content (an `iconName` icon on a nav item, a new decorator inside
|
|
70
88
|
a nav link) must reserve its own `width`/`height` in CSS — an unsized element measures at
|
|
71
89
|
`0` during the initial geometry pass and then pops in afterward with nothing to detect it.
|