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
|
@@ -174,6 +174,66 @@ template: `
|
|
|
174
174
|
This keeps the ID wiring self-contained inside the component — the parent story just
|
|
175
175
|
consumes what the slot exposes, rather than generating its own ID.
|
|
176
176
|
|
|
177
|
+
### Extra controls that are not component props
|
|
178
|
+
|
|
179
|
+
Use when you want a Storybook control that sets something other than a component prop — e.g. a CSS custom property toggle.
|
|
180
|
+
|
|
181
|
+
`Meta<typeof Component>` is strict: its `argTypes`/`args` keys must match the component's actual props. Adding extras causes a TypeScript error. The fix is a `StoryArgs` type that covers both:
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import { computed } from "vue"; // ← must be explicit in .ts files (not auto-imported)
|
|
185
|
+
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
186
|
+
import ComponentName from "../ComponentName.vue";
|
|
187
|
+
|
|
188
|
+
type StoryArgs = {
|
|
189
|
+
// mirror the component props you want controls for
|
|
190
|
+
tag?: "div" | "section";
|
|
191
|
+
// plus any extras
|
|
192
|
+
headerBackground?: string;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const meta: Meta<StoryArgs> = { // ← StoryArgs, not typeof ComponentName
|
|
196
|
+
title: "...",
|
|
197
|
+
component: ComponentName,
|
|
198
|
+
argTypes: {
|
|
199
|
+
headerBackground: { control: "color", description: "Sets --my-header-bg" },
|
|
200
|
+
},
|
|
201
|
+
args: { headerBackground: "" },
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
export default meta;
|
|
205
|
+
type Story = StoryObj<typeof ComponentName>; // ← still strict for individual stories
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Strip extra args before `v-bind` using a `useStorySetup` helper in `setup()`:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
function useStorySetup(args: StoryArgs) {
|
|
212
|
+
const bgStyles = computed(() => ({
|
|
213
|
+
...(args.headerBackground ? { "--my-header-bg": args.headerBackground } : {}),
|
|
214
|
+
}));
|
|
215
|
+
const componentArgs = computed(() => {
|
|
216
|
+
const { headerBackground: _h, ...rest } = args;
|
|
217
|
+
return rest;
|
|
218
|
+
});
|
|
219
|
+
return { bgStyles, componentArgs };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export const Default: Story = {
|
|
223
|
+
render: (args: StoryArgs) => ({
|
|
224
|
+
components: { ComponentName },
|
|
225
|
+
setup() { return useStorySetup(args); },
|
|
226
|
+
template: `<ComponentName v-bind="componentArgs" :style="bgStyles" />`,
|
|
227
|
+
}),
|
|
228
|
+
};
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Key points:
|
|
232
|
+
|
|
233
|
+
- `computed` is **not** auto-imported in `.ts` story files — import it explicitly from `"vue"`.
|
|
234
|
+
- Extra args must be stripped before `v-bind` — spreading unknown keys onto a component makes them unknown HTML attributes.
|
|
235
|
+
- CSS custom properties set via `:style` on the component root are picked up by `var()` in the component's scoped CSS.
|
|
236
|
+
|
|
177
237
|
## Notes
|
|
178
238
|
|
|
179
239
|
- Use `table: { category: "..." }` in `argTypes` when a component has many props — it groups
|
|
@@ -165,6 +165,34 @@ it("exposes headingId via scoped slot", async () => {
|
|
|
165
165
|
- Always `afterEach(() => wrapper?.unmount())` to prevent test leaks.
|
|
166
166
|
- Use a `createWrapper` helper to keep individual tests short.
|
|
167
167
|
- Include at least one snapshot test per meaningful visual state.
|
|
168
|
+
- `nextTick` is **not** auto-imported in test files — always import it explicitly: `import { nextTick } from "vue"`.
|
|
169
|
+
|
|
170
|
+
## Fake timers
|
|
171
|
+
|
|
172
|
+
`vitest.setup.ts` calls `vi.useFakeTimers()` globally and resets in `afterEach`. **Never** call `vi.useFakeTimers()`, `vi.useRealTimers()`, or `vi.runAllTimers()` inside a test file — it conflicts with the global setup and can cause infinite loops or bleed between tests.
|
|
173
|
+
|
|
174
|
+
Use `vi.advanceTimersByTime(ms)` to move time forward by a specific amount. Avoid `vi.runAllTimers()` — it fires all queued timers including any that re-queue themselves, which loops infinitely.
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
// ✅
|
|
178
|
+
vi.advanceTimersByTime(500);
|
|
179
|
+
await nextTick();
|
|
180
|
+
|
|
181
|
+
// ❌ — can loop infinitely if a timer re-queues itself
|
|
182
|
+
vi.runAllTimers();
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Hyphenated prop attributes in tests
|
|
186
|
+
|
|
187
|
+
When a component uses a hyphenated Vue prop like `:tab-index` or `:aria-label`, Vue renders it as the literal hyphenated DOM attribute. Assert with the hyphenated form — not the camelCase equivalent:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
// ✅ — prop :tab-index renders as the DOM attribute "tab-index"
|
|
191
|
+
expect(wrapper.find(".el").attributes("tab-index")).toBe("2");
|
|
192
|
+
|
|
193
|
+
// ❌ — "tabindex" won't match
|
|
194
|
+
expect(wrapper.find(".el").attributes("tabindex")).toBe("2");
|
|
195
|
+
```
|
|
168
196
|
|
|
169
197
|
## Snapshot testing
|
|
170
198
|
|
|
@@ -195,6 +223,34 @@ const style = (el.element as HTMLElement).style;
|
|
|
195
223
|
expect(style.getPropertyValue("--custom-prop")).toBe("expected-value");
|
|
196
224
|
```
|
|
197
225
|
|
|
226
|
+
**`v-bind()` in CSS — root component only.** Vue's `v-bind(propName)` in a component's `<style>` block sets `--v-bind-propName` on that component's root element in JSDOM. This only works when the component under test **is** the root wrapper. When the component is rendered as a **child** inside a parent, JSDOM does not apply those inline styles — `getPropertyValue` returns `""`.
|
|
227
|
+
|
|
228
|
+
```ts
|
|
229
|
+
// ❌ Won't work — StepperList is a child; JSDOM doesn't apply its v-bind() styles
|
|
230
|
+
const list = wrapper.find(".stepper-list");
|
|
231
|
+
expect((list.element as HTMLElement).style.getPropertyValue("--v-bind-indicatorSize")).toBe("4rem");
|
|
232
|
+
|
|
233
|
+
// ✅ Use findComponent + props() to test prop pass-through to a child component
|
|
234
|
+
expect(wrapper.findComponent(StepperList).props("indicatorSize")).toBe("4rem");
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
## Testing prop pass-through to child components
|
|
238
|
+
|
|
239
|
+
When a parent component forwards a prop to a child, use `findComponent` + `.props()` rather than inspecting the DOM:
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
import ChildComponent from "../../child/ChildComponent.vue";
|
|
243
|
+
|
|
244
|
+
it("passes myProp to ChildComponent", async () => {
|
|
245
|
+
const wrapper = await mountSuspended(ParentComponent, {
|
|
246
|
+
props: { myProp: "value" },
|
|
247
|
+
});
|
|
248
|
+
expect(wrapper.findComponent(ChildComponent).props("myProp")).toBe("value");
|
|
249
|
+
});
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Import the child component directly in the test file — it is not auto-imported there.
|
|
253
|
+
|
|
198
254
|
## Mocking browser APIs
|
|
199
255
|
|
|
200
256
|
Mock before the `describe` block if the component uses ResizeObserver, IntersectionObserver, etc.:
|
|
@@ -78,8 +78,8 @@
|
|
|
78
78
|
/* ===========================================
|
|
79
79
|
ANCHOR LINK VARIABLES
|
|
80
80
|
=========================================== */
|
|
81
|
-
--colour-link-default: var(--blue-
|
|
82
|
-
--colour-link-hover: var(--blue-
|
|
81
|
+
--colour-link-default: var(--blue-02);
|
|
82
|
+
--colour-link-hover: var(--blue-03);
|
|
83
83
|
|
|
84
84
|
/* ===========================================
|
|
85
85
|
FORM INPUT VARIABLES
|
package/app/assets/styles/setup/05.typography/02.utility-classes/_font-classes-page-link.css
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
.page-link-large {
|
|
2
|
-
color: var(--
|
|
2
|
+
color: var(--colour-link-default);
|
|
3
3
|
font-size: var(--step-6);
|
|
4
4
|
font-weight: 400;
|
|
5
5
|
font-variation-settings: "wght" 400;
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
.page-link-large-semibold {
|
|
11
|
-
color: var(--
|
|
11
|
+
color: var(--colour-link-default);
|
|
12
12
|
font-size: var(--step-6);
|
|
13
13
|
font-weight: 600;
|
|
14
14
|
font-variation-settings: "wght" 600;
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
.page-link-medium {
|
|
20
|
-
color: var(--
|
|
20
|
+
color: var(--colour-link-default);
|
|
21
21
|
font-size: var(--step-5);
|
|
22
22
|
font-weight: 400;
|
|
23
23
|
font-variation-settings: "wght" 400;
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
.page-link-medium-semibold {
|
|
29
|
-
color: var(--
|
|
29
|
+
color: var(--colour-link-default);
|
|
30
30
|
font-size: var(--step-5);
|
|
31
31
|
font-weight: 600;
|
|
32
32
|
font-variation-settings: "wght" 600;
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
.page-link-normal {
|
|
38
|
-
color: var(--
|
|
38
|
+
color: var(--colour-link-default);
|
|
39
39
|
font-size: var(--step-4);
|
|
40
40
|
font-weight: 400;
|
|
41
41
|
font-variation-settings: "wght" 400;
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
.page-link-normal-semibold {
|
|
47
|
-
color: var(--
|
|
47
|
+
color: var(--colour-link-default);
|
|
48
48
|
font-size: var(--step-4);
|
|
49
49
|
font-weight: 600;
|
|
50
50
|
font-variation-settings: "wght" 600;
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
.page-link-small {
|
|
56
|
-
color: var(--
|
|
56
|
+
color: var(--colour-link-default);
|
|
57
57
|
font-size: var(--step-3);
|
|
58
58
|
font-weight: 400;
|
|
59
59
|
font-variation-settings: "wght" 400;
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
.page-link-small-semibold {
|
|
65
|
-
color: var(--
|
|
65
|
+
color: var(--colour-link-default);
|
|
66
66
|
font-size: var(--step-3);
|
|
67
67
|
font-weight: 600;
|
|
68
68
|
font-variation-settings: "wght" 600;
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
.page-link-xsmall {
|
|
74
|
-
color: var(--
|
|
74
|
+
color: var(--colour-link-default);
|
|
75
75
|
font-size: var(--step-2);
|
|
76
76
|
font-weight: 400;
|
|
77
77
|
font-variation-settings: "wght" 400;
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
.page-link-xsmall-semibold {
|
|
83
|
-
color: var(--
|
|
83
|
+
color: var(--colour-link-default);
|
|
84
84
|
font-size: var(--step-2);
|
|
85
85
|
font-weight: 600;
|
|
86
86
|
font-variation-settings: "wght" 600;
|
|
@@ -100,16 +100,16 @@
|
|
|
100
100
|
.page-link-xsmall-semibold {
|
|
101
101
|
margin: 0;
|
|
102
102
|
&:visited {
|
|
103
|
-
color: var(--
|
|
103
|
+
color: var(--colour-link-default);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
&:hover {
|
|
107
|
-
color: var(--
|
|
107
|
+
color: var(--colour-link-hover);
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
&:focus-visible {
|
|
111
|
-
color: var(--
|
|
112
|
-
outline: 2px solid var(--
|
|
111
|
+
color: var(--colour-link-hover);
|
|
112
|
+
outline: 2px solid var(--colour-link-default);
|
|
113
113
|
outline-offset: 3px;
|
|
114
114
|
border-radius: 4px;
|
|
115
115
|
}
|
|
@@ -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
|
+
};
|