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,92 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component
|
|
3
|
+
:is="tag"
|
|
4
|
+
class="card-core"
|
|
5
|
+
:class="[variant, elementClasses, { 'has-dividers': hasDividers }, { 'no-outline': noOutline }]"
|
|
6
|
+
>
|
|
7
|
+
<template v-for="(_, name) in $slots" :key="name">
|
|
8
|
+
<div class="card-row" :class="`card-row-${name}`">
|
|
9
|
+
<slot :name="name"></slot>
|
|
10
|
+
</div>
|
|
11
|
+
</template>
|
|
12
|
+
</component>
|
|
13
|
+
</template>
|
|
14
|
+
|
|
15
|
+
<script setup lang="ts">
|
|
16
|
+
interface Props {
|
|
17
|
+
tag?: "div" | "section" | "article" | "aside" | "main" | "nav";
|
|
18
|
+
hasDividers?: boolean;
|
|
19
|
+
noOutline?: boolean;
|
|
20
|
+
variant?: "solid" | "subtle" | "soft" | "outline";
|
|
21
|
+
styleClassPassthrough?: string | string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
25
|
+
tag: "div",
|
|
26
|
+
hasDividers: false,
|
|
27
|
+
noOutline: false,
|
|
28
|
+
variant: "solid",
|
|
29
|
+
styleClassPassthrough: () => [],
|
|
30
|
+
});
|
|
31
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
32
|
+
|
|
33
|
+
watch(
|
|
34
|
+
() => props.styleClassPassthrough,
|
|
35
|
+
() => {
|
|
36
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<style lang="css">
|
|
42
|
+
@layer components {
|
|
43
|
+
.card-core {
|
|
44
|
+
--_inner-padding: 1rem;
|
|
45
|
+
--_background-color: white;
|
|
46
|
+
--_border-color: green;
|
|
47
|
+
--_border-width: 0.2rem;
|
|
48
|
+
--_box-shadow-color: transparent;
|
|
49
|
+
|
|
50
|
+
display: grid;
|
|
51
|
+
grid-auto-flow: row;
|
|
52
|
+
/* gap: 1rem; */
|
|
53
|
+
border-radius: 0.5rem;
|
|
54
|
+
overflow: hidden;
|
|
55
|
+
|
|
56
|
+
background-color: var(--_background-color, transparent);
|
|
57
|
+
border: var(--_border-width) solid var(--_border-color, transparent);
|
|
58
|
+
box-shadow: 0 0 0.4rem var(--_border-width) var(--_box-shadow-color, transparent);
|
|
59
|
+
|
|
60
|
+
&.no-outline {
|
|
61
|
+
--_border-width: 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
&.solid {
|
|
65
|
+
--_background-color: light-dark(var(--slate-00), var(--slate-10));
|
|
66
|
+
--_border-color: red;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
&.subtle {
|
|
70
|
+
--_background-color: color-mix(in oklab, light-dark(var(--slate-01), var(--slate-08)) 50%, transparent);
|
|
71
|
+
--_border-color: red;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
&.soft {
|
|
75
|
+
--_background-color: color-mix(in oklab, light-dark(var(--slate-01), var(--slate-08)) 20%, transparent);
|
|
76
|
+
--_box-shadow-color: color-mix(in oklab, light-dark(var(--slate-02), var(--slate-08)) 80%, transparent);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
&.outline {
|
|
80
|
+
--_background-color: transparent;
|
|
81
|
+
--_border-color: green;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
&.has-dividers {
|
|
85
|
+
.card-row + .card-row {
|
|
86
|
+
/* border-top: 0.2rem solid var(--_border-color); */
|
|
87
|
+
border-top: 0.2rem solid green;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
</style>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
+
import StorybookComponent from "../CardCore.vue";
|
|
3
|
+
|
|
4
|
+
interface CardCoreArgs {
|
|
5
|
+
tag: "div" | "section" | "article" | "aside" | "main" | "nav";
|
|
6
|
+
variant: "solid" | "subtle" | "soft" | "outline";
|
|
7
|
+
hasDividers: boolean;
|
|
8
|
+
noOutline: boolean;
|
|
9
|
+
styleClassPassthrough: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default {
|
|
13
|
+
title: "Atoms/Card/Card Core Dynamic",
|
|
14
|
+
component: StorybookComponent,
|
|
15
|
+
argTypes: {
|
|
16
|
+
tag: {
|
|
17
|
+
control: { type: "select" },
|
|
18
|
+
options: ["div", "section", "article", "aside", "main", "nav"],
|
|
19
|
+
description: "HTML tag to render",
|
|
20
|
+
table: { category: "Semantic" },
|
|
21
|
+
},
|
|
22
|
+
variant: {
|
|
23
|
+
control: { type: "select" },
|
|
24
|
+
options: ["solid", "subtle", "soft", "outline"],
|
|
25
|
+
description: "Visual style variant",
|
|
26
|
+
table: { category: "Appearance" },
|
|
27
|
+
},
|
|
28
|
+
hasDividers: {
|
|
29
|
+
control: { type: "boolean" },
|
|
30
|
+
description: "Add dividers between slot sections",
|
|
31
|
+
table: { category: "Appearance" },
|
|
32
|
+
},
|
|
33
|
+
noOutline: {
|
|
34
|
+
control: { type: "boolean" },
|
|
35
|
+
description: "Remove border outline",
|
|
36
|
+
table: { category: "Appearance" },
|
|
37
|
+
},
|
|
38
|
+
styleClassPassthrough: {
|
|
39
|
+
table: { disable: true },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
args: {
|
|
43
|
+
tag: "div",
|
|
44
|
+
variant: "solid",
|
|
45
|
+
hasDividers: false,
|
|
46
|
+
noOutline: false,
|
|
47
|
+
styleClassPassthrough: [],
|
|
48
|
+
},
|
|
49
|
+
parameters: {
|
|
50
|
+
docs: {
|
|
51
|
+
description: {
|
|
52
|
+
component:
|
|
53
|
+
"A dynamic display card that renders whatever named slots are provided. Any number of slots can be passed and each is wrapped in its own div.",
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
} as Meta<CardCoreArgs>;
|
|
58
|
+
|
|
59
|
+
const Template: StoryFn<CardCoreArgs> = (args) => ({
|
|
60
|
+
components: { StorybookComponent },
|
|
61
|
+
setup() {
|
|
62
|
+
return { args };
|
|
63
|
+
},
|
|
64
|
+
template: `
|
|
65
|
+
<div style="padding: 40px; max-width: 480px;">
|
|
66
|
+
<StorybookComponent
|
|
67
|
+
:tag="args.tag"
|
|
68
|
+
:variant="args.variant"
|
|
69
|
+
:has-dividers="args.hasDividers"
|
|
70
|
+
:no-outline="args.noOutline"
|
|
71
|
+
:style-class-passthrough="args.styleClassPassthrough"
|
|
72
|
+
>
|
|
73
|
+
<template #header>
|
|
74
|
+
<div style="padding: 1.6rem; border-bottom: 1px solid transparent;">
|
|
75
|
+
<p style="margin: 0; font-size: 1.2rem; text-transform: uppercase; letter-spacing: 0.08em; opacity: 0.6;">Category</p>
|
|
76
|
+
<h2 style="margin: 0.4rem 0 0; font-size: 2rem; font-weight: 600;">Card Title</h2>
|
|
77
|
+
</div>
|
|
78
|
+
</template>
|
|
79
|
+
<template #media>
|
|
80
|
+
<div style="aspect-ratio: 16/9; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-size: 1.4rem;">
|
|
81
|
+
Media Slot
|
|
82
|
+
</div>
|
|
83
|
+
</template>
|
|
84
|
+
<template #body>
|
|
85
|
+
<div style="padding: 1.6rem;">
|
|
86
|
+
<p style="margin: 0; line-height: 1.6; opacity: 0.8;">
|
|
87
|
+
This is the body content. It can contain any markup — paragraphs, lists, or other components.
|
|
88
|
+
</p>
|
|
89
|
+
</div>
|
|
90
|
+
</template>
|
|
91
|
+
<template #footer>
|
|
92
|
+
<div style="padding: 1.2rem 1.6rem; display: flex; gap: 0.8rem; justify-content: flex-end;">
|
|
93
|
+
<button style="padding: 0.8rem 1.6rem; border-radius: 0.4rem; border: 1px solid currentColor; background: transparent; cursor: pointer; opacity: 0.7;">Cancel</button>
|
|
94
|
+
<button style="padding: 0.8rem 1.6rem; border-radius: 0.4rem; border: none; background: #667eea; color: white; cursor: pointer;">Confirm</button>
|
|
95
|
+
</div>
|
|
96
|
+
</template>
|
|
97
|
+
</StorybookComponent>
|
|
98
|
+
</div>
|
|
99
|
+
`,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export const Default = Template.bind({});
|
|
103
|
+
Default.args = {};
|
|
104
|
+
|
|
105
|
+
export const Subtle = Template.bind({});
|
|
106
|
+
Subtle.args = { variant: "subtle" };
|
|
107
|
+
|
|
108
|
+
export const Soft = Template.bind({});
|
|
109
|
+
Soft.args = { variant: "soft" };
|
|
110
|
+
|
|
111
|
+
export const Outline = Template.bind({});
|
|
112
|
+
Outline.args = { variant: "outline" };
|
|
113
|
+
|
|
114
|
+
export const WithDividers = Template.bind({});
|
|
115
|
+
WithDividers.args = { hasDividers: true };
|
|
116
|
+
WithDividers.parameters = {
|
|
117
|
+
docs: {
|
|
118
|
+
description: {
|
|
119
|
+
story: "Adds visual dividers between each slot section.",
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const NoOutline = Template.bind({});
|
|
125
|
+
NoOutline.args = { noOutline: true };
|
|
126
|
+
NoOutline.parameters = {
|
|
127
|
+
docs: {
|
|
128
|
+
description: {
|
|
129
|
+
story: "Card without a border or box shadow.",
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
};
|
|
@@ -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
|
+
};
|