srcdev-nuxt-components 9.0.15 → 9.0.17
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-export-types.md +61 -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 +15 -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 +162 -0
- package/app/components/02.molecules/navigation/navigation-horizontal/stories/NavigationHorizontal.stories.ts +373 -0
- package/app/components/02.molecules/navigation/navigation-horizontal/tests/NavigationHorizontal.spec.ts +152 -0
- package/app/components/02.molecules/navigation/navigation-horizontal/tests/__snapshots__/NavigationHorizontal.spec.ts.snap +17 -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 +484 -0
- package/app/pages/ui/services/services-section/[slug].vue +3 -1
- package/app/types/components/index.ts +1 -0
- package/app/types/components/navigation-horizontal.d.ts +11 -0
- 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,207 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import CardCore from "../CardCore.vue";
|
|
4
|
+
|
|
5
|
+
describe("CardCore", () => {
|
|
6
|
+
// ─── Mount ───────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
it("mounts without error", async () => {
|
|
9
|
+
const wrapper = await mountSuspended(CardCore);
|
|
10
|
+
expect(wrapper.vm).toBeTruthy();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// ─── Snapshots ───────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
it("renders correct HTML structure (default)", async () => {
|
|
16
|
+
const wrapper = await mountSuspended(CardCore, {
|
|
17
|
+
slots: {
|
|
18
|
+
header: "<h2>Title</h2>",
|
|
19
|
+
body: "<p>Content</p>",
|
|
20
|
+
footer: "<p>Footer</p>",
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("renders correct HTML structure (article, subtle, dividers)", async () => {
|
|
27
|
+
const wrapper = await mountSuspended(CardCore, {
|
|
28
|
+
props: { tag: "article", variant: "subtle", hasDividers: true },
|
|
29
|
+
slots: {
|
|
30
|
+
header: "<h2>Title</h2>",
|
|
31
|
+
body: "<p>Content</p>",
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("renders correct HTML structure with styleClassPassthrough", async () => {
|
|
38
|
+
const wrapper = await mountSuspended(CardCore, {
|
|
39
|
+
props: { styleClassPassthrough: ["featured-card"] },
|
|
40
|
+
});
|
|
41
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// ─── Tag rendering ───────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
it("renders as <div> by default", async () => {
|
|
47
|
+
const wrapper = await mountSuspended(CardCore);
|
|
48
|
+
expect(wrapper.element.tagName).toBe("DIV");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it.each([
|
|
52
|
+
["section", "SECTION"],
|
|
53
|
+
["article", "ARTICLE"],
|
|
54
|
+
["aside", "ASIDE"],
|
|
55
|
+
["main", "MAIN"],
|
|
56
|
+
["nav", "NAV"],
|
|
57
|
+
] as const)("renders as <%s> when tag='%s'", async (tag, expected) => {
|
|
58
|
+
const wrapper = await mountSuspended(CardCore, { props: { tag } });
|
|
59
|
+
expect(wrapper.element.tagName).toBe(expected);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ─── Base class ──────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
it("always has the card-core class", async () => {
|
|
65
|
+
const wrapper = await mountSuspended(CardCore);
|
|
66
|
+
expect(wrapper.classes()).toContain("card-core");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// ─── Variant ─────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
it("applies solid class by default", async () => {
|
|
72
|
+
const wrapper = await mountSuspended(CardCore);
|
|
73
|
+
expect(wrapper.classes()).toContain("solid");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it.each(["solid", "subtle", "soft", "outline"] as const)(
|
|
77
|
+
"applies %s variant class",
|
|
78
|
+
async (variant) => {
|
|
79
|
+
const wrapper = await mountSuspended(CardCore, { props: { variant } });
|
|
80
|
+
expect(wrapper.classes()).toContain(variant);
|
|
81
|
+
}
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// ─── hasDividers ─────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
it("does not have has-dividers class by default", async () => {
|
|
87
|
+
const wrapper = await mountSuspended(CardCore);
|
|
88
|
+
expect(wrapper.classes()).not.toContain("has-dividers");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("applies has-dividers class when hasDividers=true", async () => {
|
|
92
|
+
const wrapper = await mountSuspended(CardCore, {
|
|
93
|
+
props: { hasDividers: true },
|
|
94
|
+
});
|
|
95
|
+
expect(wrapper.classes()).toContain("has-dividers");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ─── noOutline ───────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
it("does not have no-outline class by default", async () => {
|
|
101
|
+
const wrapper = await mountSuspended(CardCore);
|
|
102
|
+
expect(wrapper.classes()).not.toContain("no-outline");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("applies no-outline class when noOutline=true", async () => {
|
|
106
|
+
const wrapper = await mountSuspended(CardCore, {
|
|
107
|
+
props: { noOutline: true },
|
|
108
|
+
});
|
|
109
|
+
expect(wrapper.classes()).toContain("no-outline");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ─── Dynamic slots ───────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
it("renders a .card-row wrapper for each slot", async () => {
|
|
115
|
+
const wrapper = await mountSuspended(CardCore, {
|
|
116
|
+
slots: {
|
|
117
|
+
header: "<h2>Header</h2>",
|
|
118
|
+
body: "<p>Body</p>",
|
|
119
|
+
footer: "<p>Footer</p>",
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
expect(wrapper.findAll(".card-row")).toHaveLength(3);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("applies card-row-{name} class derived from the slot name", async () => {
|
|
126
|
+
const wrapper = await mountSuspended(CardCore, {
|
|
127
|
+
slots: {
|
|
128
|
+
header: "<h2>Header</h2>",
|
|
129
|
+
media: "<img src='x.jpg' alt='' />",
|
|
130
|
+
footer: "<p>Footer</p>",
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
expect(wrapper.find(".card-row-header").exists()).toBe(true);
|
|
134
|
+
expect(wrapper.find(".card-row-media").exists()).toBe(true);
|
|
135
|
+
expect(wrapper.find(".card-row-footer").exists()).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("renders slot content inside the correct card-row", async () => {
|
|
139
|
+
const wrapper = await mountSuspended(CardCore, {
|
|
140
|
+
slots: {
|
|
141
|
+
header: "<h2>My Title</h2>",
|
|
142
|
+
body: "<p>My Body</p>",
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
expect(wrapper.find(".card-row-header").text()).toBe("My Title");
|
|
146
|
+
expect(wrapper.find(".card-row-body").text()).toBe("My Body");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("renders no card-row elements when no slots are provided", async () => {
|
|
150
|
+
const wrapper = await mountSuspended(CardCore);
|
|
151
|
+
expect(wrapper.findAll(".card-row")).toHaveLength(0);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// ─── styleClassPassthrough ───────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
it("applies a single styleClassPassthrough string", async () => {
|
|
157
|
+
const wrapper = await mountSuspended(CardCore, {
|
|
158
|
+
props: { styleClassPassthrough: "featured-card" },
|
|
159
|
+
});
|
|
160
|
+
expect(wrapper.classes()).toContain("featured-card");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("applies multiple styleClassPassthrough classes from an array", async () => {
|
|
164
|
+
const wrapper = await mountSuspended(CardCore, {
|
|
165
|
+
props: { styleClassPassthrough: ["featured-card", "highlight"] },
|
|
166
|
+
});
|
|
167
|
+
expect(wrapper.classes()).toContain("featured-card");
|
|
168
|
+
expect(wrapper.classes()).toContain("highlight");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("updates classes when styleClassPassthrough prop changes", async () => {
|
|
172
|
+
const wrapper = await mountSuspended(CardCore, {
|
|
173
|
+
props: { styleClassPassthrough: ["original"] },
|
|
174
|
+
});
|
|
175
|
+
expect(wrapper.classes()).toContain("original");
|
|
176
|
+
await wrapper.setProps({ styleClassPassthrough: ["updated"] });
|
|
177
|
+
expect(wrapper.classes()).not.toContain("original");
|
|
178
|
+
expect(wrapper.classes()).toContain("updated");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ─── Combined ────────────────────────────────────────────────────────────
|
|
182
|
+
|
|
183
|
+
it("renders correctly with all props and slots combined", async () => {
|
|
184
|
+
const wrapper = await mountSuspended(CardCore, {
|
|
185
|
+
props: {
|
|
186
|
+
tag: "article",
|
|
187
|
+
variant: "soft",
|
|
188
|
+
hasDividers: true,
|
|
189
|
+
noOutline: true,
|
|
190
|
+
styleClassPassthrough: ["featured-card"],
|
|
191
|
+
},
|
|
192
|
+
slots: {
|
|
193
|
+
header: "<h2>Title</h2>",
|
|
194
|
+
media: "<img src='x.jpg' alt='' />",
|
|
195
|
+
body: "<p>Content</p>",
|
|
196
|
+
footer: "<p>Footer</p>",
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
expect(wrapper.element.tagName).toBe("ARTICLE");
|
|
200
|
+
expect(wrapper.classes()).toContain("soft");
|
|
201
|
+
expect(wrapper.classes()).toContain("has-dividers");
|
|
202
|
+
expect(wrapper.classes()).toContain("no-outline");
|
|
203
|
+
expect(wrapper.classes()).toContain("featured-card");
|
|
204
|
+
expect(wrapper.findAll(".card-row")).toHaveLength(4);
|
|
205
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`CardCore > renders correct HTML structure (article, subtle, dividers) 1`] = `
|
|
4
|
+
"<article class="card-core subtle has-dividers">
|
|
5
|
+
<div class="card-row card-row-header">
|
|
6
|
+
<h2>Title</h2>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="card-row card-row-body">
|
|
9
|
+
<p>Content</p>
|
|
10
|
+
</div>
|
|
11
|
+
</article>"
|
|
12
|
+
`;
|
|
13
|
+
|
|
14
|
+
exports[`CardCore > renders correct HTML structure (default) 1`] = `
|
|
15
|
+
"<div class="card-core solid">
|
|
16
|
+
<div class="card-row card-row-header">
|
|
17
|
+
<h2>Title</h2>
|
|
18
|
+
</div>
|
|
19
|
+
<div class="card-row card-row-body">
|
|
20
|
+
<p>Content</p>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="card-row card-row-footer">
|
|
23
|
+
<p>Footer</p>
|
|
24
|
+
</div>
|
|
25
|
+
</div>"
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
exports[`CardCore > renders correct HTML structure with styleClassPassthrough 1`] = `"<div class="card-core solid featured-card"></div>"`;
|
|
29
|
+
|
|
30
|
+
exports[`CardCore > renders correctly with all props and slots combined 1`] = `
|
|
31
|
+
"<article class="card-core soft featured-card has-dividers no-outline">
|
|
32
|
+
<div class="card-row card-row-header">
|
|
33
|
+
<h2>Title</h2>
|
|
34
|
+
</div>
|
|
35
|
+
<div class="card-row card-row-media"><img src="x.jpg" alt=""></div>
|
|
36
|
+
<div class="card-row card-row-body">
|
|
37
|
+
<p>Content</p>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="card-row card-row-footer">
|
|
40
|
+
<p>Footer</p>
|
|
41
|
+
</div>
|
|
42
|
+
</article>"
|
|
43
|
+
`;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component :is="tag" class="content-columns-2" :class="[elementClasses]" :data-testid="dataTestid">
|
|
3
|
+
<div class="inner">
|
|
4
|
+
<div v-if="hasSlot1" class="col-1">
|
|
5
|
+
<slot name="slot1"></slot>
|
|
6
|
+
</div>
|
|
7
|
+
<div v-if="hasSlot2" class="col-2">
|
|
8
|
+
<slot name="slot2"></slot>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
</component>
|
|
12
|
+
</template>
|
|
13
|
+
|
|
14
|
+
<script setup lang="ts">
|
|
15
|
+
interface Props {
|
|
16
|
+
tag?: "div" | "section" | "article" | "main";
|
|
17
|
+
dataTestid?: string;
|
|
18
|
+
styleClassPassthrough?: string | string[];
|
|
19
|
+
}
|
|
20
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
21
|
+
tag: "div",
|
|
22
|
+
dataTestid: "content-columns-2",
|
|
23
|
+
styleClassPassthrough: () => [],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const slots = useSlots();
|
|
27
|
+
const hasSlot1 = computed(() => !!slots.slot1);
|
|
28
|
+
const hasSlot2 = computed(() => !!slots.slot2);
|
|
29
|
+
|
|
30
|
+
const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<style lang="css">
|
|
34
|
+
@layer components {
|
|
35
|
+
.content-columns-2 {
|
|
36
|
+
container-type: inline-size;
|
|
37
|
+
container-name: contentColumns2;
|
|
38
|
+
|
|
39
|
+
.inner {
|
|
40
|
+
display: grid;
|
|
41
|
+
grid-auto-flow: row;
|
|
42
|
+
gap: 1.6rem;
|
|
43
|
+
|
|
44
|
+
@container contentColumns2 (width >= 768px) {
|
|
45
|
+
gap: 1.6rem;
|
|
46
|
+
grid-template-columns: repeat(2, 1fr);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
</style>
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import ContentColumns2 from "../ContentColumns2.vue";
|
|
2
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof ContentColumns2> = {
|
|
5
|
+
title: "Atoms/Content Wrappers/ContentColumns2",
|
|
6
|
+
component: ContentColumns2,
|
|
7
|
+
argTypes: {
|
|
8
|
+
tag: {
|
|
9
|
+
control: { type: "select" },
|
|
10
|
+
options: ["div", "section", "article", "main"],
|
|
11
|
+
description: "Semantic HTML tag for the root element",
|
|
12
|
+
table: { category: "Semantic" },
|
|
13
|
+
},
|
|
14
|
+
dataTestid: {
|
|
15
|
+
control: { type: "text" },
|
|
16
|
+
description: "Test ID for the root element",
|
|
17
|
+
table: { category: "Testing" },
|
|
18
|
+
},
|
|
19
|
+
styleClassPassthrough: {
|
|
20
|
+
table: { disable: true },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
parameters: {
|
|
24
|
+
docs: {
|
|
25
|
+
description: {
|
|
26
|
+
component:
|
|
27
|
+
"A responsive two-column layout using CSS Container Queries. Stacks to a single column below 768px container width, switching to equal-width columns above that threshold.",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default meta;
|
|
34
|
+
type Story = StoryObj<typeof ContentColumns2>;
|
|
35
|
+
|
|
36
|
+
const placeholder = (label: string, bg: string, color: string) =>
|
|
37
|
+
`<div style="padding: 2.4rem; background: ${bg}; border-radius: 0.8rem; color: ${color}; font-family: sans-serif;">
|
|
38
|
+
<strong style="display:block; margin-bottom: 0.8rem;">${label}</strong>
|
|
39
|
+
<p style="margin: 0; font-size: 1.4rem; line-height: 1.6; opacity: 0.8;">Placeholder content for this column slot.</p>
|
|
40
|
+
</div>`;
|
|
41
|
+
|
|
42
|
+
export const Default: Story = {
|
|
43
|
+
args: {
|
|
44
|
+
tag: "div",
|
|
45
|
+
dataTestid: "content-columns-2",
|
|
46
|
+
},
|
|
47
|
+
render: (args) => ({
|
|
48
|
+
components: { ContentColumns2 },
|
|
49
|
+
setup() {
|
|
50
|
+
return { args };
|
|
51
|
+
},
|
|
52
|
+
template: `
|
|
53
|
+
<ContentColumns2 v-bind="args">
|
|
54
|
+
<template #slot1>${placeholder("Column 1", "#dbeafe", "#1e40af")}</template>
|
|
55
|
+
<template #slot2>${placeholder("Column 2", "#dcfce7", "#166534")}</template>
|
|
56
|
+
</ContentColumns2>
|
|
57
|
+
`,
|
|
58
|
+
}),
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const SingleColumn: Story = {
|
|
62
|
+
args: {
|
|
63
|
+
tag: "div",
|
|
64
|
+
dataTestid: "content-columns-2",
|
|
65
|
+
},
|
|
66
|
+
render: (args) => ({
|
|
67
|
+
components: { ContentColumns2 },
|
|
68
|
+
setup() {
|
|
69
|
+
return { args };
|
|
70
|
+
},
|
|
71
|
+
template: `
|
|
72
|
+
<ContentColumns2 v-bind="args">
|
|
73
|
+
<template #slot1>${placeholder("Column 1 only", "#fef3c7", "#92400e")}</template>
|
|
74
|
+
</ContentColumns2>
|
|
75
|
+
`,
|
|
76
|
+
}),
|
|
77
|
+
parameters: {
|
|
78
|
+
docs: {
|
|
79
|
+
description: {
|
|
80
|
+
story: "When only one slot is provided, that column renders alone with no empty space reserved for the other.",
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const SemanticSection: Story = {
|
|
87
|
+
args: {
|
|
88
|
+
tag: "section",
|
|
89
|
+
dataTestid: "content-columns-2",
|
|
90
|
+
},
|
|
91
|
+
render: (args) => ({
|
|
92
|
+
components: { ContentColumns2 },
|
|
93
|
+
setup() {
|
|
94
|
+
return { args };
|
|
95
|
+
},
|
|
96
|
+
template: `
|
|
97
|
+
<ContentColumns2 v-bind="args">
|
|
98
|
+
<template #slot1>${placeholder("Left", "#ede9fe", "#5b21b6")}</template>
|
|
99
|
+
<template #slot2>${placeholder("Right", "#fce7f3", "#9d174d")}</template>
|
|
100
|
+
</ContentColumns2>
|
|
101
|
+
`,
|
|
102
|
+
}),
|
|
103
|
+
parameters: {
|
|
104
|
+
docs: {
|
|
105
|
+
description: {
|
|
106
|
+
story: "Rendered as a <code><section></code> for thematic content groupings.",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
};
|
package/app/components/01.atoms/content-wrappers/content-columns-2/tests/ContentColumns2.spec.ts
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import ContentColumns2 from "../ContentColumns2.vue";
|
|
4
|
+
|
|
5
|
+
describe("ContentColumns2", () => {
|
|
6
|
+
it("mounts without error", async () => {
|
|
7
|
+
const wrapper = await mountSuspended(ContentColumns2);
|
|
8
|
+
expect(wrapper.vm).toBeTruthy();
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("renders correct HTML structure", async () => {
|
|
12
|
+
const wrapper = await mountSuspended(ContentColumns2, {
|
|
13
|
+
slots: {
|
|
14
|
+
slot1: "<p>Column 1</p>",
|
|
15
|
+
slot2: "<p>Column 2</p>",
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("applies the default data-testid", async () => {
|
|
22
|
+
const wrapper = await mountSuspended(ContentColumns2);
|
|
23
|
+
expect(wrapper.find("[data-testid='content-columns-2']").exists()).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("applies a custom data-testid", async () => {
|
|
27
|
+
const wrapper = await mountSuspended(ContentColumns2, {
|
|
28
|
+
props: { dataTestid: "my-layout" },
|
|
29
|
+
});
|
|
30
|
+
expect(wrapper.find("[data-testid='my-layout']").exists()).toBe(true);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("applies styleClassPassthrough classes", async () => {
|
|
34
|
+
const wrapper = await mountSuspended(ContentColumns2, {
|
|
35
|
+
props: { styleClassPassthrough: ["custom-class", "another-class"] },
|
|
36
|
+
});
|
|
37
|
+
const el = wrapper.find(".content-columns-2");
|
|
38
|
+
expect(el.classes()).toContain("custom-class");
|
|
39
|
+
expect(el.classes()).toContain("another-class");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("applies styleClassPassthrough as a string", async () => {
|
|
43
|
+
const wrapper = await mountSuspended(ContentColumns2, {
|
|
44
|
+
props: { styleClassPassthrough: "string-class" },
|
|
45
|
+
});
|
|
46
|
+
expect(wrapper.find(".content-columns-2").classes()).toContain("string-class");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("renders slot1 content when slot1 is provided", async () => {
|
|
50
|
+
const wrapper = await mountSuspended(ContentColumns2, {
|
|
51
|
+
slots: { slot1: "<p>Left content</p>" },
|
|
52
|
+
});
|
|
53
|
+
expect(wrapper.find(".col-1").exists()).toBe(true);
|
|
54
|
+
expect(wrapper.find(".col-1").text()).toBe("Left content");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("renders slot2 content when slot2 is provided", async () => {
|
|
58
|
+
const wrapper = await mountSuspended(ContentColumns2, {
|
|
59
|
+
slots: { slot2: "<p>Right content</p>" },
|
|
60
|
+
});
|
|
61
|
+
expect(wrapper.find(".col-2").exists()).toBe(true);
|
|
62
|
+
expect(wrapper.find(".col-2").text()).toBe("Right content");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("does not render col-1 when slot1 is not provided", async () => {
|
|
66
|
+
const wrapper = await mountSuspended(ContentColumns2, {
|
|
67
|
+
slots: { slot2: "<p>Right only</p>" },
|
|
68
|
+
});
|
|
69
|
+
expect(wrapper.find(".col-1").exists()).toBe(false);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("does not render col-2 when slot2 is not provided", async () => {
|
|
73
|
+
const wrapper = await mountSuspended(ContentColumns2, {
|
|
74
|
+
slots: { slot1: "<p>Left only</p>" },
|
|
75
|
+
});
|
|
76
|
+
expect(wrapper.find(".col-2").exists()).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("renders as a div by default", async () => {
|
|
80
|
+
const wrapper = await mountSuspended(ContentColumns2);
|
|
81
|
+
expect(wrapper.element.tagName).toBe("DIV");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it.each(["section", "article", "main"] as const)("renders as <%s> when tag prop is set", async (tag) => {
|
|
85
|
+
const wrapper = await mountSuspended(ContentColumns2, { props: { tag } });
|
|
86
|
+
expect(wrapper.element.tagName).toBe(tag.toUpperCase());
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("renders neither column when no slots are provided", async () => {
|
|
90
|
+
const wrapper = await mountSuspended(ContentColumns2);
|
|
91
|
+
expect(wrapper.find(".col-1").exists()).toBe(false);
|
|
92
|
+
expect(wrapper.find(".col-2").exists()).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("renders both columns when both slots are provided", async () => {
|
|
96
|
+
const wrapper = await mountSuspended(ContentColumns2, {
|
|
97
|
+
slots: {
|
|
98
|
+
slot1: "<p>Left</p>",
|
|
99
|
+
slot2: "<p>Right</p>",
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
expect(wrapper.find(".col-1").exists()).toBe(true);
|
|
103
|
+
expect(wrapper.find(".col-2").exists()).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`ContentColumns2 > renders correct HTML structure 1`] = `
|
|
4
|
+
"<div class="content-columns-2" data-testid="content-columns-2">
|
|
5
|
+
<div class="inner">
|
|
6
|
+
<div class="col-1">
|
|
7
|
+
<p>Column 1</p>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="col-2">
|
|
10
|
+
<p>Column 2</p>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
</div>"
|
|
14
|
+
`;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component
|
|
3
|
+
:is="tag"
|
|
4
|
+
:id="id"
|
|
5
|
+
class="content-width-wrapper"
|
|
6
|
+
:class="elementClasses"
|
|
7
|
+
:tab-index="isLandmark ? 0 : null"
|
|
8
|
+
:aria-label="isLandmark ? 'Content Width Landmark' : undefined"
|
|
9
|
+
>
|
|
10
|
+
<div class="content-width" :class="justifyContent">
|
|
11
|
+
<div class="content-width-inner">
|
|
12
|
+
<slot name="default"></slot>
|
|
13
|
+
</div>
|
|
14
|
+
</div>
|
|
15
|
+
</component>
|
|
16
|
+
</template>
|
|
17
|
+
|
|
18
|
+
<script setup lang="ts">
|
|
19
|
+
interface Props {
|
|
20
|
+
tag?: "div" | "section" | "article" | "aside" | "header" | "footer" | "main" | "nav";
|
|
21
|
+
label?: string;
|
|
22
|
+
isLandmark?: boolean;
|
|
23
|
+
justifyContent?: "start" | "center" | "end";
|
|
24
|
+
styleClassPassthrough?: string | string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
28
|
+
tag: "div",
|
|
29
|
+
label: "",
|
|
30
|
+
isLandmark: false,
|
|
31
|
+
justifyContent: "center",
|
|
32
|
+
styleClassPassthrough: () => [],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const id = useId();
|
|
36
|
+
|
|
37
|
+
const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<style lang="css">
|
|
41
|
+
@layer components {
|
|
42
|
+
.content-width-wrapper {
|
|
43
|
+
container-type: inline-size;
|
|
44
|
+
container-name: content-width;
|
|
45
|
+
|
|
46
|
+
.content-width {
|
|
47
|
+
--gutter: 16px;
|
|
48
|
+
--content-max-width: auto;
|
|
49
|
+
--justify-content: initial;
|
|
50
|
+
|
|
51
|
+
display: grid;
|
|
52
|
+
|
|
53
|
+
grid-template-columns:
|
|
54
|
+
[gutter-start]
|
|
55
|
+
var(--gutter)
|
|
56
|
+
[content-start]
|
|
57
|
+
var(--content-max-width)
|
|
58
|
+
[content-end]
|
|
59
|
+
var(--gutter)
|
|
60
|
+
[gutter-end];
|
|
61
|
+
|
|
62
|
+
justify-content: var(--justify-content);
|
|
63
|
+
box-sizing: border-box;
|
|
64
|
+
|
|
65
|
+
@container content-width (width >= 1092px) {
|
|
66
|
+
--gutter: 0;
|
|
67
|
+
--content-max-width: 1064px;
|
|
68
|
+
--justify-content: center;
|
|
69
|
+
|
|
70
|
+
&.start {
|
|
71
|
+
--gutter: 16px;
|
|
72
|
+
--justify-content: start;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
&.end {
|
|
76
|
+
--gutter: 16px;
|
|
77
|
+
--justify-content: end;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.content-width-inner {
|
|
82
|
+
grid-column: content;
|
|
83
|
+
background-color: var(--page-bg);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
</style>
|