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
@@ -0,0 +1,156 @@
1
+ # ExpandingPanel Component
2
+
3
+ ## Overview
4
+
5
+ `ExpandingPanel` is a single expand/collapse panel built on the native `<details>`/`<summary>` element. It animates open/close via a CSS grid-template-rows trick, supports `v-model` for controlled state, and can be locked open with `forceOpened`. Multiple panels can be grouped into a native accordion by sharing the same `name` prop (see `AccordianCore`).
6
+
7
+ ---
8
+
9
+ ## Props reference
10
+
11
+ | Prop | Type | Default | Notes |
12
+ |------|------|---------|-------|
13
+ | `name` | `string` | `useId()` | Identifies the panel. Used in ARIA attributes (`id-{name}-trigger`, `id-{name}-content`). If omitted, a unique id is generated automatically. |
14
+ | `animationDuration` | `number` | `400` | Expand/collapse transition duration in milliseconds. Pass `0` to disable animation. |
15
+ | `forceOpened` | `boolean` | `false` | When `true`, the panel is always open. The toggle icon is hidden and clicks do not close the panel. |
16
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root `.expanding-panel` element. |
17
+
18
+ ## Model
19
+
20
+ | Model | Type | Default | Notes |
21
+ |-------|------|---------|-------|
22
+ | `v-model` | `boolean` | `false` | Controls open/closed state. Bind to a `ref<boolean>` to manage state externally. |
23
+
24
+ ---
25
+
26
+ ## Slots
27
+
28
+ | Slot | Purpose |
29
+ |------|---------|
30
+ | `#summary` | Content rendered inside the clickable `<summary>` row (label area). |
31
+ | `#icon` | Custom toggle icon. Defaults to a `bi:caret-down-fill` icon that flips on open. Hidden when `forceOpened` is `true`. |
32
+ | `#content` | Content revealed when the panel is open. Can contain any markup. |
33
+
34
+ ---
35
+
36
+ ## Usage examples
37
+
38
+ ### Basic uncontrolled panel
39
+
40
+ ```vue
41
+ <ExpandingPanel name="delivery">
42
+ <template #summary>
43
+ <span>Delivery &amp; Returns</span>
44
+ </template>
45
+ <template #content>
46
+ <p>Free standard delivery on orders over £50.</p>
47
+ </template>
48
+ </ExpandingPanel>
49
+ ```
50
+
51
+ ### Controlled via v-model
52
+
53
+ ```vue
54
+ <script setup lang="ts">
55
+ const isOpen = ref(false);
56
+ </script>
57
+
58
+ <template>
59
+ <ExpandingPanel name="faq-1" v-model="isOpen">
60
+ <template #summary><span>What is your returns policy?</span></template>
61
+ <template #content><p>You can return any item within 30 days.</p></template>
62
+ </ExpandingPanel>
63
+ <button @click="isOpen = !isOpen">Toggle externally</button>
64
+ </template>
65
+ ```
66
+
67
+ ### Force opened (always visible, no toggle)
68
+
69
+ ```vue
70
+ <ExpandingPanel name="notice" :force-opened="true">
71
+ <template #summary><strong>Important notice</strong></template>
72
+ <template #content>
73
+ <p>This panel cannot be collapsed.</p>
74
+ </template>
75
+ </ExpandingPanel>
76
+ ```
77
+
78
+ ### Custom icon
79
+
80
+ ```vue
81
+ <ExpandingPanel name="custom-icon">
82
+ <template #summary><span>Section title</span></template>
83
+ <template #icon>
84
+ <svg width="12" height="12" viewBox="0 0 12 12">
85
+ <path d="M6 9L1 3h10z" fill="currentColor" />
86
+ </svg>
87
+ </template>
88
+ <template #content><p>Content here.</p></template>
89
+ </ExpandingPanel>
90
+ ```
91
+
92
+ ### Slow animation
93
+
94
+ ```vue
95
+ <ExpandingPanel name="slow" :animation-duration="800">
96
+ <template #summary><span>Slow panel</span></template>
97
+ <template #content><p>Opens and closes over 800 ms.</p></template>
98
+ </ExpandingPanel>
99
+ ```
100
+
101
+ ---
102
+
103
+ ## ARIA / accessibility
104
+
105
+ The component wires ARIA automatically from the `name` prop:
106
+
107
+ | Element | Attribute | Value |
108
+ |---------|-----------|-------|
109
+ | `<summary>` | `id` | `id-{name}-trigger` |
110
+ | `<summary>` | `aria-controls` | `id-{name}-content` |
111
+ | `<summary>` | `aria-expanded` | `true` / `false` |
112
+ | content div | `id` | `id-{name}-content` |
113
+ | content div | `aria-labelledby` | `id-{name}-trigger` |
114
+ | content div | `role` | `region` |
115
+
116
+ Always supply a meaningful `name` prop when using multiple panels on the same page to avoid duplicate IDs.
117
+
118
+ ---
119
+
120
+ ## Local style override scaffold
121
+
122
+ When consuming this component, scaffold a style block using `styleClassPassthrough`. Delete the block if unused.
123
+
124
+ See [component-local-style-override.md](../component-local-style-override.md) for the full pattern.
125
+
126
+ ```vue
127
+ <ExpandingPanel name="my-item" :style-class-passthrough="['my-panel']">
128
+ ...
129
+ </ExpandingPanel>
130
+
131
+ <style>
132
+ /* ─── ExpandingPanel local overrides ───────────────────────────────
133
+ Colours, borders, geometry only — do not override behaviour.
134
+ Delete this block if no overrides are needed.
135
+ ─────────────────────────────────────────────────────────────────── */
136
+ .expanding-panel {
137
+ &.my-panel {
138
+ /* Border */
139
+ /* border-block-end: 1px solid currentColor; */
140
+
141
+ /* Summary row geometry */
142
+ /* .expanding-panel-details .expanding-panel-summary { padding-block: 1.2rem; } */
143
+ }
144
+ }
145
+ </style>
146
+ ```
147
+
148
+ ---
149
+
150
+ ## Notes
151
+
152
+ - The open/close animation uses `grid-template-rows: 0fr → 1fr` — no JS height measurement needed.
153
+ - When `forceOpened` is `true`, `open` stays `true` regardless of `v-model`, but `v-model` still updates internally on clicks (useful if you later set `forceOpened` back to `false`).
154
+ - Group panels into a native accordion (only one open at a time) by passing the same `name` to multiple panels or use `AccordianCore` which handles this automatically.
155
+ - Auto-imported in Nuxt — no manual import needed.
156
+ - File: `app/components/02.molecules/expandable/expanding-panel/ExpandingPanel.vue`
@@ -77,6 +77,31 @@ Text is always `text-transform: uppercase` — do not pass pre-uppercased string
77
77
 
78
78
  Override via `styleClassPassthrough` or a parent HOC `<style>` block targeting `.eyebrow-text`.
79
79
 
80
+ ## Local style override scaffold
81
+
82
+ When consuming this component, scaffold a style block using `styleClassPassthrough`. Delete the block if unused.
83
+
84
+ See [component-local-style-override.md](../component-local-style-override.md) for the full pattern.
85
+
86
+ ```vue
87
+ <EyebrowText :style-class-passthrough="['my-eyebrow']" text-content="Our Services" />
88
+
89
+ <style>
90
+ /* ─── EyebrowText local overrides ──────────────────────────────────
91
+ Colours, borders, geometry only — do not override behaviour.
92
+ Delete this block if no overrides are needed.
93
+ ─────────────────────────────────────────────────────────────────── */
94
+ .eyebrow-text {
95
+ &.my-eyebrow {
96
+ /* Colours */
97
+ /* --colour-text-eyebrow: var(--brand-accent); */
98
+ }
99
+ }
100
+ </style>
101
+ ```
102
+
103
+ > **Note:** Font size is controlled via the `fontSize` prop and theme tokens `--eyebrow-text-large/medium/small` — define these at theme level, not as local overrides.
104
+
80
105
  ## Notes
81
106
 
82
107
  - Component is auto-imported in Nuxt — no import needed.
@@ -103,6 +103,31 @@ Key CSS custom properties:
103
103
  - `--colour-text-accent` — colour applied to `.accent` spans and the icon
104
104
  - `--hero-text-{scale}` — font size per scale value
105
105
 
106
+ ## Local style override scaffold
107
+
108
+ When consuming this component, scaffold a style block using `styleClassPassthrough`. Delete the block if unused.
109
+
110
+ See [component-local-style-override.md](../component-local-style-override.md) for the full pattern.
111
+
112
+ ```vue
113
+ <HeroText :style-class-passthrough="['my-hero']" tag="h1" :text-content="[...]" />
114
+
115
+ <style>
116
+ /* ─── HeroText local overrides ─────────────────────────────────────
117
+ Colours, borders, geometry only — do not override behaviour.
118
+ Delete this block if no overrides are needed.
119
+ ─────────────────────────────────────────────────────────────────── */
120
+ .hero-text {
121
+ &.my-hero {
122
+ /* Colours */
123
+ /* --colour-text-accent: var(--brand-primary); */
124
+ }
125
+ }
126
+ </style>
127
+ ```
128
+
129
+ > **Note:** Font size is controlled via the `fontSize` prop and theme tokens `--hero-text-display/title/heading/subheading/label` — define these at theme level, not as local overrides.
130
+
106
131
  ## Notes
107
132
 
108
133
  - Text segments are trimmed and a trailing space is automatically appended between segments in horizontal axis — do not manually pad `text` values.
@@ -0,0 +1,147 @@
1
+ # LayoutGridByCols Component
2
+
3
+ ## Overview
4
+
5
+ `LayoutGridByCols` is a CSS grid layout wrapper that arranges content into a fixed number of equal-width columns. It uses **named dynamic slots** — the component renders whatever slots the consumer passes, in the order they appear. It collapses to a single column below a configurable breakpoint.
6
+
7
+ ---
8
+
9
+ ## Slot pattern
10
+
11
+ Pass any number of slots with any names. The component renders each one in document order inside the grid.
12
+
13
+ ```vue
14
+ <LayoutGridByCols :column-count="3">
15
+ <template #item-0><ServicesCard /></template>
16
+ <template #item-1><ServicesCard /></template>
17
+ <template #item-2><ServicesCard /></template>
18
+ </LayoutGridByCols>
19
+ ```
20
+
21
+ When filling from a data array, use a dynamic slot name in a `v-for`:
22
+
23
+ ```vue
24
+ <LayoutGridByCols :column-count="3">
25
+ <template v-for="(item, i) in items" #[`item-${i}`] :key="i">
26
+ <ServicesCard :data="item" />
27
+ </template>
28
+ </LayoutGridByCols>
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Props reference
34
+
35
+ > **Hyphenation rule**: Vue's ESLint config enforces `vue/attribute-hyphenation`. Always write camelCase prop names hyphenated in templates: `:column-count`, `:single-col-below`, `:style-class-passthrough`.
36
+
37
+ | Prop (template form) | Type | Default | Notes |
38
+ |------|------|---------|-------|
39
+ | `:column-count` | `2 \| 3 \| 4 \| 5 \| 6` | `2` | Number of equal-width columns above `single-col-below`. Minimum is 2. |
40
+ | `:gap` | `string` | `"1rem"` | Any valid CSS length or shorthand. |
41
+ | `:single-col-below` | `string` | `"768px"` | Container width below which the grid collapses to a single column. |
42
+ | `tag` | `"div" \| "section"` | `"div"` | Use `"section"` for semantic page regions. |
43
+ | `label` | `string` | `""` | Required when `tag="section"` — rendered as a visually-hidden `<p>` linked via `aria-labelledby`. |
44
+ | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes on the root element. |
45
+
46
+ ---
47
+
48
+ ## Usage examples
49
+
50
+ ### Two-column grid (default)
51
+
52
+ ```vue
53
+ <LayoutGridByCols>
54
+ <template #left>
55
+ <p>Left cell</p>
56
+ </template>
57
+ <template #right>
58
+ <p>Right cell</p>
59
+ </template>
60
+ </LayoutGridByCols>
61
+ ```
62
+
63
+ ### Three-column card grid
64
+
65
+ ```vue
66
+ <LayoutGridByCols :column-count="3" gap="2rem">
67
+ <template #card-a><ServicesCard title="Card A" /></template>
68
+ <template #card-b><ServicesCard title="Card B" /></template>
69
+ <template #card-c><ServicesCard title="Card C" /></template>
70
+ </LayoutGridByCols>
71
+ ```
72
+
73
+ ### Section with accessible label
74
+
75
+ ```vue
76
+ <LayoutGridByCols tag="section" label="Our team" :column-count="4" gap="1.6rem">
77
+ <template #alice><TeamCard name="Alice" /></template>
78
+ <template #bob><TeamCard name="Bob" /></template>
79
+ <template #carol><TeamCard name="Carol" /></template>
80
+ <template #dan><TeamCard name="Dan" /></template>
81
+ </LayoutGridByCols>
82
+ ```
83
+
84
+ ### Data-driven with v-for
85
+
86
+ ```vue
87
+ <LayoutGridByCols :column-count="3">
88
+ <template v-for="(item, i) in items" #[`item-${i}`] :key="i">
89
+ <Card :data="item" />
90
+ </template>
91
+ </LayoutGridByCols>
92
+ ```
93
+
94
+ ---
95
+
96
+ ## Accessibility
97
+
98
+ - When `tag="section"`, the root element automatically receives `aria-labelledby` pointing to a visually-hidden `<p>` with the `label` value.
99
+ - Always provide a meaningful `label` when using `tag="section"`.
100
+ - When `tag="div"`, no label or ARIA attributes are added.
101
+
102
+ ---
103
+
104
+ ## Responsive behaviour
105
+
106
+ Uses **CSS container queries** (`container-type: inline-size`) — responds to its own container width, not the viewport.
107
+
108
+ - **Below `singleColBelow`**: single-column stacked layout.
109
+ - **At or above `singleColBelow`**: `columnCount`-column grid.
110
+
111
+ ---
112
+
113
+ ## Local style override scaffold
114
+
115
+ When consuming this component, scaffold a style block using `styleClassPassthrough`. Delete the block if unused.
116
+
117
+ See [component-local-style-override.md](../component-local-style-override.md) for the full pattern.
118
+
119
+ ```vue
120
+ <LayoutGridByCols :style-class-passthrough="['my-grid']" :column-count="3">
121
+ ...
122
+ </LayoutGridByCols>
123
+
124
+ <style>
125
+ /* ─── LayoutGridByCols local overrides ─────────────────────────────
126
+ Colours, borders, geometry only — do not override behaviour.
127
+ Delete this block if no overrides are needed.
128
+ ─────────────────────────────────────────────────────────────────── */
129
+ .layout-grid-by-cols {
130
+ &.my-grid {
131
+ /* Colours */
132
+ /* background: var(--brand-surface); */
133
+ }
134
+ }
135
+ </style>
136
+ ```
137
+
138
+ > **Note:** `gap` and `column-count` are prop-driven — use the props rather than CSS overrides for layout changes.
139
+
140
+ ---
141
+
142
+ ## Notes
143
+
144
+ - Auto-imported in Nuxt — no manual import needed.
145
+ - `column-count` is clamped to a minimum of 2 internally.
146
+ - `gap` accepts any CSS value including compound values like `"1rem 2rem"`.
147
+ - Slot names can be anything — semantic or indexed. Document order determines render order.
@@ -196,6 +196,41 @@ Override in consuming app to adjust all track sizes globally:
196
196
 
197
197
  ---
198
198
 
199
+ ## Local style override scaffold
200
+
201
+ When consuming this component, scaffold a style block using `styleClassPassthrough`. Delete the block if unused.
202
+
203
+ See [component-local-style-override.md](../component-local-style-override.md) for the full pattern.
204
+
205
+ ```vue
206
+ <LayoutRow variant="content" :style-class-passthrough="['my-row']">
207
+ ...
208
+ </LayoutRow>
209
+
210
+ <style>
211
+ /* ─── LayoutRow local overrides ────────────────────────────────────
212
+ Colours, borders, geometry only — do not override behaviour.
213
+ Delete this block if no overrides are needed.
214
+ ─────────────────────────────────────────────────────────────────── */
215
+ .layout-row {
216
+ &.my-row {
217
+ /* Track widths — for this instance only */
218
+ /* --content-max-width: 1200px; */
219
+ /* --popout-max-width: 1600px; */
220
+ /* --inset-content-max-width: 900px; */
221
+ /* --minimum-content-padding: 2rem; */
222
+
223
+ /* Colours */
224
+ /* background: var(--brand-surface); */
225
+ }
226
+ }
227
+ </style>
228
+ ```
229
+
230
+ > **Note:** Track width custom properties set here affect only this instance. For site-wide changes, set them at `:root` or theme level.
231
+
232
+ ---
233
+
199
234
  ## Notes
200
235
 
201
236
  - Auto-imported in Nuxt — no import needed.
@@ -93,6 +93,39 @@ Key CSS custom properties — define these in your consuming app to control appe
93
93
 
94
94
  Override via `styleClassPassthrough` or a parent HOC `<style>` block targeting `.link-text`.
95
95
 
96
+ ## Local style override scaffold
97
+
98
+ When consuming this component, scaffold a style block using `styleClassPassthrough`. Delete the block if unused.
99
+
100
+ See [component-local-style-override.md](../component-local-style-override.md) for the full pattern.
101
+
102
+ ```vue
103
+ <LinkText :style-class-passthrough="['my-link']" to="/path" link-text="Learn More" />
104
+
105
+ <style>
106
+ /* ─── LinkText local overrides ─────────────────────────────────────
107
+ Colours, borders, geometry only — do not override behaviour.
108
+ Delete this block if no overrides are needed.
109
+ ─────────────────────────────────────────────────────────────────── */
110
+ .link-text {
111
+ &.my-link {
112
+ /* Colours */
113
+ /* --link-text-colour: var(--brand-primary); */
114
+ /* --link-text-colour-hover: var(--brand-primary-dark); */
115
+
116
+ /* Typography */
117
+ /* --link-text-font-size: 1.6rem; */
118
+ /* --link-text-decoration: underline; */
119
+ /* --link-text-decoration-hover: none; */
120
+ /* --link-text-underline-offset: 0.3em; */
121
+
122
+ /* Geometry */
123
+ /* --link-text-gap: 0.6em; */
124
+ }
125
+ }
126
+ </style>
127
+ ```
128
+
96
129
  ## Notes
97
130
 
98
131
  - Component is auto-imported in Nuxt — no import needed.
@@ -0,0 +1,224 @@
1
+ # PageHeroHighlights Component
2
+
3
+ ## Overview
4
+
5
+ `PageHeroHighlights` is a page-level layout template that creates a "hero + highlights strip" grid. It has:
6
+
7
+ - A **header zone** — full edge-to-edge background, content drives the row height
8
+ - A **highlights strip** — straddles the header/content boundary (overlaps both), sits above via `z-index`
9
+ - A **content zone** — background fills behind the highlights strip, actual content sits below it
10
+
11
+ The layout uses a 4-row CSS Grid with `subgrid` — no `translate`, negative margins, or absolute positioning.
12
+
13
+ ## Props
14
+
15
+ | Prop | Type | Default | Description |
16
+ | ----------------------- | ------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------- |
17
+ | `tag` | `"div" \| "section" \| "main"` | `"div"` | Root element tag |
18
+ | `highlightsEqualWidths` | `boolean` | `false` | Equal-width grid columns for highlight items |
19
+ | `highlightsJustify` | `"start" \| "center" \| "end" \| "space-between" \| "space-around"` | `"start"` | Alignment of highlight items along the main axis |
20
+ | `maxWidth` | `string` | `undefined` | Cap the central content column (e.g. `"1064px"`). Gutters grow to enforce the constraint; below this width they hold at `16px`. |
21
+ | `contentAlign` | `"start" \| "center"` | `"center"` | When `maxWidth` is set: `"center"` grows gutters equally; `"start"` pins content to the left with a fixed `16px` left gutter. |
22
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes on the root element |
23
+
24
+ ## Slots
25
+
26
+ | Slot | Slot props | Purpose |
27
+ | ------------ | ----------------------- | ----------------------------------- |
28
+ | `header` | `{ headingId: string }` | Header zone — text, title, subtitle |
29
+ | `highlights` | — | Highlight cards in the strip |
30
+ | `content` | — | Page body content below the strip |
31
+
32
+ ## Basic usage
33
+
34
+ ```vue
35
+ <PageHeroHighlights>
36
+ <template #header>
37
+ <p class="page-heading-1">Dashboard</p>
38
+ <p class="page-body-normal">Overview of your account activity.</p>
39
+ </template>
40
+
41
+ <template #highlights>
42
+ <div class="card">Total Revenue: £24,500</div>
43
+ <div class="card">Active Users: 1,284</div>
44
+ <div class="card">Open Tasks: 37</div>
45
+ </template>
46
+
47
+ <template #content>
48
+ <p class="page-heading-2">Recent Activity</p>
49
+ </template>
50
+ </PageHeroHighlights>
51
+ ```
52
+
53
+ ## Composition with other library components
54
+
55
+ Each slot accepts any content, but these library components are natural fits:
56
+
57
+ | Slot | Component | Notes |
58
+ | ------------- | --------------------------------- | ----------------------------------------------------------------------------------------------- |
59
+ | `#header` | `HeroText` | Heading with accent text, icon, and configurable size — wires `headingId` for `aria-labelledby` |
60
+ | `#highlights` | `ServicesCard` (×n) | Portrait cards with image, title, description, and CTA slot |
61
+ | `#highlights` | `LayoutGridByCols` wrapping cards | When you want a responsive column grid rather than a single row of cards |
62
+ | `#content` | Any content component | Below the straddle — safe to use `LayoutRow`, `ServicesSection`, etc. |
63
+
64
+ Example with `HeroText` in the header slot:
65
+
66
+ ```vue
67
+ <PageHeroHighlights tag="section" max-width="1064px">
68
+ <template #header="{ headingId }">
69
+ <HeroText :heading-id="headingId" text="Welcome back" accent-text="Simon" />
70
+ </template>
71
+
72
+ <template #highlights>
73
+ <ServicesCard v-for="item in highlights" :key="item.id" v-bind="item">
74
+ <template #actions>
75
+ <LinkText :href="item.href">View</LinkText>
76
+ </template>
77
+ </ServicesCard>
78
+ </template>
79
+
80
+ <template #content>
81
+ <!-- page body -->
82
+ </template>
83
+ </PageHeroHighlights>
84
+ ```
85
+
86
+ ## With aria-labelledby (section tag)
87
+
88
+ When `tag="section"`, `aria-labelledby` is set automatically. Wire the heading id via the scoped slot prop:
89
+
90
+ ```vue
91
+ <PageHeroHighlights tag="section">
92
+ <template #header="{ headingId }">
93
+ <h1 :id="headingId" class="page-heading-1">Dashboard</h1>
94
+ </template>
95
+ ...
96
+ </PageHeroHighlights>
97
+ ```
98
+
99
+ See [component-aria-landmark.md](../component-aria-landmark.md) for the full pattern.
100
+
101
+ ## Equal-width highlights
102
+
103
+ By default, highlight items size to their content (`flex-wrap`). Pass `:highlights-equal-widths="true"` to switch to a grid where all items share equal column widths:
104
+
105
+ ```vue
106
+ <PageHeroHighlights :highlights-equal-widths="true">
107
+ ...
108
+ </PageHeroHighlights>
109
+ ```
110
+
111
+ ## Constraining the central column width
112
+
113
+ Pass `max-width` to cap the content column. The gutters grow to enforce it — full-bleed backgrounds are unaffected and `subgrid` continues to work. Use `content-align` to pin to the left or centre:
114
+
115
+ ```vue
116
+ <!-- Centred, capped at 1064px -->
117
+ <PageHeroHighlights max-width="1064px" content-align="center">...</PageHeroHighlights>
118
+
119
+ <!-- Left-pinned, capped at 1064px (right side takes remaining space) -->
120
+ <PageHeroHighlights max-width="1064px" content-align="start">...</PageHeroHighlights>
121
+ ```
122
+
123
+ See [css-grid-max-width-gutters.md](../css-grid-max-width-gutters.md) for the full pattern explanation.
124
+
125
+ ## Local style override scaffold
126
+
127
+ When consuming this component, scaffold a style block using `styleClassPassthrough`. The block below lists every available CSS custom property — update the values you need and delete the rest.
128
+
129
+ See [component-local-style-override.md](../component-local-style-override.md) for the full pattern.
130
+
131
+ ```vue
132
+ <PageHeroHighlights :style-class-passthrough="['my-page-hero']">
133
+ ...
134
+ </PageHeroHighlights>
135
+
136
+ <style>
137
+ /* ─── PageHeroHighlights local overrides ────────────────────────────
138
+ Update values as needed. Delete tokens you are not overriding.
139
+ ─────────────────────────────────────────────────────────────────── */
140
+ .page-hero-highlights {
141
+ &.my-page-hero {
142
+ /* Header zone */
143
+ /* --header-row-background-colour: darkblue; */
144
+
145
+ /* Highlights strip */
146
+ /* --highlights-row-item-gap: 1rem; */
147
+ /* --highlights-row-initial-item-offset: 1.2rem; */
148
+
149
+ /* Highlight cards */
150
+ /* --highlight-rows-gap: 1.2rem; */
151
+ /* --highlight-title-height: 1fr; */ /* see: highlight-title-baseline prop */
152
+ /* --highlight-padding-block-start: 1.2rem; */
153
+ /* --highlight-padding: 1.2rem; */
154
+ /* --highlight-background-color: white; */
155
+ /* --highlight-border: 1px solid black; */
156
+ /* --highlight-border-radius: 8px; */
157
+ /* --highlight-color: black; */
158
+
159
+ /* Content zone */
160
+ /* --content-row-background-color: var(--slate-01); */ /* transparent */
161
+ /* --content-row-start-gap: 1.2rem; */
162
+ /* --content-row-end-gap: 1.2rem; */
163
+
164
+ /* Content slot decorative border */
165
+ /* --content-slot-margin-block-start: 2.4rem; */
166
+ /* --content-slot-margin: var(--highlights-row-initial-item-offset); */
167
+ /* --content-slot-background-color: var(--slate-00); */
168
+ /* --content-slot-border: 1px solid var(--slate-06); */
169
+ /* --content-slot-border-radius: 0.8rem; */
170
+ /* --content-slot-outline: 1px solid var(--slate-02); */
171
+
172
+ /* When using :highlight-title-baseline="true" */
173
+ /* &.highlight-title-baseline { */
174
+ /* --highlight-title-height: 4rem; */ /* proportional value preferred */
175
+ /* --highlight-padding-block-start: 0; */
176
+ /* } */
177
+ }
178
+ }
179
+ </style>
180
+ ```
181
+
182
+ > **Note:** The minimum gutter width (`16px`) and layout behaviour are not overridable via CSS custom properties. Use the `max-width` and `content-align` props to control column constraints.
183
+
184
+ ## Grid structure (reference)
185
+
186
+ ```text
187
+ col: [gutter] [centre] [gutter]
188
+ row1: header content (height driven by slot)
189
+ row2: highlights top half ← highlights spans rows 2–3, z-index: 1
190
+ row3: highlights bottom half
191
+ row4: page content (never underflows highlights)
192
+ ```
193
+
194
+ `.header-row` spans cols 1–3, rows 1–2 (edge-to-edge bg). `.header-slot` is placed in row 1 only.
195
+ `.content-row` spans cols 1–3, rows 3–4 (bg fills behind highlights; `.content-slot` is placed in row 4 only). The decorative border behind `.content-slot` is rendered via `.content-row:before` — there is no separate DOM element for it.
196
+
197
+ ## Layout pitfall: do not use `grid-template-rows: subgrid` inside `.highlights-row`
198
+
199
+ The `.highlights-row` element spans rows 2–3 of the parent grid (the "straddle"). If you add an inner grid to `.highlights-row` (e.g. to extend `equal-widths` behaviour) and include `grid-template-rows: subgrid`, auto-placed items will only occupy row 1 of the subgrid (= parent row 2). Parent row 3 collapses to 0-height, destroying the straddle effect — `.content-row` appears immediately below the highlights instead of overlapping it.
200
+
201
+ ```css
202
+ /* ❌ — breaks the straddle when items are auto-placed by column flow */
203
+ &.equal-widths {
204
+ display: grid;
205
+ grid-template-rows: subgrid; /* row 3 collapses */
206
+ grid-auto-columns: 1fr;
207
+ grid-auto-flow: column;
208
+ }
209
+
210
+ /* ✅ — single implicit row; items stretch to fill combined height of rows 2–3 */
211
+ &.equal-widths {
212
+ display: grid;
213
+ grid-auto-columns: 1fr;
214
+ grid-auto-flow: column;
215
+ }
216
+ ```
217
+
218
+ ## Notes
219
+
220
+ - Component is auto-imported in Nuxt — no import needed.
221
+ - Lives in `app/components/04.templates/page-hero-highlights/`.
222
+ - Storybook title: `"Templates/PageHeroHighlights"`.
223
+ - **Minimum gutter is fixed at `16px`** — it is baked into the `gridColumns` computed and cannot be overridden by a CSS custom property. If a consumer needs a different minimum (e.g. `24px`), it requires a prop or a fork of the component.
224
+ - **`contentAlign` has no effect without `maxWidth`** — both sides always hold `16px` when `maxWidth` is not set.