srcdev-nuxt-components 9.0.15 → 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.
Files changed (107) hide show
  1. package/.claude/settings.json +25 -0
  2. package/.claude/skills/component-aria-landmark.md +68 -0
  3. package/.claude/skills/component-dynamic-slots.md +150 -0
  4. package/.claude/skills/component-local-style-override.md +126 -0
  5. package/.claude/skills/component-prop-driven-container-layout.md +42 -0
  6. package/.claude/skills/components/accordian-core.md +159 -0
  7. package/.claude/skills/components/contact-section.md +101 -0
  8. package/.claude/skills/components/expanding-panel.md +156 -0
  9. package/.claude/skills/components/eyebrow-text.md +25 -0
  10. package/.claude/skills/components/hero-text.md +25 -0
  11. package/.claude/skills/components/layout-grid-by-cols.md +147 -0
  12. package/.claude/skills/components/layout-row.md +35 -0
  13. package/.claude/skills/components/link-text.md +33 -0
  14. package/.claude/skills/components/page-hero-highlights.md +224 -0
  15. package/.claude/skills/components/services-card.md +28 -0
  16. package/.claude/skills/components/services-section.md +25 -0
  17. package/.claude/skills/components/stepper-list.md +227 -0
  18. package/.claude/skills/css-grid-max-width-gutters.md +67 -0
  19. package/.claude/skills/index.md +14 -3
  20. package/.claude/skills/storybook-add-story.md +60 -0
  21. package/.claude/skills/testing-add-unit-test.md +56 -0
  22. package/app/assets/styles/setup/01.config/index.css +0 -1
  23. package/app/assets/styles/setup/03.theming/default/_dark.css +2 -2
  24. package/app/assets/styles/setup/04.elements/forms/02.typography.css +1 -0
  25. package/app/assets/styles/setup/05.typography/02.utility-classes/_font-classes-page-link.css +14 -14
  26. package/app/assets/styles/setup/index.css +0 -1
  27. package/app/components/01.atoms/card/CardCore.vue +92 -0
  28. package/app/components/01.atoms/card/stories/CardCore.stories.ts +132 -0
  29. package/app/components/01.atoms/card/tests/CardCore.spec.ts +207 -0
  30. package/app/components/01.atoms/card/tests/__snapshots__/CardCore.spec.ts.snap +43 -0
  31. package/app/components/01.atoms/content-wrappers/content-columns-2/ContentColumns2.vue +51 -0
  32. package/app/components/01.atoms/content-wrappers/content-columns-2/stories/ContentColumns2.stories.ts +110 -0
  33. package/app/components/01.atoms/content-wrappers/content-columns-2/tests/ContentColumns2.spec.ts +105 -0
  34. package/app/components/01.atoms/content-wrappers/content-columns-2/tests/__snapshots__/ContentColumns2.spec.ts.snap +14 -0
  35. package/app/components/01.atoms/content-wrappers/content-width/ContentWidth.vue +88 -0
  36. package/app/components/01.atoms/content-wrappers/content-width/stories/ContentWidth.stories.ts +362 -0
  37. package/app/components/01.atoms/content-wrappers/content-width/tests/ContentWidth.spec.ts +132 -0
  38. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/LayoutGridByCols.vue +71 -0
  39. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/stories/LayoutGridByCols.stories.ts +219 -0
  40. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/LayoutGridByCols.spec.ts +174 -0
  41. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/__snapshots__/LayoutGrid.spec.ts.snap +36 -0
  42. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/__snapshots__/LayoutGridByCols.spec.ts.snap +36 -0
  43. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/LayoutGridByWidth.vue +70 -0
  44. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/stories/LayoutGridByWidth.stories.ts +220 -0
  45. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/LayoutGridByWidth.spec.ts +174 -0
  46. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/__snapshots__/LayoutGrid.spec.ts.snap +36 -0
  47. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/__snapshots__/LayoutGridByCols.spec.ts.snap +36 -0
  48. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/__snapshots__/LayoutGridByWidth.spec.ts.snap +36 -0
  49. package/app/components/01.atoms/text-blocks/eyebrow-text/stories/EyebrowText.stories.ts +1 -1
  50. package/app/components/01.atoms/text-blocks/hero-text/stories/HeroText.stories.ts +1 -1
  51. package/app/components/01.atoms/text-blocks/link-text/stories/LinkText.stories.ts +1 -1
  52. package/app/components/02.molecules/contact-section/stories/ContactSection.stories.ts +5 -0
  53. package/app/components/02.molecules/contact-section/tests/ContactSection.spec.ts +15 -0
  54. package/app/components/02.molecules/contact-section/tests/ContactSection.vue +25 -17
  55. package/app/components/{accordian → 02.molecules/expandable/accordian}/stories/AccordianCore.stories.ts +1 -1
  56. package/app/components/02.molecules/expandable/expanding-panel/stories/ExpandingPanel.stories.ts +245 -0
  57. package/app/components/02.molecules/expandable/expanding-panel/tests/ExpandingPanel.spec.ts +351 -0
  58. package/app/components/02.molecules/expandable/expanding-panel/tests/__snapshots__/ExpandingPanel.spec.ts.snap +38 -0
  59. package/app/components/02.molecules/navigation/navigation-horizontal/NavigationHorizontal.vue +139 -0
  60. package/app/components/02.molecules/navigation/navigation-horizontal/NavigationHorizontalAdvanced.vue +172 -0
  61. package/app/components/02.molecules/profile-section/ProfileSection.vue +2 -3
  62. package/app/components/02.molecules/profile-section/tests/ProfileSection.spec.ts +2 -2
  63. package/app/components/02.molecules/stepper-list/StepperList.vue +131 -92
  64. package/app/components/02.molecules/stepper-list/stories/StepperList.stories.ts +31 -0
  65. package/app/components/02.molecules/stepper-list/tests/StepperList.spec.ts +24 -0
  66. package/app/components/02.molecules/stepper-list/tests/__snapshots__/StepperList.spec.ts.snap +22 -9
  67. package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +782 -0
  68. package/app/components/03.organisms/image-galleries/slider-gallery/stories/SliderGallery.stories.ts +233 -0
  69. package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +226 -0
  70. package/app/components/03.organisms/image-galleries/slider-gallery/tests/__snapshots__/SliderGallery.spec.ts.snap +69 -0
  71. package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +1 -1
  72. package/app/components/03.organisms/services/services-grids/ServicesSectionGrid.vue +1 -1
  73. package/app/components/03.organisms/services/services-section/ServicesSection.vue +2 -3
  74. package/app/components/04.templates/page-hero-highlights/PageHeroHighlights.vue +239 -0
  75. package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlights.stories.ts +404 -0
  76. package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlights.spec.ts +198 -0
  77. package/app/components/04.templates/page-hero-highlights/tests/__snapshots__/PageHeroHighlights.spec.ts.snap +19 -0
  78. package/app/components/container-glow/ContainerGlowCore.vue +20 -27
  79. package/app/components/forms/input-button/InputButtonCore.vue +105 -104
  80. package/app/components/glowing-border/stories/GlowingBorder.stories.ts +21 -21
  81. package/app/composables/useAriaLabelledById.ts +13 -0
  82. package/app/layouts/default.vue +8 -3
  83. package/app/pages/forms/examples/buttons/index.vue +6 -6
  84. package/app/pages/forms/examples/material/checkbox-radio-panels.vue +3 -3
  85. package/app/pages/forms/examples/material/text-fields.vue +607 -610
  86. package/app/pages/page-hero-highlights.vue +81 -0
  87. package/app/pages/ui/{display-card.vue → card-core.vue} +15 -15
  88. package/app/pages/ui/contact-section.vue +1 -1
  89. package/app/pages/ui/container-glow.vue +1 -1
  90. package/app/pages/ui/content-width.vue +126 -0
  91. package/app/pages/ui/glowing-border.vue +9 -9
  92. package/app/pages/ui/navigation/navigation-horizontal.vue +493 -0
  93. package/app/pages/ui/services/services-section/[slug].vue +3 -1
  94. package/package.json +2 -2
  95. package/app/assets/styles/setup/01.config/_basic-resets.css +0 -9
  96. package/app/components/content-columns/TwoColumns.vue +0 -59
  97. package/app/components/content-columns/stories/TwoColumns.stories.ts +0 -561
  98. package/app/components/content-containers/ContentContainer.vue +0 -89
  99. package/app/components/content-containers/stories/ContentContainer.stories.ts +0 -465
  100. package/app/components/content-grid/ContentGrid.vue +0 -85
  101. package/app/components/display-card/DisplayCard.vue +0 -122
  102. package/app/components/image-galleries/SliderGallery.vue +0 -786
  103. package/app/pages/ui/content-container.vue +0 -112
  104. /package/app/components/{accordian → 02.molecules/expandable/accordian}/AccordianCore.vue +0 -0
  105. /package/app/components/{accordian → 02.molecules/expandable/accordian}/tests/AccordianCore.spec.ts +0 -0
  106. /package/app/components/{accordian → 02.molecules/expandable/accordian}/tests/__snapshots__/AccordianCore.spec.ts.snap +0 -0
  107. /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.
@@ -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 — override the default theme with a custom colour scale
27
- ├── colour-scheme-disable.md — disable light/dark scheme support in a consumer app
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
- └── services-section.md — ServicesSection props, summary-link/cta slots, summary vs full mode
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.:
@@ -1,3 +1,2 @@
1
1
  @import "./_normalise";
2
- @import "./_basic-resets";
3
2
  @import "./_head";
@@ -78,8 +78,8 @@
78
78
  /* ===========================================
79
79
  ANCHOR LINK VARIABLES
80
80
  =========================================== */
81
- --colour-link-default: var(--blue-10);
82
- --colour-link-hover: var(--blue-09);
81
+ --colour-link-default: var(--blue-02);
82
+ --colour-link-hover: var(--blue-03);
83
83
 
84
84
  /* ===========================================
85
85
  FORM INPUT VARIABLES
@@ -13,6 +13,7 @@
13
13
  --button-font-size: 1.6rem; /* 16px */
14
14
  --button-line-height: 1.4;
15
15
  --button-font-weight: 600;
16
+ --button-text-transform: none;
16
17
 
17
18
  /* Description */
18
19
  --field-description-font-size: 1.3rem;
@@ -1,5 +1,5 @@
1
1
  .page-link-large {
2
- color: var(--theme-link-default);
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(--theme-link-default);
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(--theme-link-default);
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(--theme-link-default);
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(--theme-link-default);
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(--theme-link-default);
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(--theme-link-default);
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(--theme-link-default);
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(--theme-link-default);
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(--theme-link-default);
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(--theme-link-default);
103
+ color: var(--colour-link-default);
104
104
  }
105
105
 
106
106
  &:hover {
107
- color: var(--theme-link-hover);
107
+ color: var(--colour-link-hover);
108
108
  }
109
109
 
110
110
  &:focus-visible {
111
- color: var(--theme-link-hover);
112
- outline: 2px solid var(--theme-link-default);
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
  }
@@ -1,4 +1,3 @@
1
- /* @layer reset, colours, theming, form-tokens, typography, a11y, utilities, components; */
2
1
  @layer reset, colours, theming, form-tokens, typography, a11y, components, utilities;
3
2
 
4
3
  @import "./01.config/" layer(reset);