srcdev-nuxt-components 9.0.14 → 9.0.16
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/settings.json +25 -0
- package/.claude/skills/component-aria-landmark.md +68 -0
- package/.claude/skills/component-dynamic-slots.md +150 -0
- package/.claude/skills/component-local-style-override.md +126 -0
- package/.claude/skills/component-prop-driven-container-layout.md +42 -0
- package/.claude/skills/components/accordian-core.md +159 -0
- package/.claude/skills/components/contact-section.md +101 -0
- package/.claude/skills/components/expanding-panel.md +156 -0
- package/.claude/skills/components/eyebrow-text.md +25 -0
- package/.claude/skills/components/hero-text.md +25 -0
- package/.claude/skills/components/layout-grid-by-cols.md +147 -0
- package/.claude/skills/components/layout-row.md +35 -0
- package/.claude/skills/components/link-text.md +33 -0
- package/.claude/skills/components/page-hero-highlights.md +224 -0
- package/.claude/skills/components/services-card.md +28 -0
- package/.claude/skills/components/services-section.md +25 -0
- package/.claude/skills/components/stepper-list.md +227 -0
- package/.claude/skills/css-grid-max-width-gutters.md +67 -0
- package/.claude/skills/index.md +14 -3
- package/.claude/skills/storybook-add-story.md +60 -0
- package/.claude/skills/testing-add-unit-test.md +56 -0
- package/app/assets/styles/setup/01.config/index.css +0 -1
- package/app/assets/styles/setup/03.theming/default/_dark.css +2 -2
- package/app/assets/styles/setup/04.elements/forms/02.typography.css +1 -0
- package/app/assets/styles/setup/05.typography/02.utility-classes/_font-classes-page-link.css +14 -14
- package/app/assets/styles/setup/index.css +0 -1
- package/app/components/01.atoms/card/CardCore.vue +92 -0
- package/app/components/01.atoms/card/stories/CardCore.stories.ts +132 -0
- package/app/components/01.atoms/card/tests/CardCore.spec.ts +207 -0
- package/app/components/01.atoms/card/tests/__snapshots__/CardCore.spec.ts.snap +43 -0
- package/app/components/01.atoms/content-wrappers/content-columns-2/ContentColumns2.vue +51 -0
- package/app/components/01.atoms/content-wrappers/content-columns-2/stories/ContentColumns2.stories.ts +110 -0
- package/app/components/01.atoms/content-wrappers/content-columns-2/tests/ContentColumns2.spec.ts +105 -0
- package/app/components/01.atoms/content-wrappers/content-columns-2/tests/__snapshots__/ContentColumns2.spec.ts.snap +14 -0
- package/app/components/01.atoms/content-wrappers/content-width/ContentWidth.vue +88 -0
- package/app/components/01.atoms/content-wrappers/content-width/stories/ContentWidth.stories.ts +362 -0
- package/app/components/01.atoms/content-wrappers/content-width/tests/ContentWidth.spec.ts +132 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/LayoutGridByCols.vue +71 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/stories/LayoutGridByCols.stories.ts +219 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/LayoutGridByCols.spec.ts +174 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/__snapshots__/LayoutGrid.spec.ts.snap +36 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/__snapshots__/LayoutGridByCols.spec.ts.snap +36 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/LayoutGridByWidth.vue +70 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/stories/LayoutGridByWidth.stories.ts +220 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/LayoutGridByWidth.spec.ts +174 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/__snapshots__/LayoutGrid.spec.ts.snap +36 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/__snapshots__/LayoutGridByCols.spec.ts.snap +36 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/__snapshots__/LayoutGridByWidth.spec.ts.snap +36 -0
- package/app/components/01.atoms/text-blocks/eyebrow-text/stories/EyebrowText.stories.ts +1 -1
- package/app/components/01.atoms/text-blocks/hero-text/stories/HeroText.stories.ts +1 -1
- package/app/components/01.atoms/text-blocks/link-text/stories/LinkText.stories.ts +1 -1
- package/app/components/02.molecules/contact-section/stories/ContactSection.stories.ts +5 -0
- package/app/components/02.molecules/contact-section/tests/ContactSection.spec.ts +15 -0
- package/app/components/02.molecules/contact-section/tests/ContactSection.vue +25 -17
- package/app/components/{accordian → 02.molecules/expandable/accordian}/stories/AccordianCore.stories.ts +1 -1
- package/app/components/02.molecules/expandable/expanding-panel/stories/ExpandingPanel.stories.ts +245 -0
- package/app/components/02.molecules/expandable/expanding-panel/tests/ExpandingPanel.spec.ts +351 -0
- package/app/components/02.molecules/expandable/expanding-panel/tests/__snapshots__/ExpandingPanel.spec.ts.snap +38 -0
- package/app/components/02.molecules/navigation/navigation-horizontal/NavigationHorizontal.vue +139 -0
- package/app/components/02.molecules/navigation/navigation-horizontal/NavigationHorizontalAdvanced.vue +172 -0
- package/app/components/02.molecules/profile-section/ProfileSection.vue +2 -3
- package/app/components/02.molecules/profile-section/tests/ProfileSection.spec.ts +2 -2
- package/app/components/02.molecules/stepper-list/StepperList.vue +131 -92
- package/app/components/02.molecules/stepper-list/stories/StepperList.stories.ts +31 -0
- package/app/components/02.molecules/stepper-list/tests/StepperList.spec.ts +24 -0
- package/app/components/02.molecules/stepper-list/tests/__snapshots__/StepperList.spec.ts.snap +22 -9
- package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +782 -0
- package/app/components/03.organisms/image-galleries/slider-gallery/stories/SliderGallery.stories.ts +233 -0
- package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +226 -0
- package/app/components/03.organisms/image-galleries/slider-gallery/tests/__snapshots__/SliderGallery.spec.ts.snap +69 -0
- package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +1 -1
- package/app/components/03.organisms/services/services-grids/ServicesSectionGrid.vue +1 -1
- package/app/components/03.organisms/services/services-section/ServicesSection.vue +2 -3
- package/app/components/04.templates/page-hero-highlights/PageHeroHighlights.vue +239 -0
- package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlights.stories.ts +404 -0
- package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlights.spec.ts +198 -0
- package/app/components/04.templates/page-hero-highlights/tests/__snapshots__/PageHeroHighlights.spec.ts.snap +19 -0
- package/app/components/container-glow/ContainerGlowCore.vue +20 -27
- package/app/components/forms/input-button/InputButtonCore.vue +105 -104
- package/app/components/glowing-border/stories/GlowingBorder.stories.ts +21 -21
- package/app/composables/useAriaLabelledById.ts +13 -0
- package/app/layouts/default.vue +8 -3
- package/app/pages/forms/examples/buttons/index.vue +6 -6
- package/app/pages/forms/examples/material/checkbox-radio-panels.vue +3 -3
- package/app/pages/forms/examples/material/text-fields.vue +607 -610
- package/app/pages/page-hero-highlights.vue +81 -0
- package/app/pages/ui/{display-card.vue → card-core.vue} +15 -15
- package/app/pages/ui/contact-section.vue +1 -1
- package/app/pages/ui/container-glow.vue +1 -1
- package/app/pages/ui/content-width.vue +126 -0
- package/app/pages/ui/glowing-border.vue +9 -9
- package/app/pages/ui/navigation/navigation-horizontal.vue +493 -0
- package/app/pages/ui/services/services-section/[slug].vue +3 -1
- package/nuxt.config.ts +4 -1
- package/package.json +2 -2
- package/app/assets/styles/setup/01.config/_basic-resets.css +0 -9
- package/app/components/content-columns/TwoColumns.vue +0 -59
- package/app/components/content-columns/stories/TwoColumns.stories.ts +0 -561
- package/app/components/content-containers/ContentContainer.vue +0 -89
- package/app/components/content-containers/stories/ContentContainer.stories.ts +0 -465
- package/app/components/content-grid/ContentGrid.vue +0 -85
- package/app/components/display-card/DisplayCard.vue +0 -122
- package/app/components/image-galleries/SliderGallery.vue +0 -786
- package/app/pages/ui/content-container.vue +0 -112
- /package/app/components/{accordian → 02.molecules/expandable/accordian}/AccordianCore.vue +0 -0
- /package/app/components/{accordian → 02.molecules/expandable/accordian}/tests/AccordianCore.spec.ts +0 -0
- /package/app/components/{accordian → 02.molecules/expandable/accordian}/tests/__snapshots__/AccordianCore.spec.ts.snap +0 -0
- /package/app/components/{expanding-panel → 02.molecules/expandable/expanding-panel}/ExpandingPanel.vue +0 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component
|
|
3
|
+
:is="tag"
|
|
4
|
+
class="layout-grid-by-width"
|
|
5
|
+
:aria-labelledby="ariaLabelledby"
|
|
6
|
+
:class="[elementClasses]"
|
|
7
|
+
>
|
|
8
|
+
<p v-if="ariaLabelledby" :id="headingId" class="sr-only">
|
|
9
|
+
{{ props.label || "If tag='section' then a label is required" }}
|
|
10
|
+
</p>
|
|
11
|
+
<div class="layout-grid-inner">
|
|
12
|
+
<template v-for="(_, name) in $slots" :key="name">
|
|
13
|
+
<slot :name="name"></slot>
|
|
14
|
+
</template>
|
|
15
|
+
</div>
|
|
16
|
+
</component>
|
|
17
|
+
</template>
|
|
18
|
+
|
|
19
|
+
<script setup lang="ts">
|
|
20
|
+
interface Props {
|
|
21
|
+
tag?: "div" | "section";
|
|
22
|
+
label?: string;
|
|
23
|
+
columnWidth?: string;
|
|
24
|
+
gap?: string;
|
|
25
|
+
singleColBelow?: string;
|
|
26
|
+
styleClassPassthrough?: string | string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
30
|
+
tag: "div",
|
|
31
|
+
label: "",
|
|
32
|
+
columnWidth: "300px",
|
|
33
|
+
gap: "1rem",
|
|
34
|
+
singleColBelow: "768px",
|
|
35
|
+
styleClassPassthrough: () => [],
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const { headingId, ariaLabelledby } = useAriaLabelledById(() => props.tag);
|
|
39
|
+
|
|
40
|
+
const { elementClasses, updateElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
41
|
+
|
|
42
|
+
watch(
|
|
43
|
+
() => props.styleClassPassthrough,
|
|
44
|
+
() => {
|
|
45
|
+
updateElementClasses(props.styleClassPassthrough);
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<style lang="css">
|
|
51
|
+
@layer components {
|
|
52
|
+
.layout-grid-by-width {
|
|
53
|
+
container-type: inline-size;
|
|
54
|
+
container-name: layoutGrid;
|
|
55
|
+
|
|
56
|
+
--_gap: v-bind(gap);
|
|
57
|
+
|
|
58
|
+
.layout-grid-inner {
|
|
59
|
+
display: grid;
|
|
60
|
+
grid-auto-flow: row;
|
|
61
|
+
gap: var(--_gap);
|
|
62
|
+
|
|
63
|
+
@container layoutGrid (width >= 768px) {
|
|
64
|
+
grid-template-columns: repeat(auto-fit, minmax(v-bind(columnWidth), 1fr));
|
|
65
|
+
gap: var(--_gap);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
</style>
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { computed } from "vue";
|
|
2
|
+
import LayoutGridByWidth from "../LayoutGridByWidth.vue";
|
|
3
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
4
|
+
|
|
5
|
+
type StoryArgs = InstanceType<typeof LayoutGridByWidth>["$props"] & { itemCount?: number };
|
|
6
|
+
|
|
7
|
+
const meta: Meta<StoryArgs> = {
|
|
8
|
+
title: "Atoms/Content Wrappers/Layout Grid By Width",
|
|
9
|
+
component: LayoutGridByWidth,
|
|
10
|
+
argTypes: {
|
|
11
|
+
tag: {
|
|
12
|
+
control: { type: "select" },
|
|
13
|
+
options: ["div", "section"],
|
|
14
|
+
description: "Semantic HTML tag for the root element — use section with a label for landmark regions",
|
|
15
|
+
table: { category: "Semantic" },
|
|
16
|
+
},
|
|
17
|
+
label: {
|
|
18
|
+
control: "text",
|
|
19
|
+
description:
|
|
20
|
+
"Accessible label for the grid — required when tag is section (rendered as a visually hidden element)",
|
|
21
|
+
table: { category: "Semantic" },
|
|
22
|
+
},
|
|
23
|
+
itemCount: {
|
|
24
|
+
control: { type: "range", min: 0, max: 18, step: 1 },
|
|
25
|
+
description: "Number of grid cells to render — must match the number of #item-{n} slots provided",
|
|
26
|
+
},
|
|
27
|
+
columnWidth: {
|
|
28
|
+
control: { type: "select" },
|
|
29
|
+
options: ["200px", "312px", "400px", "500px"],
|
|
30
|
+
description:
|
|
31
|
+
"String → value to set repeat minmax minimum value CSS string (e.g. '200px', '15rem') → repeat(auto-fill, minmax(value, 1fr)) wrapping columns.",
|
|
32
|
+
},
|
|
33
|
+
gap: {
|
|
34
|
+
control: "text",
|
|
35
|
+
description: "Grid gap — any valid CSS length (e.g. '1rem', '2.4rem', '16px')",
|
|
36
|
+
},
|
|
37
|
+
singleColBelow: {
|
|
38
|
+
control: "text",
|
|
39
|
+
description:
|
|
40
|
+
"Container width below which the grid collapses to a single column — any valid CSS length (e.g. '600px', '40rem'). Default '0px' means never collapse.",
|
|
41
|
+
},
|
|
42
|
+
styleClassPassthrough: {
|
|
43
|
+
table: { disable: true },
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
args: {
|
|
47
|
+
tag: "div",
|
|
48
|
+
itemCount: 6,
|
|
49
|
+
columnWidth: "312px",
|
|
50
|
+
gap: "1rem",
|
|
51
|
+
singleColBelow: "768px",
|
|
52
|
+
styleClassPassthrough: [],
|
|
53
|
+
},
|
|
54
|
+
parameters: {
|
|
55
|
+
docs: {
|
|
56
|
+
description: {
|
|
57
|
+
component:
|
|
58
|
+
"A CSS grid wrapper driven by props. Content is placed via dynamic named slots (#item-0, #item-1, …). Pass an integer to columnWidth for N equal columns, or a CSS width string for auto-fill behaviour. The grid collapses to a single column below singleColBelow using a CSS container query.",
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export default meta;
|
|
65
|
+
type Story = StoryObj<StoryArgs>;
|
|
66
|
+
|
|
67
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
const colours = [
|
|
70
|
+
{ bg: "#dbeafe", fg: "#1e40af" },
|
|
71
|
+
{ bg: "#dcfce7", fg: "#166534" },
|
|
72
|
+
{ bg: "#fef3c7", fg: "#92400e" },
|
|
73
|
+
{ bg: "#ede9fe", fg: "#5b21b6" },
|
|
74
|
+
{ bg: "#fce7f3", fg: "#9d174d" },
|
|
75
|
+
{ bg: "#ffedd5", fg: "#9a3412" },
|
|
76
|
+
{ bg: "#f0fdf4", fg: "#14532d" },
|
|
77
|
+
{ bg: "#fdf4ff", fg: "#7e22ce" },
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
const cell = (label: string, index: number, body = "Placeholder content for this grid cell.") =>
|
|
81
|
+
`<div style="padding: 2.4rem; background: ${colours[index % colours.length]?.bg}; border-radius: 0.8rem; color: ${colours[index % colours.length]?.fg}; font-family: sans-serif; height: 100%;">
|
|
82
|
+
<strong style="display: block; margin-bottom: 0.8rem;">${label}</strong>
|
|
83
|
+
<p style="margin: 0; font-size: 1.4rem; line-height: 1.6; opacity: 0.8;">${body}</p>
|
|
84
|
+
</div>`;
|
|
85
|
+
|
|
86
|
+
// Pre-generate max slots — the component only renders up to itemCount, extras are ignored.
|
|
87
|
+
const allSlots = Array.from(
|
|
88
|
+
{ length: 18 },
|
|
89
|
+
(_, i) => `<template #item-${i}>${cell(`Item ${i + 1}`, i)}</template>`
|
|
90
|
+
).join("\n ");
|
|
91
|
+
|
|
92
|
+
// Normalise args: explicitly maps each prop so no raw slider number leaks through to the component.
|
|
93
|
+
// colWidth: 0 → undefined (not set), N → "Nrem" CSS string.
|
|
94
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
95
|
+
const toStoryArgs = (args: any) =>
|
|
96
|
+
computed(() => ({
|
|
97
|
+
tag: args.tag as "div" | "section",
|
|
98
|
+
label: args.label as string,
|
|
99
|
+
itemCount: Number(args.itemCount),
|
|
100
|
+
columnWidth: args.columnWidth as string,
|
|
101
|
+
gap: args.gap as string,
|
|
102
|
+
singleColBelow: args.singleColBelow as string,
|
|
103
|
+
styleClassPassthrough: args.styleClassPassthrough as string[],
|
|
104
|
+
}));
|
|
105
|
+
|
|
106
|
+
// ─── Stories ──────────────────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
/** Default — equal columns. Adjust columns, colWidth and itemCount with the sliders. */
|
|
109
|
+
export const Default: Story = {
|
|
110
|
+
args: {
|
|
111
|
+
itemCount: 6,
|
|
112
|
+
columnWidth: "240px",
|
|
113
|
+
},
|
|
114
|
+
render: (args) => ({
|
|
115
|
+
components: { LayoutGridByWidth },
|
|
116
|
+
setup() {
|
|
117
|
+
return { storyArgs: toStoryArgs(args) };
|
|
118
|
+
},
|
|
119
|
+
template: `
|
|
120
|
+
<LayoutGridByWidth v-bind="storyArgs">
|
|
121
|
+
${allSlots}
|
|
122
|
+
</LayoutGridByWidth>
|
|
123
|
+
`,
|
|
124
|
+
}),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
/** Single column below a breakpoint — collapses to stacked layout on narrow containers. */
|
|
128
|
+
export const SingleColBelow: Story = {
|
|
129
|
+
name: "Single Column Below 600px",
|
|
130
|
+
args: {
|
|
131
|
+
itemCount: 6,
|
|
132
|
+
columnWidth: "300px",
|
|
133
|
+
singleColBelow: "600px",
|
|
134
|
+
},
|
|
135
|
+
render: (args) => ({
|
|
136
|
+
components: { LayoutGridByWidth },
|
|
137
|
+
setup() {
|
|
138
|
+
return { storyArgs: toStoryArgs(args) };
|
|
139
|
+
},
|
|
140
|
+
template: `
|
|
141
|
+
<LayoutGridByWidth v-bind="storyArgs">
|
|
142
|
+
${allSlots}
|
|
143
|
+
</LayoutGridByWidth>
|
|
144
|
+
`,
|
|
145
|
+
}),
|
|
146
|
+
parameters: {
|
|
147
|
+
docs: {
|
|
148
|
+
description: {
|
|
149
|
+
story:
|
|
150
|
+
"Below 600px container width the grid collapses to a single column via a CSS container query. Resize the canvas to see the switch.",
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
/** Custom gap — wider spacing between cells. */
|
|
157
|
+
export const CustomGap: Story = {
|
|
158
|
+
name: "Custom Gap",
|
|
159
|
+
args: {
|
|
160
|
+
itemCount: 6,
|
|
161
|
+
columnWidth: "300px",
|
|
162
|
+
gap: "3.2rem",
|
|
163
|
+
},
|
|
164
|
+
render: (args) => ({
|
|
165
|
+
components: { LayoutGridByWidth },
|
|
166
|
+
setup() {
|
|
167
|
+
return { storyArgs: toStoryArgs(args) };
|
|
168
|
+
},
|
|
169
|
+
template: `
|
|
170
|
+
<LayoutGridByWidth v-bind="storyArgs">
|
|
171
|
+
${allSlots}
|
|
172
|
+
</LayoutGridByWidth>
|
|
173
|
+
`,
|
|
174
|
+
}),
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/** Semantic section — tag='section' with a visually hidden label for accessibility. */
|
|
178
|
+
export const SemanticSection: Story = {
|
|
179
|
+
name: "Semantic Section",
|
|
180
|
+
args: {
|
|
181
|
+
tag: "section",
|
|
182
|
+
label: "Feature highlights",
|
|
183
|
+
itemCount: 6,
|
|
184
|
+
columnWidth: "300px",
|
|
185
|
+
},
|
|
186
|
+
render: (args) => ({
|
|
187
|
+
components: { LayoutGridByWidth },
|
|
188
|
+
setup() {
|
|
189
|
+
return { storyArgs: toStoryArgs(args) };
|
|
190
|
+
},
|
|
191
|
+
template: `
|
|
192
|
+
<LayoutGridByWidth v-bind="storyArgs">
|
|
193
|
+
${allSlots}
|
|
194
|
+
</LayoutGridByWidth>
|
|
195
|
+
`,
|
|
196
|
+
}),
|
|
197
|
+
parameters: {
|
|
198
|
+
docs: {
|
|
199
|
+
description: {
|
|
200
|
+
story:
|
|
201
|
+
"When tag is section, an aria-labelledby attribute is added and the label is rendered as a visually hidden element for screen readers.",
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
/** Zero items — renders an empty grid with no cells. */
|
|
208
|
+
export const ZeroItems: Story = {
|
|
209
|
+
name: "Zero Items",
|
|
210
|
+
args: {
|
|
211
|
+
itemCount: 0,
|
|
212
|
+
},
|
|
213
|
+
render: (args) => ({
|
|
214
|
+
components: { LayoutGridByWidth },
|
|
215
|
+
setup() {
|
|
216
|
+
return { storyArgs: toStoryArgs(args) };
|
|
217
|
+
},
|
|
218
|
+
template: `<LayoutGridByWidth v-bind="storyArgs" />`,
|
|
219
|
+
}),
|
|
220
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import LayoutGridByWidth from "../LayoutGridByWidth.vue";
|
|
4
|
+
|
|
5
|
+
describe("LayoutGridByWidth", () => {
|
|
6
|
+
// ─── Mount ───────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
it("mounts without error", async () => {
|
|
9
|
+
const wrapper = await mountSuspended(LayoutGridByWidth);
|
|
10
|
+
expect(wrapper.vm).toBeTruthy();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// ─── Snapshots ───────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
it("renders correct HTML structure (div, 2 items)", async () => {
|
|
16
|
+
const wrapper = await mountSuspended(LayoutGridByWidth, {
|
|
17
|
+
slots: {
|
|
18
|
+
"item-0": "<p>First</p>",
|
|
19
|
+
"item-1": "<p>Second</p>",
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("renders correct HTML structure (section with label)", async () => {
|
|
26
|
+
const wrapper = await mountSuspended(LayoutGridByWidth, {
|
|
27
|
+
props: { tag: "section", label: "Feature grid" },
|
|
28
|
+
});
|
|
29
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("renders correct HTML structure with styleClassPassthrough", async () => {
|
|
33
|
+
const wrapper = await mountSuspended(LayoutGridByWidth, {
|
|
34
|
+
props: { styleClassPassthrough: ["custom-class", "another-class"] },
|
|
35
|
+
});
|
|
36
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ─── Tag rendering ───────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
it("renders as <div> by default", async () => {
|
|
42
|
+
const wrapper = await mountSuspended(LayoutGridByWidth);
|
|
43
|
+
expect(wrapper.element.tagName).toBe("DIV");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("renders as <section> when tag='section'", async () => {
|
|
47
|
+
const wrapper = await mountSuspended(LayoutGridByWidth, {
|
|
48
|
+
props: { tag: "section" },
|
|
49
|
+
});
|
|
50
|
+
expect(wrapper.element.tagName).toBe("SECTION");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ─── Base class ──────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
it("always has the layout-grid class", async () => {
|
|
56
|
+
const wrapper = await mountSuspended(LayoutGridByWidth);
|
|
57
|
+
expect(wrapper.classes()).toContain("layout-grid-by-width");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ─── Inner div ───────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
it("renders a .layout-grid-inner div", async () => {
|
|
63
|
+
const wrapper = await mountSuspended(LayoutGridByWidth);
|
|
64
|
+
expect(wrapper.find(".layout-grid-inner").exists()).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// ─── Accessibility ───────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
it("renders sr-only label and aria-labelledby when tag is section", async () => {
|
|
70
|
+
const wrapper = await mountSuspended(LayoutGridByWidth, {
|
|
71
|
+
props: { tag: "section", label: "Card grid" },
|
|
72
|
+
});
|
|
73
|
+
const srOnly = wrapper.find(".sr-only");
|
|
74
|
+
expect(srOnly.exists()).toBe(true);
|
|
75
|
+
expect(srOnly.text()).toBe("Card grid");
|
|
76
|
+
expect(wrapper.attributes("aria-labelledby")).toBeTruthy();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("does not render sr-only label or aria-labelledby when tag is div", async () => {
|
|
80
|
+
const wrapper = await mountSuspended(LayoutGridByWidth, {
|
|
81
|
+
props: { tag: "div", label: "Ignored" },
|
|
82
|
+
});
|
|
83
|
+
expect(wrapper.find(".sr-only").exists()).toBe(false);
|
|
84
|
+
expect(wrapper.attributes("aria-labelledby")).toBeUndefined();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// ─── Dynamic slots ───────────────────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
it("renders slot content for each item inside the inner div", async () => {
|
|
90
|
+
const wrapper = await mountSuspended(LayoutGridByWidth, {
|
|
91
|
+
slots: {
|
|
92
|
+
"item-0": "<span>Alpha</span>",
|
|
93
|
+
"item-1": "<span>Beta</span>",
|
|
94
|
+
"item-2": "<span>Gamma</span>",
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
const inner = wrapper.find(".layout-grid-inner");
|
|
98
|
+
expect(inner.text()).toContain("Alpha");
|
|
99
|
+
expect(inner.text()).toContain("Beta");
|
|
100
|
+
expect(inner.text()).toContain("Gamma");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("renders no slot content when no slots are provided", async () => {
|
|
104
|
+
const wrapper = await mountSuspended(LayoutGridByWidth);
|
|
105
|
+
expect(wrapper.find(".layout-grid-inner").text().trim()).toBe("");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ─── styleClassPassthrough ───────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
it("applies a single styleClassPassthrough string", async () => {
|
|
111
|
+
const wrapper = await mountSuspended(LayoutGridByWidth, {
|
|
112
|
+
props: { styleClassPassthrough: "my-class" },
|
|
113
|
+
});
|
|
114
|
+
expect(wrapper.classes()).toContain("my-class");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("applies multiple styleClassPassthrough classes from an array", async () => {
|
|
118
|
+
const wrapper = await mountSuspended(LayoutGridByWidth, {
|
|
119
|
+
props: { styleClassPassthrough: ["class-a", "class-b"] },
|
|
120
|
+
});
|
|
121
|
+
expect(wrapper.classes()).toContain("class-a");
|
|
122
|
+
expect(wrapper.classes()).toContain("class-b");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// ─── columnWidth prop ─────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
it("accepts columnWidth without error", async () => {
|
|
128
|
+
const wrapper = await mountSuspended(LayoutGridByWidth, {
|
|
129
|
+
props: { columnWidth: "312px" },
|
|
130
|
+
});
|
|
131
|
+
expect(wrapper.vm).toBeTruthy();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ─── Other prop acceptance ────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
it("accepts gap prop without error", async () => {
|
|
137
|
+
const wrapper = await mountSuspended(LayoutGridByWidth, {
|
|
138
|
+
props: { gap: "2rem" },
|
|
139
|
+
});
|
|
140
|
+
expect(wrapper.vm).toBeTruthy();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("accepts singleColBelow prop without error", async () => {
|
|
144
|
+
const wrapper = await mountSuspended(LayoutGridByWidth, {
|
|
145
|
+
props: { singleColBelow: "600px" },
|
|
146
|
+
});
|
|
147
|
+
expect(wrapper.vm).toBeTruthy();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// ─── Combined ────────────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
it("renders correctly with all props and slots combined", async () => {
|
|
153
|
+
const wrapper = await mountSuspended(LayoutGridByWidth, {
|
|
154
|
+
props: {
|
|
155
|
+
tag: "section",
|
|
156
|
+
label: "Services",
|
|
157
|
+
columnWidth: "260px",
|
|
158
|
+
gap: "2rem",
|
|
159
|
+
singleColBelow: "768px",
|
|
160
|
+
styleClassPassthrough: ["services-grid"],
|
|
161
|
+
},
|
|
162
|
+
slots: {
|
|
163
|
+
"item-0": "<p>One</p>",
|
|
164
|
+
"item-1": "<p>Two</p>",
|
|
165
|
+
"item-2": "<p>Three</p>",
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
expect(wrapper.element.tagName).toBe("SECTION");
|
|
169
|
+
expect(wrapper.classes()).toContain("services-grid");
|
|
170
|
+
expect(wrapper.find(".layout-grid-inner").exists()).toBe(true);
|
|
171
|
+
expect(wrapper.text()).toContain("One");
|
|
172
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`LayoutGridByCols > renders correct HTML structure (div, 2 items) 1`] = `
|
|
4
|
+
"<div class="layout-grid">
|
|
5
|
+
<!--v-if-->
|
|
6
|
+
<div class="layout-grid-inner">
|
|
7
|
+
<p>First</p>
|
|
8
|
+
<p>Second</p>
|
|
9
|
+
</div>
|
|
10
|
+
</div>"
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
exports[`LayoutGridByCols > renders correct HTML structure (section with label) 1`] = `
|
|
14
|
+
"<section class="layout-grid" aria-labelledby="v-0-0">
|
|
15
|
+
<p id="v-0-0" class="sr-only">Feature grid</p>
|
|
16
|
+
<div class="layout-grid-inner"></div>
|
|
17
|
+
</section>"
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
exports[`LayoutGridByCols > renders correct HTML structure with styleClassPassthrough 1`] = `
|
|
21
|
+
"<div class="layout-grid custom-class another-class">
|
|
22
|
+
<!--v-if-->
|
|
23
|
+
<div class="layout-grid-inner"></div>
|
|
24
|
+
</div>"
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
exports[`LayoutGridByCols > renders correctly with all props and slots combined 1`] = `
|
|
28
|
+
"<section class="layout-grid services-grid" aria-labelledby="v-0-0" columns="3">
|
|
29
|
+
<p id="v-0-0" class="sr-only">Services</p>
|
|
30
|
+
<div class="layout-grid-inner">
|
|
31
|
+
<p>One</p>
|
|
32
|
+
<p>Two</p>
|
|
33
|
+
<p>Three</p>
|
|
34
|
+
</div>
|
|
35
|
+
</section>"
|
|
36
|
+
`;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`LayoutGridByCols > renders correct HTML structure (div, 2 items) 1`] = `
|
|
4
|
+
"<div class="layout-grid-by-width">
|
|
5
|
+
<!--v-if-->
|
|
6
|
+
<div class="layout-grid-inner">
|
|
7
|
+
<p>First</p>
|
|
8
|
+
<p>Second</p>
|
|
9
|
+
</div>
|
|
10
|
+
</div>"
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
exports[`LayoutGridByCols > renders correct HTML structure (section with label) 1`] = `
|
|
14
|
+
"<section class="layout-grid-by-width" aria-labelledby="v-0-0">
|
|
15
|
+
<p id="v-0-0" class="sr-only">Feature grid</p>
|
|
16
|
+
<div class="layout-grid-inner"></div>
|
|
17
|
+
</section>"
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
exports[`LayoutGridByCols > renders correct HTML structure with styleClassPassthrough 1`] = `
|
|
21
|
+
"<div class="layout-grid-by-width custom-class another-class">
|
|
22
|
+
<!--v-if-->
|
|
23
|
+
<div class="layout-grid-inner"></div>
|
|
24
|
+
</div>"
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
exports[`LayoutGridByCols > renders correctly with all props and slots combined 1`] = `
|
|
28
|
+
"<section class="layout-grid-by-width services-grid" aria-labelledby="v-0-0" columns="3">
|
|
29
|
+
<p id="v-0-0" class="sr-only">Services</p>
|
|
30
|
+
<div class="layout-grid-inner">
|
|
31
|
+
<p>One</p>
|
|
32
|
+
<p>Two</p>
|
|
33
|
+
<p>Three</p>
|
|
34
|
+
</div>
|
|
35
|
+
</section>"
|
|
36
|
+
`;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`LayoutGridByWidth > renders correct HTML structure (div, 2 items) 1`] = `
|
|
4
|
+
"<div class="layout-grid-by-width">
|
|
5
|
+
<!--v-if-->
|
|
6
|
+
<div class="layout-grid-inner">
|
|
7
|
+
<p>First</p>
|
|
8
|
+
<p>Second</p>
|
|
9
|
+
</div>
|
|
10
|
+
</div>"
|
|
11
|
+
`;
|
|
12
|
+
|
|
13
|
+
exports[`LayoutGridByWidth > renders correct HTML structure (section with label) 1`] = `
|
|
14
|
+
"<section class="layout-grid-by-width" aria-labelledby="v-0-0">
|
|
15
|
+
<p id="v-0-0" class="sr-only">Feature grid</p>
|
|
16
|
+
<div class="layout-grid-inner"></div>
|
|
17
|
+
</section>"
|
|
18
|
+
`;
|
|
19
|
+
|
|
20
|
+
exports[`LayoutGridByWidth > renders correct HTML structure with styleClassPassthrough 1`] = `
|
|
21
|
+
"<div class="layout-grid-by-width custom-class another-class">
|
|
22
|
+
<!--v-if-->
|
|
23
|
+
<div class="layout-grid-inner"></div>
|
|
24
|
+
</div>"
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
exports[`LayoutGridByWidth > renders correctly with all props and slots combined 1`] = `
|
|
28
|
+
"<section class="layout-grid-by-width services-grid" aria-labelledby="v-0-0">
|
|
29
|
+
<p id="v-0-0" class="sr-only">Services</p>
|
|
30
|
+
<div class="layout-grid-inner">
|
|
31
|
+
<p>One</p>
|
|
32
|
+
<p>Two</p>
|
|
33
|
+
<p>Three</p>
|
|
34
|
+
</div>
|
|
35
|
+
</section>"
|
|
36
|
+
`;
|
|
@@ -2,7 +2,7 @@ import EyebrowText from "../EyebrowText.vue";
|
|
|
2
2
|
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
3
|
|
|
4
4
|
const meta: Meta<typeof EyebrowText> = {
|
|
5
|
-
title: "Atoms/Text Blocks/
|
|
5
|
+
title: "Atoms/Text Blocks/Eyebrow Text",
|
|
6
6
|
component: EyebrowText,
|
|
7
7
|
argTypes: {
|
|
8
8
|
tag: {
|
|
@@ -10,6 +10,10 @@ const meta: Meta<typeof ContactSection> = {
|
|
|
10
10
|
options: ["div", "section", "article", "main"],
|
|
11
11
|
description: "HTML element to render as the root",
|
|
12
12
|
},
|
|
13
|
+
stepperIndicatorSize: {
|
|
14
|
+
control: "text",
|
|
15
|
+
description: "Size of the StepperList indicator bubble (any valid CSS length)",
|
|
16
|
+
},
|
|
13
17
|
styleClassPassthrough: {
|
|
14
18
|
control: "object",
|
|
15
19
|
description: "Additional CSS classes applied to the root element",
|
|
@@ -17,6 +21,7 @@ const meta: Meta<typeof ContactSection> = {
|
|
|
17
21
|
},
|
|
18
22
|
args: {
|
|
19
23
|
tag: "div",
|
|
24
|
+
stepperIndicatorSize: "3rem",
|
|
20
25
|
styleClassPassthrough: [],
|
|
21
26
|
},
|
|
22
27
|
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
3
|
import ContactSection from "./ContactSection.vue";
|
|
4
|
+
import StepperList from "../../stepper-list/StepperList.vue";
|
|
4
5
|
|
|
5
6
|
describe("ContactSection", () => {
|
|
6
7
|
// ─── Mount ───────────────────────────────────────────────────────────────
|
|
@@ -174,6 +175,20 @@ describe("ContactSection", () => {
|
|
|
174
175
|
expect(wrapper.classes()).toContain("contact-section");
|
|
175
176
|
});
|
|
176
177
|
|
|
178
|
+
// ─── stepperIndicatorSize ─────────────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
it("passes stepperIndicatorSize to the internal StepperList", async () => {
|
|
181
|
+
const wrapper = await mountSuspended(ContactSection, {
|
|
182
|
+
props: { stepperIndicatorSize: "4rem" },
|
|
183
|
+
});
|
|
184
|
+
expect(wrapper.findComponent(StepperList).props("indicatorSize")).toBe("4rem");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("uses the default stepperIndicatorSize of 3rem", async () => {
|
|
188
|
+
const wrapper = await mountSuspended(ContactSection);
|
|
189
|
+
expect(wrapper.findComponent(StepperList).props("indicatorSize")).toBe("3rem");
|
|
190
|
+
});
|
|
191
|
+
|
|
177
192
|
// ─── Combined ─────────────────────────────────────────────────────────────
|
|
178
193
|
|
|
179
194
|
it("renders correctly with all props and slots combined", async () => {
|