srcdev-nuxt-components 9.0.15 → 9.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +25 -0
- package/.claude/skills/component-aria-landmark.md +68 -0
- package/.claude/skills/component-dynamic-slots.md +150 -0
- package/.claude/skills/component-export-types.md +61 -0
- package/.claude/skills/component-local-style-override.md +126 -0
- package/.claude/skills/component-prop-driven-container-layout.md +42 -0
- package/.claude/skills/components/accordian-core.md +159 -0
- package/.claude/skills/components/contact-section.md +101 -0
- package/.claude/skills/components/expanding-panel.md +156 -0
- package/.claude/skills/components/eyebrow-text.md +25 -0
- package/.claude/skills/components/hero-text.md +25 -0
- package/.claude/skills/components/layout-grid-by-cols.md +147 -0
- package/.claude/skills/components/layout-row.md +35 -0
- package/.claude/skills/components/link-text.md +33 -0
- package/.claude/skills/components/page-hero-highlights.md +224 -0
- package/.claude/skills/components/services-card.md +28 -0
- package/.claude/skills/components/services-section.md +25 -0
- package/.claude/skills/components/stepper-list.md +227 -0
- package/.claude/skills/css-grid-max-width-gutters.md +67 -0
- package/.claude/skills/index.md +15 -3
- package/.claude/skills/storybook-add-story.md +60 -0
- package/.claude/skills/testing-add-unit-test.md +56 -0
- package/app/assets/styles/setup/01.config/index.css +0 -1
- package/app/assets/styles/setup/03.theming/default/_dark.css +2 -2
- package/app/assets/styles/setup/04.elements/forms/02.typography.css +1 -0
- package/app/assets/styles/setup/05.typography/02.utility-classes/_font-classes-page-link.css +14 -14
- package/app/assets/styles/setup/index.css +0 -1
- package/app/components/01.atoms/card/CardCore.vue +92 -0
- package/app/components/01.atoms/card/stories/CardCore.stories.ts +132 -0
- package/app/components/01.atoms/card/tests/CardCore.spec.ts +207 -0
- package/app/components/01.atoms/card/tests/__snapshots__/CardCore.spec.ts.snap +43 -0
- package/app/components/01.atoms/content-wrappers/content-columns-2/ContentColumns2.vue +51 -0
- package/app/components/01.atoms/content-wrappers/content-columns-2/stories/ContentColumns2.stories.ts +110 -0
- package/app/components/01.atoms/content-wrappers/content-columns-2/tests/ContentColumns2.spec.ts +105 -0
- package/app/components/01.atoms/content-wrappers/content-columns-2/tests/__snapshots__/ContentColumns2.spec.ts.snap +14 -0
- package/app/components/01.atoms/content-wrappers/content-width/ContentWidth.vue +88 -0
- package/app/components/01.atoms/content-wrappers/content-width/stories/ContentWidth.stories.ts +362 -0
- package/app/components/01.atoms/content-wrappers/content-width/tests/ContentWidth.spec.ts +132 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/LayoutGridByCols.vue +71 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/stories/LayoutGridByCols.stories.ts +219 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/LayoutGridByCols.spec.ts +174 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/__snapshots__/LayoutGrid.spec.ts.snap +36 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/__snapshots__/LayoutGridByCols.spec.ts.snap +36 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/LayoutGridByWidth.vue +70 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/stories/LayoutGridByWidth.stories.ts +220 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/LayoutGridByWidth.spec.ts +174 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/__snapshots__/LayoutGrid.spec.ts.snap +36 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/__snapshots__/LayoutGridByCols.spec.ts.snap +36 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/__snapshots__/LayoutGridByWidth.spec.ts.snap +36 -0
- package/app/components/01.atoms/text-blocks/eyebrow-text/stories/EyebrowText.stories.ts +1 -1
- package/app/components/01.atoms/text-blocks/hero-text/stories/HeroText.stories.ts +1 -1
- package/app/components/01.atoms/text-blocks/link-text/stories/LinkText.stories.ts +1 -1
- package/app/components/02.molecules/contact-section/stories/ContactSection.stories.ts +5 -0
- package/app/components/02.molecules/contact-section/tests/ContactSection.spec.ts +15 -0
- package/app/components/02.molecules/contact-section/tests/ContactSection.vue +25 -17
- package/app/components/{accordian → 02.molecules/expandable/accordian}/stories/AccordianCore.stories.ts +1 -1
- package/app/components/02.molecules/expandable/expanding-panel/stories/ExpandingPanel.stories.ts +245 -0
- package/app/components/02.molecules/expandable/expanding-panel/tests/ExpandingPanel.spec.ts +351 -0
- package/app/components/02.molecules/expandable/expanding-panel/tests/__snapshots__/ExpandingPanel.spec.ts.snap +38 -0
- package/app/components/02.molecules/navigation/navigation-horizontal/NavigationHorizontal.vue +162 -0
- package/app/components/02.molecules/navigation/navigation-horizontal/stories/NavigationHorizontal.stories.ts +373 -0
- package/app/components/02.molecules/navigation/navigation-horizontal/tests/NavigationHorizontal.spec.ts +152 -0
- package/app/components/02.molecules/navigation/navigation-horizontal/tests/__snapshots__/NavigationHorizontal.spec.ts.snap +17 -0
- package/app/components/02.molecules/profile-section/ProfileSection.vue +2 -3
- package/app/components/02.molecules/profile-section/tests/ProfileSection.spec.ts +2 -2
- package/app/components/02.molecules/stepper-list/StepperList.vue +131 -92
- package/app/components/02.molecules/stepper-list/stories/StepperList.stories.ts +31 -0
- package/app/components/02.molecules/stepper-list/tests/StepperList.spec.ts +24 -0
- package/app/components/02.molecules/stepper-list/tests/__snapshots__/StepperList.spec.ts.snap +22 -9
- package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +782 -0
- package/app/components/03.organisms/image-galleries/slider-gallery/stories/SliderGallery.stories.ts +233 -0
- package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +226 -0
- package/app/components/03.organisms/image-galleries/slider-gallery/tests/__snapshots__/SliderGallery.spec.ts.snap +69 -0
- package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +1 -1
- package/app/components/03.organisms/services/services-grids/ServicesSectionGrid.vue +1 -1
- package/app/components/03.organisms/services/services-section/ServicesSection.vue +2 -3
- package/app/components/04.templates/page-hero-highlights/PageHeroHighlights.vue +239 -0
- package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlights.stories.ts +404 -0
- package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlights.spec.ts +198 -0
- package/app/components/04.templates/page-hero-highlights/tests/__snapshots__/PageHeroHighlights.spec.ts.snap +19 -0
- package/app/components/container-glow/ContainerGlowCore.vue +20 -27
- package/app/components/forms/input-button/InputButtonCore.vue +105 -104
- package/app/components/glowing-border/stories/GlowingBorder.stories.ts +21 -21
- package/app/composables/useAriaLabelledById.ts +13 -0
- package/app/layouts/default.vue +8 -3
- package/app/pages/forms/examples/buttons/index.vue +6 -6
- package/app/pages/forms/examples/material/checkbox-radio-panels.vue +3 -3
- package/app/pages/forms/examples/material/text-fields.vue +607 -610
- package/app/pages/page-hero-highlights.vue +81 -0
- package/app/pages/ui/{display-card.vue → card-core.vue} +15 -15
- package/app/pages/ui/contact-section.vue +1 -1
- package/app/pages/ui/container-glow.vue +1 -1
- package/app/pages/ui/content-width.vue +126 -0
- package/app/pages/ui/glowing-border.vue +9 -9
- package/app/pages/ui/navigation/navigation-horizontal.vue +484 -0
- package/app/pages/ui/services/services-section/[slug].vue +3 -1
- package/app/types/components/index.ts +1 -0
- package/app/types/components/navigation-horizontal.d.ts +11 -0
- package/package.json +2 -2
- package/app/assets/styles/setup/01.config/_basic-resets.css +0 -9
- package/app/components/content-columns/TwoColumns.vue +0 -59
- package/app/components/content-columns/stories/TwoColumns.stories.ts +0 -561
- package/app/components/content-containers/ContentContainer.vue +0 -89
- package/app/components/content-containers/stories/ContentContainer.stories.ts +0 -465
- package/app/components/content-grid/ContentGrid.vue +0 -85
- package/app/components/display-card/DisplayCard.vue +0 -122
- package/app/components/image-galleries/SliderGallery.vue +0 -786
- package/app/pages/ui/content-container.vue +0 -112
- /package/app/components/{accordian → 02.molecules/expandable/accordian}/AccordianCore.vue +0 -0
- /package/app/components/{accordian → 02.molecules/expandable/accordian}/tests/AccordianCore.spec.ts +0 -0
- /package/app/components/{accordian → 02.molecules/expandable/accordian}/tests/__snapshots__/AccordianCore.spec.ts.snap +0 -0
- /package/app/components/{expanding-panel → 02.molecules/expandable/expanding-panel}/ExpandingPanel.vue +0 -0
|
@@ -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.
|
|
@@ -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,27 @@ 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
|
|
33
|
+
├── component-export-types.md — move inline component types to app/types/components/ barrel for consumer imports
|
|
28
34
|
└── components/
|
|
35
|
+
├── accordian-core.md — AccordianCore indexed dynamic slots (accordian-{n}-summary/icon/content), exclusive-open grouping
|
|
29
36
|
├── eyebrow-text.md — EyebrowText props, usage patterns, styling
|
|
30
37
|
├── hero-text.md — HeroText props, usage patterns, styling
|
|
38
|
+
├── layout-grid-by-cols.md — LayoutGridByCols dynamic slots (item-{n}), props, column/gap/breakpoint decisions
|
|
31
39
|
├── layout-row.md — LayoutRow variant guide, width/margin decisions, usage patterns
|
|
32
40
|
├── link-text.md — LinkText props, slots, usage patterns, styling
|
|
41
|
+
├── page-hero-highlights.md — PageHeroHighlights template: hero + highlights strip grid, CSS custom property theming
|
|
33
42
|
├── services-card.md — ServicesCard props, actions slot, usage patterns
|
|
34
|
-
|
|
43
|
+
├── services-section.md — ServicesSection props, summary-link/cta slots, summary vs full mode
|
|
44
|
+
├── contact-section.md — ContactSection props (stepperIndicatorSize pass-through), 3-item info+form layout, slot API
|
|
45
|
+
├── stepper-list.md — StepperList dynamic slots (item-{n}/indicator-{n}), props, connector behaviour
|
|
46
|
+
└── expanding-panel.md — ExpandingPanel v-model, forceOpened, slots (summary/icon/content), ARIA wiring
|
|
35
47
|
```
|
|
36
48
|
|
|
37
49
|
## Skill file template
|