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
|
@@ -53,6 +53,34 @@ The `actions` slot receives `serviceData` as a scoped prop so the consumer can c
|
|
|
53
53
|
</ServicesCard>
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
+
## Local style override scaffold
|
|
57
|
+
|
|
58
|
+
When consuming this component, scaffold a style block using `styleClassPassthrough`. Delete the block if unused.
|
|
59
|
+
|
|
60
|
+
See [component-local-style-override.md](../component-local-style-override.md) for the full pattern.
|
|
61
|
+
|
|
62
|
+
```vue
|
|
63
|
+
<ServicesCard :style-class-passthrough="['my-card']" :service-data="service">
|
|
64
|
+
...
|
|
65
|
+
</ServicesCard>
|
|
66
|
+
|
|
67
|
+
<style>
|
|
68
|
+
/* ─── ServicesCard local overrides ─────────────────────────────────
|
|
69
|
+
Colours, borders, geometry only — do not override behaviour.
|
|
70
|
+
Delete this block if no overrides are needed.
|
|
71
|
+
─────────────────────────────────────────────────────────────────── */
|
|
72
|
+
.services-card {
|
|
73
|
+
&.my-card {
|
|
74
|
+
/* Geometry — image */
|
|
75
|
+
/* .image-wrapper { border-radius: 1.2rem; } */
|
|
76
|
+
|
|
77
|
+
/* Colours */
|
|
78
|
+
/* .description { color: var(--brand-text-secondary); } */
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
</style>
|
|
82
|
+
```
|
|
83
|
+
|
|
56
84
|
## Notes
|
|
57
85
|
|
|
58
86
|
- Component is auto-imported in Nuxt — no import needed.
|
|
@@ -104,6 +104,31 @@ Both slots receive `serviceData` as a scoped prop.
|
|
|
104
104
|
</ServicesSection>
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
+
## Local style override scaffold
|
|
108
|
+
|
|
109
|
+
When consuming this component, scaffold a style block using `styleClassPassthrough`. Delete the block if unused.
|
|
110
|
+
|
|
111
|
+
See [component-local-style-override.md](../component-local-style-override.md) for the full pattern.
|
|
112
|
+
|
|
113
|
+
```vue
|
|
114
|
+
<ServicesSection :style-class-passthrough="['my-section']" :service-data="service">
|
|
115
|
+
...
|
|
116
|
+
</ServicesSection>
|
|
117
|
+
|
|
118
|
+
<style>
|
|
119
|
+
/* ─── ServicesSection local overrides ──────────────────────────────
|
|
120
|
+
Colours, borders, geometry only — do not override behaviour.
|
|
121
|
+
Delete this block if no overrides are needed.
|
|
122
|
+
─────────────────────────────────────────────────────────────────── */
|
|
123
|
+
.services-section {
|
|
124
|
+
&.my-section {
|
|
125
|
+
/* Geometry — image wrapper */
|
|
126
|
+
/* .image-wrapper { border-radius: 1.2rem; } */
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
</style>
|
|
130
|
+
```
|
|
131
|
+
|
|
107
132
|
## Notes
|
|
108
133
|
|
|
109
134
|
- Component is auto-imported in Nuxt — no import needed.
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
# StepperList Component
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`StepperList` is a numbered/stepped list component where each item has a visual indicator (a counter bubble or custom icon) and a content area. Indicators can optionally be connected by a vertical line between them. The number of items is controlled by the `itemCount` prop — content is filled via **dynamic named slots**.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Dynamic slot pattern
|
|
10
|
+
|
|
11
|
+
`StepperList` generates slots based on `itemCount`. For `itemCount="3"` the following slots exist:
|
|
12
|
+
|
|
13
|
+
| Slot | Purpose |
|
|
14
|
+
|------|---------|
|
|
15
|
+
| `#item-0` | Content of the first list item |
|
|
16
|
+
| `#item-1` | Content of the second list item |
|
|
17
|
+
| `#item-2` | Content of the third list item |
|
|
18
|
+
| `#indicator-0` | Custom indicator replacing the default counter bubble for item 0 |
|
|
19
|
+
| `#indicator-1` | Custom indicator replacing the default counter bubble for item 1 |
|
|
20
|
+
| … | … |
|
|
21
|
+
|
|
22
|
+
**Rules:**
|
|
23
|
+
- Slots are always **zero-indexed**: first item = `item-0`, last item = `item-{itemCount - 1}`.
|
|
24
|
+
- `item-{n}` slots are required if you want content in each row.
|
|
25
|
+
- `indicator-{n}` slots are **optional**. When omitted, a CSS counter bubble is rendered automatically. When provided, the custom content replaces the counter entirely — the CSS counter is hidden for that item only.
|
|
26
|
+
- You can mix custom indicators and counter bubbles across items in the same list.
|
|
27
|
+
- Always set `itemCount` to match the number of `#item-*` slots you provide.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Props reference
|
|
32
|
+
|
|
33
|
+
| Prop | Type | Default | Notes |
|
|
34
|
+
|------|------|---------|-------|
|
|
35
|
+
| `itemCount` | `number` | — | **Required.** Controls how many `<li>` elements are rendered and which slots exist. |
|
|
36
|
+
| `tag` | `"ul" \| "ol"` | `"ul"` | Use `"ol"` for sequentially meaningful content (recipes, instructions). |
|
|
37
|
+
| `indicatorAlignment` | `"top" \| "center"` | `"top"` | Vertical alignment of the indicator relative to each item's content. `"center"` suits single-line items; `"top"` suits multi-line content. |
|
|
38
|
+
| `indicatorVariant` | `"disc" \| "circle" \| "square"` | `"disc"` | Visual style of the auto-generated counter bubble. |
|
|
39
|
+
| `indicatorSize` | `string` | `"3rem"` | Any valid CSS length — controls the width/height of the indicator bubble. |
|
|
40
|
+
| `connected` | `boolean` | `true` | Draws a vertical connector line between indicators. JS measures indicator positions to calculate the line height precisely. |
|
|
41
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root element. |
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Usage examples
|
|
46
|
+
|
|
47
|
+
### Basic counter list (no indicator slots)
|
|
48
|
+
|
|
49
|
+
```vue
|
|
50
|
+
<StepperList :itemCount="3" indicatorVariant="disc">
|
|
51
|
+
<template #item-0><p>Plan your project goals</p></template>
|
|
52
|
+
<template #item-1><p>Set up your development environment</p></template>
|
|
53
|
+
<template #item-2><p>Ship and monitor</p></template>
|
|
54
|
+
</StepperList>
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Counter bubbles are rendered automatically via CSS — no `#indicator-*` slots needed.
|
|
58
|
+
|
|
59
|
+
### Ordered list with connectors
|
|
60
|
+
|
|
61
|
+
```vue
|
|
62
|
+
<StepperList tag="ol" :itemCount="4" :connected="true" indicatorVariant="circle">
|
|
63
|
+
<template #item-0><p>Preheat the oven to 180°C</p></template>
|
|
64
|
+
<template #item-1><p>Sift together the flour and baking powder</p></template>
|
|
65
|
+
<template #item-2><p>Cream the butter and sugar until fluffy</p></template>
|
|
66
|
+
<template #item-3><p>Bake for 25–30 minutes</p></template>
|
|
67
|
+
</StepperList>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Custom indicator icons (SVG)
|
|
71
|
+
|
|
72
|
+
```vue
|
|
73
|
+
<StepperList :itemCount="3">
|
|
74
|
+
<template #indicator-0>
|
|
75
|
+
<svg class="indicator-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
76
|
+
<polyline points="20 6 9 17 4 12" />
|
|
77
|
+
</svg>
|
|
78
|
+
</template>
|
|
79
|
+
<template #item-0><p>Identity verified</p></template>
|
|
80
|
+
|
|
81
|
+
<template #indicator-1>
|
|
82
|
+
<svg class="indicator-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
83
|
+
<polyline points="20 6 9 17 4 12" />
|
|
84
|
+
</svg>
|
|
85
|
+
</template>
|
|
86
|
+
<template #item-1><p>Payment confirmed</p></template>
|
|
87
|
+
|
|
88
|
+
<template #indicator-2>
|
|
89
|
+
<svg class="indicator-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
90
|
+
<circle cx="12" cy="12" r="10" /><line x1="12" y1="8" x2="12" y2="12" /><line x1="12" y1="16" x2="12.01" y2="16" />
|
|
91
|
+
</svg>
|
|
92
|
+
</template>
|
|
93
|
+
<template #item-2><p>Awaiting email confirmation</p></template>
|
|
94
|
+
</StepperList>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Custom indicator SVGs should use the `indicator-icon` class — it applies `color: var(--stepper-list-icon)` and sizes the icon to match `indicatorSize`.
|
|
98
|
+
|
|
99
|
+
### Mixed — custom icons for completed steps, counters for pending
|
|
100
|
+
|
|
101
|
+
```vue
|
|
102
|
+
<StepperList :itemCount="4" :connected="true">
|
|
103
|
+
<!-- Completed steps: custom checkmark icon -->
|
|
104
|
+
<template #indicator-0>
|
|
105
|
+
<svg class="indicator-icon" ...>...</svg>
|
|
106
|
+
</template>
|
|
107
|
+
<template #item-0><p>Account created</p></template>
|
|
108
|
+
|
|
109
|
+
<template #indicator-1>
|
|
110
|
+
<svg class="indicator-icon" ...>...</svg>
|
|
111
|
+
</template>
|
|
112
|
+
<template #item-1><p>Email verified</p></template>
|
|
113
|
+
|
|
114
|
+
<!-- Pending steps: no indicator slot → CSS counter bubble shown -->
|
|
115
|
+
<template #item-2><p>Choose a plan</p></template>
|
|
116
|
+
<template #item-3><p>Start building</p></template>
|
|
117
|
+
</StepperList>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Rich item content (heading + body)
|
|
121
|
+
|
|
122
|
+
```vue
|
|
123
|
+
<StepperList tag="ol" :itemCount="3">
|
|
124
|
+
<template #item-0>
|
|
125
|
+
<div>
|
|
126
|
+
<strong>Create your account</strong>
|
|
127
|
+
<p style="margin: 0.25rem 0 0">Enter your email and choose a password.</p>
|
|
128
|
+
</div>
|
|
129
|
+
</template>
|
|
130
|
+
<template #item-1>
|
|
131
|
+
<div>
|
|
132
|
+
<strong>Verify your email</strong>
|
|
133
|
+
<p style="margin: 0.25rem 0 0">Click the link we sent to your inbox.</p>
|
|
134
|
+
</div>
|
|
135
|
+
</template>
|
|
136
|
+
<template #item-2>
|
|
137
|
+
<div>
|
|
138
|
+
<strong>Start building</strong>
|
|
139
|
+
<p style="margin: 0.25rem 0 0">You're all set — open your dashboard.</p>
|
|
140
|
+
</div>
|
|
141
|
+
</template>
|
|
142
|
+
</StepperList>
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## CSS custom properties
|
|
148
|
+
|
|
149
|
+
Override these in your consuming component or theme to restyle the indicators and connectors:
|
|
150
|
+
|
|
151
|
+
| Property | Used by |
|
|
152
|
+
|----------|---------|
|
|
153
|
+
| `--stepper-list-counter-disc-background` | Disc variant counter bubble background |
|
|
154
|
+
| `--stepper-list-counter-disc-text` | Disc variant counter number colour |
|
|
155
|
+
| `--stepper-list-counter-disc-border` | Disc variant counter border colour |
|
|
156
|
+
| `--stepper-list-counter-circle-background` | Circle variant counter bubble background |
|
|
157
|
+
| `--stepper-list-counter-circle-text` | Circle variant counter number colour |
|
|
158
|
+
| `--stepper-list-counter-circle-border` | Circle variant counter border colour |
|
|
159
|
+
| `--stepper-list-counter-square-background` | Square variant counter bubble background |
|
|
160
|
+
| `--stepper-list-counter-square-text` | Square variant counter number colour |
|
|
161
|
+
| `--stepper-list-counter-square-border` | Square variant counter border colour |
|
|
162
|
+
| `--stepper-list-connector-color` | Connector line colour (defaults to `currentColor`) |
|
|
163
|
+
| `--stepper-list-icon` | Icon colour for custom `indicator-icon` SVGs |
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Connector behaviour
|
|
168
|
+
|
|
169
|
+
- Connectors are drawn as `::after` pseudo-elements on each `<li>` except the last.
|
|
170
|
+
- JS (`ResizeObserver`) measures the bottom of each indicator and the top of the next to set `--_connector-top` and `--_connector-height` precisely, keeping the line flush between bubbles regardless of content height.
|
|
171
|
+
- Before JS runs (SSR / initial paint), CSS fallback values approximate the correct position for `indicatorAlignment="top"`.
|
|
172
|
+
- If `connected="false"`, no connectors are rendered and JS measurement is skipped.
|
|
173
|
+
- Connectors recalculate automatically when `itemCount`, `connected`, or `indicatorAlignment` props change.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Local style override scaffold
|
|
178
|
+
|
|
179
|
+
When consuming this component, scaffold a style block using `styleClassPassthrough`. Delete the block if unused.
|
|
180
|
+
|
|
181
|
+
See [component-local-style-override.md](../component-local-style-override.md) for the full pattern.
|
|
182
|
+
|
|
183
|
+
```vue
|
|
184
|
+
<StepperList :style-class-passthrough="['my-stepper']" :item-count="3">
|
|
185
|
+
...
|
|
186
|
+
</StepperList>
|
|
187
|
+
|
|
188
|
+
<style>
|
|
189
|
+
/* ─── StepperList local overrides ──────────────────────────────────
|
|
190
|
+
Colours, borders, geometry only — do not override behaviour.
|
|
191
|
+
Delete this block if no overrides are needed.
|
|
192
|
+
─────────────────────────────────────────────────────────────────── */
|
|
193
|
+
.stepper-list {
|
|
194
|
+
&.my-stepper {
|
|
195
|
+
/* Counter bubble — disc variant */
|
|
196
|
+
/* --stepper-list-counter-disc-background: var(--brand-primary); */
|
|
197
|
+
/* --stepper-list-counter-disc-text: white; */
|
|
198
|
+
/* --stepper-list-counter-disc-border: transparent; */
|
|
199
|
+
|
|
200
|
+
/* Counter bubble — circle variant */
|
|
201
|
+
/* --stepper-list-counter-circle-background: transparent; */
|
|
202
|
+
/* --stepper-list-counter-circle-text: var(--brand-primary); */
|
|
203
|
+
/* --stepper-list-counter-circle-border: var(--brand-primary); */
|
|
204
|
+
|
|
205
|
+
/* Counter bubble — square variant */
|
|
206
|
+
/* --stepper-list-counter-square-background: var(--brand-primary); */
|
|
207
|
+
/* --stepper-list-counter-square-text: white; */
|
|
208
|
+
/* --stepper-list-counter-square-border: transparent; */
|
|
209
|
+
|
|
210
|
+
/* Custom indicator icon colour */
|
|
211
|
+
/* --stepper-list-icon: var(--brand-primary); */
|
|
212
|
+
|
|
213
|
+
/* Connector line */
|
|
214
|
+
/* --stepper-list-connector-color: var(--brand-primary); */
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
</style>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## Notes
|
|
223
|
+
|
|
224
|
+
- Always keep `itemCount` in sync with the number of `#item-*` slots you provide — mismatches will render empty `<li>` rows.
|
|
225
|
+
- Slot names are **zero-indexed** — `#item-0` not `#item-1`.
|
|
226
|
+
- The `indicator-icon` class is defined inside the component CSS and sizes the SVG to `var(--_counter-size)` (driven by `indicatorSize`). Always add it to custom SVGs.
|
|
227
|
+
- Auto-imported in Nuxt — no manual import needed.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# CSS Grid — Max Width via Growing Gutters
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
A pattern for capping the width of a central grid column without breaking full-bleed backgrounds. Instead of capping the column with `max-width`, the gutters grow to enforce the constraint. The centre column stays `1fr` and the layout never breaks.
|
|
6
|
+
|
|
7
|
+
## The problem with capping the centre column
|
|
8
|
+
|
|
9
|
+
Using `minmax(0, 1064px)` on the centre column caps its width but doesn't distribute the leftover space — it simply goes unused. Full-bleed backgrounds on adjacent rows break, and you lose the ability to use `subgrid`.
|
|
10
|
+
|
|
11
|
+
## The pattern
|
|
12
|
+
|
|
13
|
+
Grow the gutters instead:
|
|
14
|
+
|
|
15
|
+
```css
|
|
16
|
+
grid-template-columns: max(MIN_GUTTER, (100% - MAX_WIDTH) / 2) 1fr max(MIN_GUTTER, (100% - MAX_WIDTH) / 2);
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- When the container is **narrower** than `MAX_WIDTH`: `(100% - MAX_WIDTH) / 2` is negative, `max()` clamps back to `MIN_GUTTER`. Gutters hold their minimum.
|
|
20
|
+
- When the container is **wider** than `MAX_WIDTH`: gutters grow equally, enforcing the cap. The centre column never exceeds `MAX_WIDTH`.
|
|
21
|
+
|
|
22
|
+
## Start-aligned variant
|
|
23
|
+
|
|
24
|
+
When you want the content pinned to one side (e.g. left-aligned editorial layout):
|
|
25
|
+
|
|
26
|
+
```css
|
|
27
|
+
grid-template-columns: MIN_GUTTER minmax(0, MAX_WIDTH) 1fr;
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Left gutter stays fixed, content column is capped at `MAX_WIDTH`, remaining space goes to the right.
|
|
31
|
+
|
|
32
|
+
## In Vue with a prop
|
|
33
|
+
|
|
34
|
+
Because `v-bind()` in `<style>` can't be nested inside CSS functions like `max()`, build the column string as a computed and bind the whole value:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
// Props
|
|
38
|
+
interface Props {
|
|
39
|
+
maxWidth?: string; // e.g. "1064px"
|
|
40
|
+
contentAlign?: "start" | "center";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
44
|
+
maxWidth: undefined,
|
|
45
|
+
contentAlign: "center",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Computed column string
|
|
49
|
+
const gridColumns = computed(() => {
|
|
50
|
+
if (!props.maxWidth) return "16px 1fr 16px";
|
|
51
|
+
if (props.contentAlign === "start") return `16px minmax(0, ${props.maxWidth}) 1fr`;
|
|
52
|
+
return `max(16px, (100% - ${props.maxWidth}) / 2) 1fr max(16px, (100% - ${props.maxWidth}) / 2)`;
|
|
53
|
+
});
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```css
|
|
57
|
+
.component {
|
|
58
|
+
display: grid;
|
|
59
|
+
grid-template-columns: v-bind(gridColumns);
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Notes
|
|
64
|
+
|
|
65
|
+
- `subgrid` on child elements still works — the column count doesn't change, only the gutter widths.
|
|
66
|
+
- The minimum gutter (e.g. `16px`) is always enforced, so narrow viewports are safe without media queries.
|
|
67
|
+
- `contentAlign` has no effect when `maxWidth` is not set — fall through to the fixed-gutter default.
|
package/.claude/skills/index.md
CHANGED
|
@@ -23,15 +23,26 @@ Each skill is a single markdown file named `<area>-<task>.md`.
|
|
|
23
23
|
├── storybook-add-font.md — add a new font to Storybook
|
|
24
24
|
├── testing-add-unit-test.md — create a Vitest unit test with snapshots
|
|
25
25
|
├── testing-add-playwright.md — create a Playwright visual regression test
|
|
26
|
-
├── theming-override-default.md
|
|
27
|
-
├── colour-scheme-disable.md
|
|
26
|
+
├── theming-override-default.md — override the default theme with a custom colour scale
|
|
27
|
+
├── colour-scheme-disable.md — disable light/dark scheme support in a consumer app
|
|
28
|
+
├── component-dynamic-slots.md — named dynamic slots ($slots iteration) vs indexed dynamic slots (itemCount pattern)
|
|
29
|
+
├── component-local-style-override.md — styleClassPassthrough + scoped style block for per-usage visual customisation
|
|
30
|
+
├── component-prop-driven-container-layout.md — vary CSS grid layout inside @container queries using data-* attribute selectors
|
|
31
|
+
├── css-grid-max-width-gutters.md — cap a centre grid column width by growing gutters, with start/center alignment variants
|
|
32
|
+
├── component-aria-landmark.md — useAriaLabelledById composable: aria-labelledby for section/main/article/aside tags
|
|
28
33
|
└── components/
|
|
34
|
+
├── accordian-core.md — AccordianCore indexed dynamic slots (accordian-{n}-summary/icon/content), exclusive-open grouping
|
|
29
35
|
├── eyebrow-text.md — EyebrowText props, usage patterns, styling
|
|
30
36
|
├── hero-text.md — HeroText props, usage patterns, styling
|
|
37
|
+
├── layout-grid-by-cols.md — LayoutGridByCols dynamic slots (item-{n}), props, column/gap/breakpoint decisions
|
|
31
38
|
├── layout-row.md — LayoutRow variant guide, width/margin decisions, usage patterns
|
|
32
39
|
├── link-text.md — LinkText props, slots, usage patterns, styling
|
|
40
|
+
├── page-hero-highlights.md — PageHeroHighlights template: hero + highlights strip grid, CSS custom property theming
|
|
33
41
|
├── services-card.md — ServicesCard props, actions slot, usage patterns
|
|
34
|
-
|
|
42
|
+
├── services-section.md — ServicesSection props, summary-link/cta slots, summary vs full mode
|
|
43
|
+
├── contact-section.md — ContactSection props (stepperIndicatorSize pass-through), 3-item info+form layout, slot API
|
|
44
|
+
├── stepper-list.md — StepperList dynamic slots (item-{n}/indicator-{n}), props, connector behaviour
|
|
45
|
+
└── expanding-panel.md — ExpandingPanel v-model, forceOpened, slots (summary/icon/content), ARIA wiring
|
|
35
46
|
```
|
|
36
47
|
|
|
37
48
|
## Skill file template
|
|
@@ -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
|
}
|