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.
- package/.claude/settings.json +25 -0
- package/.claude/skills/component-aria-landmark.md +68 -0
- package/.claude/skills/component-dynamic-slots.md +150 -0
- package/.claude/skills/component-local-style-override.md +126 -0
- package/.claude/skills/component-prop-driven-container-layout.md +42 -0
- package/.claude/skills/components/accordian-core.md +159 -0
- package/.claude/skills/components/contact-section.md +101 -0
- package/.claude/skills/components/expanding-panel.md +156 -0
- package/.claude/skills/components/eyebrow-text.md +25 -0
- package/.claude/skills/components/hero-text.md +25 -0
- package/.claude/skills/components/layout-grid-by-cols.md +147 -0
- package/.claude/skills/components/layout-row.md +35 -0
- package/.claude/skills/components/link-text.md +33 -0
- package/.claude/skills/components/page-hero-highlights.md +224 -0
- package/.claude/skills/components/services-card.md +28 -0
- package/.claude/skills/components/services-section.md +25 -0
- package/.claude/skills/components/stepper-list.md +227 -0
- package/.claude/skills/css-grid-max-width-gutters.md +67 -0
- package/.claude/skills/index.md +14 -3
- package/.claude/skills/storybook-add-story.md +60 -0
- package/.claude/skills/testing-add-unit-test.md +56 -0
- package/app/assets/styles/setup/01.config/index.css +0 -1
- package/app/assets/styles/setup/03.theming/default/_dark.css +2 -2
- package/app/assets/styles/setup/04.elements/forms/02.typography.css +1 -0
- package/app/assets/styles/setup/05.typography/02.utility-classes/_font-classes-page-link.css +14 -14
- package/app/assets/styles/setup/index.css +0 -1
- package/app/components/01.atoms/card/CardCore.vue +92 -0
- package/app/components/01.atoms/card/stories/CardCore.stories.ts +132 -0
- package/app/components/01.atoms/card/tests/CardCore.spec.ts +207 -0
- package/app/components/01.atoms/card/tests/__snapshots__/CardCore.spec.ts.snap +43 -0
- package/app/components/01.atoms/content-wrappers/content-columns-2/ContentColumns2.vue +51 -0
- package/app/components/01.atoms/content-wrappers/content-columns-2/stories/ContentColumns2.stories.ts +110 -0
- package/app/components/01.atoms/content-wrappers/content-columns-2/tests/ContentColumns2.spec.ts +105 -0
- package/app/components/01.atoms/content-wrappers/content-columns-2/tests/__snapshots__/ContentColumns2.spec.ts.snap +14 -0
- package/app/components/01.atoms/content-wrappers/content-width/ContentWidth.vue +88 -0
- package/app/components/01.atoms/content-wrappers/content-width/stories/ContentWidth.stories.ts +362 -0
- package/app/components/01.atoms/content-wrappers/content-width/tests/ContentWidth.spec.ts +132 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/LayoutGridByCols.vue +71 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/stories/LayoutGridByCols.stories.ts +219 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/LayoutGridByCols.spec.ts +174 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/__snapshots__/LayoutGrid.spec.ts.snap +36 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/__snapshots__/LayoutGridByCols.spec.ts.snap +36 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/LayoutGridByWidth.vue +70 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/stories/LayoutGridByWidth.stories.ts +220 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/LayoutGridByWidth.spec.ts +174 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/__snapshots__/LayoutGrid.spec.ts.snap +36 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/__snapshots__/LayoutGridByCols.spec.ts.snap +36 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/__snapshots__/LayoutGridByWidth.spec.ts.snap +36 -0
- package/app/components/01.atoms/text-blocks/eyebrow-text/stories/EyebrowText.stories.ts +1 -1
- package/app/components/01.atoms/text-blocks/hero-text/stories/HeroText.stories.ts +1 -1
- package/app/components/01.atoms/text-blocks/link-text/stories/LinkText.stories.ts +1 -1
- package/app/components/02.molecules/contact-section/stories/ContactSection.stories.ts +5 -0
- package/app/components/02.molecules/contact-section/tests/ContactSection.spec.ts +15 -0
- package/app/components/02.molecules/contact-section/tests/ContactSection.vue +25 -17
- package/app/components/{accordian → 02.molecules/expandable/accordian}/stories/AccordianCore.stories.ts +1 -1
- package/app/components/02.molecules/expandable/expanding-panel/stories/ExpandingPanel.stories.ts +245 -0
- package/app/components/02.molecules/expandable/expanding-panel/tests/ExpandingPanel.spec.ts +351 -0
- package/app/components/02.molecules/expandable/expanding-panel/tests/__snapshots__/ExpandingPanel.spec.ts.snap +38 -0
- package/app/components/02.molecules/navigation/navigation-horizontal/NavigationHorizontal.vue +139 -0
- package/app/components/02.molecules/navigation/navigation-horizontal/NavigationHorizontalAdvanced.vue +172 -0
- package/app/components/02.molecules/profile-section/ProfileSection.vue +2 -3
- package/app/components/02.molecules/profile-section/tests/ProfileSection.spec.ts +2 -2
- package/app/components/02.molecules/stepper-list/StepperList.vue +131 -92
- package/app/components/02.molecules/stepper-list/stories/StepperList.stories.ts +31 -0
- package/app/components/02.molecules/stepper-list/tests/StepperList.spec.ts +24 -0
- package/app/components/02.molecules/stepper-list/tests/__snapshots__/StepperList.spec.ts.snap +22 -9
- package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +782 -0
- package/app/components/03.organisms/image-galleries/slider-gallery/stories/SliderGallery.stories.ts +233 -0
- package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +226 -0
- package/app/components/03.organisms/image-galleries/slider-gallery/tests/__snapshots__/SliderGallery.spec.ts.snap +69 -0
- package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +1 -1
- package/app/components/03.organisms/services/services-grids/ServicesSectionGrid.vue +1 -1
- package/app/components/03.organisms/services/services-section/ServicesSection.vue +2 -3
- package/app/components/04.templates/page-hero-highlights/PageHeroHighlights.vue +239 -0
- package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlights.stories.ts +404 -0
- package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlights.spec.ts +198 -0
- package/app/components/04.templates/page-hero-highlights/tests/__snapshots__/PageHeroHighlights.spec.ts.snap +19 -0
- package/app/components/container-glow/ContainerGlowCore.vue +20 -27
- package/app/components/forms/input-button/InputButtonCore.vue +105 -104
- package/app/components/glowing-border/stories/GlowingBorder.stories.ts +21 -21
- package/app/composables/useAriaLabelledById.ts +13 -0
- package/app/layouts/default.vue +8 -3
- package/app/pages/forms/examples/buttons/index.vue +6 -6
- package/app/pages/forms/examples/material/checkbox-radio-panels.vue +3 -3
- package/app/pages/forms/examples/material/text-fields.vue +607 -610
- package/app/pages/page-hero-highlights.vue +81 -0
- package/app/pages/ui/{display-card.vue → card-core.vue} +15 -15
- package/app/pages/ui/contact-section.vue +1 -1
- package/app/pages/ui/container-glow.vue +1 -1
- package/app/pages/ui/content-width.vue +126 -0
- package/app/pages/ui/glowing-border.vue +9 -9
- package/app/pages/ui/navigation/navigation-horizontal.vue +493 -0
- package/app/pages/ui/services/services-section/[slug].vue +3 -1
- package/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,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"permissions": {
|
|
3
|
+
"allow": [
|
|
4
|
+
"Bash(npx vitest run app/components/01.atoms/content-wrappers/layout-grid/tests/LayoutGrid.spec.ts)",
|
|
5
|
+
"Bash(npx vitest run app/components/01.atoms/content-wrappers/layout-grid/tests/LayoutGrid.spec.ts --update-snapshots)",
|
|
6
|
+
"Bash(npx vitest run app/components/01.atoms/content-wrappers/layout-grid/tests/LayoutGrid.spec.ts -u)",
|
|
7
|
+
"Bash(npx vitest run app/components/01.atoms/content-wrappers/content-width/tests/ContentWidth.spec.ts)",
|
|
8
|
+
"Bash(node -e \":*)",
|
|
9
|
+
"Bash(git add app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/LayoutGridByCols.vue app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/LayoutGridByCols.spec.ts app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/__snapshots__/LayoutGridByCols.spec.ts.snap app/pages/forms/examples/buttons/index.vue app/pages/forms/examples/material/checkbox-radio-panels.vue app/pages/forms/examples/material/text-fields.vue)",
|
|
10
|
+
"Bash(git commit:*)",
|
|
11
|
+
"Bash(npx vitest run app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/LayoutGridByWidth.spec.ts)",
|
|
12
|
+
"Bash(npx vitest run app/components/01.atoms/card/tests/CardCore.spec.ts)",
|
|
13
|
+
"Bash(npx vitest run app/components/image-galleries/slider-gallery/tests/SliderGallery.spec.ts)",
|
|
14
|
+
"Bash(npx vitest run)",
|
|
15
|
+
"Bash(git mv:*)",
|
|
16
|
+
"Bash(npx tsc:*)",
|
|
17
|
+
"Bash(npx vitest:*)",
|
|
18
|
+
"Bash(git show:*)"
|
|
19
|
+
],
|
|
20
|
+
"additionalDirectories": [
|
|
21
|
+
"/Users/simoncornforth/websites/nuxt-components/app/components/01.atoms/content-wrappers/content-width",
|
|
22
|
+
"/Users/simoncornforth/websites/nuxt-components/.claude/skills"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# Aria Landmark Labelling
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Components that accept a `tag` prop may render as a semantic landmark element (`section`, `main`, `article`, `aside`). Landmarks benefit from an accessible name via `aria-labelledby` pointing to a heading inside them. The `useAriaLabelledById` composable handles this automatically.
|
|
6
|
+
|
|
7
|
+
## The composable
|
|
8
|
+
|
|
9
|
+
`app/composables/useAriaLabelledById.ts`
|
|
10
|
+
|
|
11
|
+
Returns `{ headingId, ariaLabelledby }`:
|
|
12
|
+
|
|
13
|
+
- `headingId` — a stable ID (via `useId()`) to place on the heading element inside the slot
|
|
14
|
+
- `ariaLabelledby` — computed: set to `headingId` when `tag` is a landmark, `undefined` otherwise (which removes the attribute entirely)
|
|
15
|
+
|
|
16
|
+
Labelled tags: `section`, `main`, `article`, `aside`.
|
|
17
|
+
|
|
18
|
+
## Usage in a component
|
|
19
|
+
|
|
20
|
+
```vue
|
|
21
|
+
<template>
|
|
22
|
+
<component
|
|
23
|
+
:is="tag"
|
|
24
|
+
:aria-labelledby="ariaLabelledby"
|
|
25
|
+
>
|
|
26
|
+
<slot name="header" :heading-id="headingId"></slot>
|
|
27
|
+
</component>
|
|
28
|
+
</template>
|
|
29
|
+
|
|
30
|
+
<script setup lang="ts">
|
|
31
|
+
interface Props {
|
|
32
|
+
tag?: "div" | "section" | "main";
|
|
33
|
+
}
|
|
34
|
+
const props = withDefaults(defineProps<Props>(), { tag: "div" });
|
|
35
|
+
|
|
36
|
+
const { headingId, ariaLabelledby } = useAriaLabelledById(() => props.tag);
|
|
37
|
+
</script>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The composable is auto-imported — no explicit import needed in `.vue` files.
|
|
41
|
+
|
|
42
|
+
## Consumer usage
|
|
43
|
+
|
|
44
|
+
When using `tag="section"` (or another landmark), the heading element inside the slot must consume `headingId`:
|
|
45
|
+
|
|
46
|
+
```vue
|
|
47
|
+
<MyComponent tag="section">
|
|
48
|
+
<template #header="{ headingId }">
|
|
49
|
+
<h1 :id="headingId">Page Title</h1>
|
|
50
|
+
</template>
|
|
51
|
+
</MyComponent>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
When `tag="div"` (default), the `aria-labelledby` attribute is absent and `headingId` may be ignored.
|
|
55
|
+
|
|
56
|
+
## Components already using this pattern
|
|
57
|
+
|
|
58
|
+
- `PageHeroHighlights` (04.templates)
|
|
59
|
+
- `ProfileSection` (02.molecules)
|
|
60
|
+
- `ServicesSection` (03.organisms)
|
|
61
|
+
- `LayoutGridByCols` (01.atoms)
|
|
62
|
+
- `LayoutGridByWidth` (01.atoms)
|
|
63
|
+
|
|
64
|
+
## Notes
|
|
65
|
+
|
|
66
|
+
- If a component does not expose a named slot with `:heading-id`, the `headingId` is still generated — the consumer simply places their own heading inside the slot without binding the id.
|
|
67
|
+
- `headingId` is stable across renders (SSR-safe via `useId()`).
|
|
68
|
+
- Do not replicate the old manual pattern (`const needsLabel = computed(() => props.tag === "section")`) — use this composable instead.
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# Dynamic Slot Patterns
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Two distinct patterns exist for dynamic slots in this project. Choose based on who controls
|
|
6
|
+
the slot structure — the **consumer** (named dynamic slots) or the **component** (indexed dynamic slots).
|
|
7
|
+
|
|
8
|
+
**Default to named dynamic slots.** Only use indexed dynamic slots when there is a specific
|
|
9
|
+
reason the component must know the count in advance (see decision guide below).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Pattern 1 — Named Dynamic Slots
|
|
14
|
+
|
|
15
|
+
The component renders whatever slots the consumer passes. Slot names are not known in advance.
|
|
16
|
+
Best for container/card/layout components.
|
|
17
|
+
|
|
18
|
+
### When to use
|
|
19
|
+
|
|
20
|
+
- The component is a wrapper/shell (card, panel, grid, dialog)
|
|
21
|
+
- Slot names are consumer-defined (semantic or indexed — doesn't matter)
|
|
22
|
+
- `itemCount` would only be used to drive the slot loop and nothing else
|
|
23
|
+
|
|
24
|
+
### Implementation
|
|
25
|
+
|
|
26
|
+
```vue
|
|
27
|
+
<template>
|
|
28
|
+
<component :is="tag" class="my-component">
|
|
29
|
+
<template v-for="(_, name) in $slots" :key="name">
|
|
30
|
+
<div>
|
|
31
|
+
<slot :name="name"></slot>
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
34
|
+
</component>
|
|
35
|
+
</template>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
No `useSlots()` call needed — `$slots` is available directly in the template.
|
|
39
|
+
|
|
40
|
+
### Slot name as CSS class
|
|
41
|
+
|
|
42
|
+
Since `name` is already available in the loop, it can be applied as a class on the wrapper element. This gives each slot section a targetable class derived automatically from the slot name — no extra props needed:
|
|
43
|
+
|
|
44
|
+
```vue
|
|
45
|
+
<template v-for="(_, name) in $slots" :key="name">
|
|
46
|
+
<div class="card-row" :class="`card-row-${name}`">
|
|
47
|
+
<slot :name="name"></slot>
|
|
48
|
+
</div>
|
|
49
|
+
</template>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
A consumer passing `#header` gets `<div class="card-row card-row-header">`, `#footer` gets `<div class="card-row card-row-footer">`, etc. The component's CSS can then target `.card-row-header`, `.card-row-footer` etc. for per-section styling.
|
|
53
|
+
|
|
54
|
+
### Consumer usage
|
|
55
|
+
|
|
56
|
+
Any slot names work — semantic or indexed:
|
|
57
|
+
|
|
58
|
+
```vue
|
|
59
|
+
<!-- Semantic names (card) -->
|
|
60
|
+
<CardCore variant="solid">
|
|
61
|
+
<template #header>...</template>
|
|
62
|
+
<template #body>...</template>
|
|
63
|
+
<template #footer>...</template>
|
|
64
|
+
</CardCore>
|
|
65
|
+
|
|
66
|
+
<!-- Indexed names (grid) — consumer still uses v-for with dynamic slot names -->
|
|
67
|
+
<LayoutGridByCols :column-count="3">
|
|
68
|
+
<template v-for="(item, i) in items" #[`item-${i}`] :key="i">
|
|
69
|
+
{{ item }}
|
|
70
|
+
</template>
|
|
71
|
+
</LayoutGridByCols>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Slots are rendered in document order.
|
|
75
|
+
|
|
76
|
+
### Reference components (named dynamic slots)
|
|
77
|
+
|
|
78
|
+
- `app/components/01.atoms/card/CardCore.vue`
|
|
79
|
+
- `app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/LayoutGridByCols.vue`
|
|
80
|
+
- `app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/LayoutGridByWidth.vue`
|
|
81
|
+
- `app/components/container-glow/ContainerGlowCore.vue`
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Pattern 2 — Indexed Dynamic Slots
|
|
86
|
+
|
|
87
|
+
The component declares how many slots exist via an `itemCount` prop. Slot names follow a
|
|
88
|
+
predictable pattern (`item-0`, `item-1`, …).
|
|
89
|
+
|
|
90
|
+
### When to use — only when itemCount is needed for logic beyond slot iteration
|
|
91
|
+
|
|
92
|
+
| Reason to keep `itemCount` | Example |
|
|
93
|
+
|
|
94
|
+
|---|---|
|
|
95
|
+
| Multiple slot types per item must be grouped into one rendered element | `AccordianCore` — `summary`/`icon`/`content` per `<ExpandingPanel>` |
|
|
96
|
+
| Two parallel loops must stay in sync for aria linking | `TabsCore` — nav `<li>` loop + content `<div>` loop linked by id |
|
|
97
|
+
| `itemCount` drives non-slot logic (z-index, CSS scope, counters) | `WipeAwayVertical` — `z-index: itemCount - key`, `timelineScope` computed |
|
|
98
|
+
| `$slots` inspection per iteration is needed to conditionally render | `StepperList` — checks `$slots[indicator-N]` to toggle class per `<li>` |
|
|
99
|
+
|
|
100
|
+
If none of these apply and `itemCount` is **only** used to generate the slot loop — convert to named dynamic slots.
|
|
101
|
+
|
|
102
|
+
### Implementation
|
|
103
|
+
|
|
104
|
+
```vue
|
|
105
|
+
<template>
|
|
106
|
+
<div class="my-grid">
|
|
107
|
+
<template v-for="index in itemCount" :key="index">
|
|
108
|
+
<slot :name="`item-${index - 1}`"></slot>
|
|
109
|
+
</template>
|
|
110
|
+
</div>
|
|
111
|
+
</template>
|
|
112
|
+
|
|
113
|
+
<script setup lang="ts">
|
|
114
|
+
interface Props {
|
|
115
|
+
itemCount: number;
|
|
116
|
+
}
|
|
117
|
+
const props = defineProps<Props>();
|
|
118
|
+
</script>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Multi-type indexed slots
|
|
122
|
+
|
|
123
|
+
Some components pair multiple slot types per index (e.g. TabsCore):
|
|
124
|
+
|
|
125
|
+
```vue
|
|
126
|
+
<template v-for="index in itemCount" :key="index">
|
|
127
|
+
<slot :name="`tab-${index}-trigger`"></slot>
|
|
128
|
+
<slot :name="`tab-${index}-content`"></slot>
|
|
129
|
+
</template>
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Reference components (indexed dynamic slots)
|
|
133
|
+
|
|
134
|
+
- `app/components/tabs/TabsCore.vue`
|
|
135
|
+
- `app/components/accordian/AccordianCore.vue`
|
|
136
|
+
- `app/components/02.molecules/stepper-list/StepperList.vue`
|
|
137
|
+
- `app/components/view-timeline/WipeAwayVertical.vue`
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Comparison
|
|
142
|
+
|
|
143
|
+
| | Named dynamic slots | Indexed dynamic slots |
|
|
144
|
+
|---|---|---|
|
|
145
|
+
| Slot names defined by | Consumer | Component |
|
|
146
|
+
| Number of slots | Open-ended | Fixed by `itemCount` prop |
|
|
147
|
+
| Template mechanism | `v-for="(_, name) in $slots"` | `v-for="index in itemCount"` |
|
|
148
|
+
| Typical use case | Card, grid, panel, layout wrapper | Tabs, accordion, stepper, timeline |
|
|
149
|
+
| Slot naming convention | Any (semantic or indexed) | Enforced (`item-0`, `tab-0-trigger`) |
|
|
150
|
+
| Default choice? | ✅ Yes | Only when count is needed for logic |
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Component Local Style Override
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
When including a component in a page or consuming component, visual customisation can be applied
|
|
6
|
+
locally using `styleClassPassthrough` combined with a scoped style block in the consuming file.
|
|
7
|
+
This avoids adding one-off props to the component and keeps customisation co-located with the usage.
|
|
8
|
+
|
|
9
|
+
No changes to the component are required.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Pattern
|
|
14
|
+
|
|
15
|
+
### 1. Pass a modifier class via styleClassPassthrough
|
|
16
|
+
|
|
17
|
+
```vue
|
|
18
|
+
<CardCore :style-class-passthrough="['featured-card']">
|
|
19
|
+
...
|
|
20
|
+
</CardCore>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 2. Add a style block in the consuming file
|
|
24
|
+
|
|
25
|
+
Scaffold this block when adding the component. Include a comment so future developers know it
|
|
26
|
+
is safe to delete if no overrides are needed.
|
|
27
|
+
|
|
28
|
+
```vue
|
|
29
|
+
<style>
|
|
30
|
+
/* ─── CardCore local overrides ─────────────────────────────────────
|
|
31
|
+
Customise the appearance of this instance via CSS custom properties or
|
|
32
|
+
direct overrides. Delete this block if no overrides are needed.
|
|
33
|
+
Colours, borders, geometry only — do not override behaviour (display, pointer-events, etc.)
|
|
34
|
+
─────────────────────────────────────────────────────────────────────────── */
|
|
35
|
+
.card-core {
|
|
36
|
+
&.featured-card {
|
|
37
|
+
/* Colours */
|
|
38
|
+
/* --_background-color: var(--brand-primary); */
|
|
39
|
+
/* --_border-color: var(--brand-secondary); */
|
|
40
|
+
|
|
41
|
+
/* Geometry */
|
|
42
|
+
/* border-radius: 1.6rem; */
|
|
43
|
+
|
|
44
|
+
/* Border / outline */
|
|
45
|
+
/* --_border-width: 0.3rem; */
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
</style>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## What to override
|
|
54
|
+
|
|
55
|
+
| Category | Examples | Approach |
|
|
56
|
+
|---|---|---|
|
|
57
|
+
| Colours | backgrounds, borders, text | CSS custom properties if the component exposes them, otherwise direct values |
|
|
58
|
+
| Geometry | border-radius, padding, gap | Direct property or `--_` private variable |
|
|
59
|
+
| Border / outline | width, style, colour | Direct property or `--_` private variable |
|
|
60
|
+
|
|
61
|
+
**Do not override behaviour** — `display`, `visibility`, `pointer-events`, `z-index`, animations.
|
|
62
|
+
Those belong in the component or a structural parent, not a style modifier.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## CSS custom property targeting
|
|
67
|
+
|
|
68
|
+
Components use `--_` prefixed private custom properties internally. These can be targeted via
|
|
69
|
+
a modifier class at higher specificity:
|
|
70
|
+
|
|
71
|
+
```css
|
|
72
|
+
/* Component internally defines: */
|
|
73
|
+
.my-component {
|
|
74
|
+
--_background-color: white;
|
|
75
|
+
background-color: var(--_background-color);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* Consumer overrides via modifier: */
|
|
79
|
+
.my-component {
|
|
80
|
+
&.my-modifier {
|
|
81
|
+
--_background-color: var(--brand-surface); /* CSS token */
|
|
82
|
+
/* or */
|
|
83
|
+
--_background-color: #f5f0eb; /* direct value */
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Note: `--_` properties are component-internal. If the component is updated and renames them,
|
|
89
|
+
the override will silently stop working. For shared/themeable overrides, prefer components that
|
|
90
|
+
expose `--theme-*` public variables instead.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## When to use this vs other approaches
|
|
95
|
+
|
|
96
|
+
| Situation | Approach |
|
|
97
|
+
|---|---|
|
|
98
|
+
| One-off visual tweak for a specific page/context | Local style override (this skill) |
|
|
99
|
+
| Consistent appearance across all instances site-wide | Default theme (`theming-override-default.md`) |
|
|
100
|
+
| Variant that belongs in the component itself | Add a `variant` prop value to the component |
|
|
101
|
+
| Structural layout change | Wrapper element or parent component |
|
|
102
|
+
|
|
103
|
+
### Component type guide
|
|
104
|
+
|
|
105
|
+
**Local overrides are appropriate for:**
|
|
106
|
+
|
|
107
|
+
- Display/content components — cards, panels, hero sections, media blocks
|
|
108
|
+
- Layout wrappers used in a specific visual context (e.g. a grid section with a tinted background)
|
|
109
|
+
- Any component whose appearance legitimately varies per page or usage context
|
|
110
|
+
|
|
111
|
+
**Keep styling global (theme/config) for:**
|
|
112
|
+
|
|
113
|
+
- Form elements and interactive controls — inputs, buttons, toggles, checkboxes
|
|
114
|
+
- Typography components used for consistency across the site
|
|
115
|
+
- Anything where visual inconsistency between instances would be a bug
|
|
116
|
+
|
|
117
|
+
The test: *should all instances of this component look the same?* If yes → theme. If instances are expected to look different → local override.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## Notes
|
|
122
|
+
|
|
123
|
+
- `styleClassPassthrough` accepts a string or array — pass an array when combining multiple modifiers.
|
|
124
|
+
- The modifier class lands on the component's root element, so nested element overrides need the
|
|
125
|
+
full selector path: `.card-core.featured-card .card-row-header { ... }`.
|
|
126
|
+
- Keep the style block close to the component usage in the template — don't put it in a global stylesheet.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Prop-Driven Layout Variation via data-* + CSS @container
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
How to vary CSS grid layout inside a `@container` query based on a component prop, using a `data-*` attribute as the CSS hook. Used in `ContentContainer` for the `justifyContent` prop.
|
|
6
|
+
|
|
7
|
+
## Pattern
|
|
8
|
+
|
|
9
|
+
### 1. Bind the prop as a class in the template
|
|
10
|
+
|
|
11
|
+
```vue
|
|
12
|
+
<div class="content-container" :class="justifyContent">
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### 2. Use `&.class` selectors inside the `@container` query
|
|
16
|
+
|
|
17
|
+
```css
|
|
18
|
+
@container content-container (width >= 1092px) {
|
|
19
|
+
/* default (center) */
|
|
20
|
+
--gutter: 0;
|
|
21
|
+
--content-max-width: 1064px;
|
|
22
|
+
--justify-content: center;
|
|
23
|
+
|
|
24
|
+
&.start {
|
|
25
|
+
--gutter: 16px;
|
|
26
|
+
--justify-content: start;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
&.end {
|
|
30
|
+
--gutter: 16px;
|
|
31
|
+
--justify-content: end;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
The `&` selector inside a `@container` rule refers to the element that carries the class.
|
|
37
|
+
|
|
38
|
+
## Notes
|
|
39
|
+
|
|
40
|
+
- **Why not `v-bind()` in `<style>`?** `v-bind()` injects a CSS custom property at the root element — it can't be targeted with a selector. A bound class gives you selector-level control, which is necessary when different values need different combinations of CSS property overrides. Prefer `:class="propName"` over `data-*` attributes for this — it's simpler.
|
|
41
|
+
- **`grid-column: gutter` vs `grid-column: content` when gutter is 0**: When named grid column tracks are 0px wide, `gutter-start`/`gutter-end` and `content-start`/`content-end` resolve to the same positions. A child spanning `gutter` and one spanning `content` are visually identical. You can safely use `grid-column: content` uniformly and remove conditional `grid-column` overrides.
|
|
42
|
+
- This pattern works with standard CSS nesting — no preprocessor required.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# AccordianCore Component
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`AccordianCore` renders a group of `ExpandingPanel` components. When a shared `name` prop is supplied, the native `<details>` behaviour ensures only one panel can be open at a time. Content is filled via **indexed dynamic slots** — one set per panel, driven by `itemCount`.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Dynamic slot pattern
|
|
10
|
+
|
|
11
|
+
For `itemCount="3"` the following slots exist:
|
|
12
|
+
|
|
13
|
+
| Slot | Purpose |
|
|
14
|
+
|------|---------|
|
|
15
|
+
| `#accordian-0-summary` | Clickable label for panel 0 |
|
|
16
|
+
| `#accordian-0-icon` | Custom toggle icon for panel 0 (optional) |
|
|
17
|
+
| `#accordian-0-content` | Expandable content for panel 0 |
|
|
18
|
+
| `#accordian-1-summary` | Clickable label for panel 1 |
|
|
19
|
+
| `#accordian-1-icon` | Custom toggle icon for panel 1 (optional) |
|
|
20
|
+
| `#accordian-1-content` | Expandable content for panel 1 |
|
|
21
|
+
| … | … |
|
|
22
|
+
|
|
23
|
+
**Rules:**
|
|
24
|
+
- Slots are **zero-indexed**: first panel = `accordian-0-*`, last = `accordian-{itemCount - 1}-*`.
|
|
25
|
+
- `accordian-{n}-summary` and `accordian-{n}-content` are the primary slots — always provide these.
|
|
26
|
+
- `accordian-{n}-icon` is optional. When omitted, the default `ExpandingPanel` caret icon is used.
|
|
27
|
+
- Always keep `itemCount` in sync with the number of slot sets you provide.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Props reference
|
|
32
|
+
|
|
33
|
+
| Prop | Type | Default | Notes |
|
|
34
|
+
|------|------|---------|-------|
|
|
35
|
+
| `name` | `string` | `undefined` | Shared `name` passed to every `ExpandingPanel`. When set, native `<details>` grouping means only one panel can be open at a time. Omit for independent panels. |
|
|
36
|
+
| `itemCount` | `number` | `0` | Number of `ExpandingPanel` components to render. |
|
|
37
|
+
| `animationDuration` | `number` | `300` | Expand/collapse animation duration in ms, forwarded to every panel. |
|
|
38
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root `.display-accordian` element. |
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Usage examples
|
|
43
|
+
|
|
44
|
+
### Basic accordion (exclusive open)
|
|
45
|
+
|
|
46
|
+
```vue
|
|
47
|
+
<AccordianCore name="faq" :item-count="3" :animation-duration="300">
|
|
48
|
+
<template #accordian-0-summary><span>What is your returns policy?</span></template>
|
|
49
|
+
<template #accordian-0-content>
|
|
50
|
+
<p>You can return any item within 30 days of purchase.</p>
|
|
51
|
+
</template>
|
|
52
|
+
|
|
53
|
+
<template #accordian-1-summary><span>How long does delivery take?</span></template>
|
|
54
|
+
<template #accordian-1-content>
|
|
55
|
+
<p>Standard delivery takes 3–5 working days.</p>
|
|
56
|
+
</template>
|
|
57
|
+
|
|
58
|
+
<template #accordian-2-summary><span>Do you ship internationally?</span></template>
|
|
59
|
+
<template #accordian-2-content>
|
|
60
|
+
<p>Yes — we ship to over 40 countries.</p>
|
|
61
|
+
</template>
|
|
62
|
+
</AccordianCore>
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Passing `name="faq"` groups all panels so only one can be open at a time.
|
|
66
|
+
|
|
67
|
+
### Independent panels (no name — each opens freely)
|
|
68
|
+
|
|
69
|
+
```vue
|
|
70
|
+
<AccordianCore :item-count="2">
|
|
71
|
+
<template #accordian-0-summary><span>Panel A</span></template>
|
|
72
|
+
<template #accordian-0-content><p>Content A</p></template>
|
|
73
|
+
|
|
74
|
+
<template #accordian-1-summary><span>Panel B</span></template>
|
|
75
|
+
<template #accordian-1-content><p>Content B</p></template>
|
|
76
|
+
</AccordianCore>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Custom icons per panel
|
|
80
|
+
|
|
81
|
+
```vue
|
|
82
|
+
<AccordianCore :item-count="2">
|
|
83
|
+
<template #accordian-0-summary><span>Section one</span></template>
|
|
84
|
+
<template #accordian-0-icon><span>+</span></template>
|
|
85
|
+
<template #accordian-0-content><p>Content one</p></template>
|
|
86
|
+
|
|
87
|
+
<template #accordian-1-summary><span>Section two</span></template>
|
|
88
|
+
<template #accordian-1-icon><span>+</span></template>
|
|
89
|
+
<template #accordian-1-content><p>Content two</p></template>
|
|
90
|
+
</AccordianCore>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Programmatic slot rendering (many items)
|
|
94
|
+
|
|
95
|
+
```vue
|
|
96
|
+
<AccordianCore :item-count="items.length">
|
|
97
|
+
<template v-for="(item, i) in items" #[`accordian-${i}-summary`] :key="i">
|
|
98
|
+
<span>{{ item.title }}</span>
|
|
99
|
+
</template>
|
|
100
|
+
<template v-for="(item, i) in items" #[`accordian-${i}-content`] :key="i">
|
|
101
|
+
<p>{{ item.body }}</p>
|
|
102
|
+
</template>
|
|
103
|
+
</AccordianCore>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## CSS custom properties
|
|
109
|
+
|
|
110
|
+
Override in a consuming component or theme block:
|
|
111
|
+
|
|
112
|
+
| Property | Effect |
|
|
113
|
+
|----------|--------|
|
|
114
|
+
| Applied via `accordian-item` class on each `ExpandingPanel` | Panels animate `margin-block-end` and `border-radius` alongside the expand transition — duration follows `animationDuration`. |
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## Local style override scaffold
|
|
119
|
+
|
|
120
|
+
When consuming this component, scaffold a style block using `styleClassPassthrough`. Delete the block if unused.
|
|
121
|
+
|
|
122
|
+
See [component-local-style-override.md](../component-local-style-override.md) for the full pattern.
|
|
123
|
+
|
|
124
|
+
```vue
|
|
125
|
+
<AccordianCore :style-class-passthrough="['my-accordian']" :item-count="3">
|
|
126
|
+
...
|
|
127
|
+
</AccordianCore>
|
|
128
|
+
|
|
129
|
+
<style>
|
|
130
|
+
/* ─── AccordianCore local overrides ────────────────────────────────
|
|
131
|
+
Colours, borders, geometry only — do not override behaviour.
|
|
132
|
+
Delete this block if no overrides are needed.
|
|
133
|
+
─────────────────────────────────────────────────────────────────── */
|
|
134
|
+
.display-accordian {
|
|
135
|
+
&.my-accordian {
|
|
136
|
+
/* Geometry */
|
|
137
|
+
/* max-width: none; */ /* default is 600px — remove the width cap */
|
|
138
|
+
|
|
139
|
+
/* Panel-level overrides via the .accordian-item hook */
|
|
140
|
+
.accordian-item.expanding-panel {
|
|
141
|
+
/* Border */
|
|
142
|
+
/* border-block-end: 1px solid currentColor; */
|
|
143
|
+
|
|
144
|
+
/* Geometry */
|
|
145
|
+
/* border-radius: 0.8rem; */
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
</style>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Notes
|
|
155
|
+
|
|
156
|
+
- `AccordianCore` always passes `style-class-passthrough="['accordian-item']"` to every inner `ExpandingPanel` — use `.accordian-item` as the hook for per-panel styling overrides.
|
|
157
|
+
- For a single standalone expand/collapse panel, use `ExpandingPanel` directly instead.
|
|
158
|
+
- Auto-imported in Nuxt — no manual import needed.
|
|
159
|
+
- File: `app/components/02.molecules/expandable/accordian/AccordianCore.vue`
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# ContactSection Component
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`ContactSection` is a two-column molecule that pairs a 3-item info list (using `StepperList` internally) with a contact form slot. On narrow viewports the columns stack; at 768 px and above they sit side by side.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Props reference
|
|
10
|
+
|
|
11
|
+
| Prop | Type | Default | Notes |
|
|
12
|
+
|------|------|---------|-------|
|
|
13
|
+
| `tag` | `"div" \| "section" \| "article" \| "main"` | `"div"` | Root HTML element. Use `"section"` when the landmark is meaningful. |
|
|
14
|
+
| `stepperIndicatorSize` | `string` | `"3rem"` | Passed through to the internal `StepperList` `indicatorSize` prop. Any valid CSS length. |
|
|
15
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root element. |
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Slot API
|
|
20
|
+
|
|
21
|
+
The component exposes 7 slots — 3 info-item slots, 3 indicator slots, and a form slot.
|
|
22
|
+
|
|
23
|
+
| Slot | Purpose |
|
|
24
|
+
|------|---------|
|
|
25
|
+
| `#item-0` | Content of the first info item |
|
|
26
|
+
| `#item-1` | Content of the second info item |
|
|
27
|
+
| `#item-2` | Content of the third info item |
|
|
28
|
+
| `#indicator-0` | Custom indicator icon for item 0 (optional — CSS counter bubble shown if omitted) |
|
|
29
|
+
| `#indicator-1` | Custom indicator icon for item 1 (optional) |
|
|
30
|
+
| `#indicator-2` | Custom indicator icon for item 2 (optional) |
|
|
31
|
+
| `#form` | Contact form or any right-column content |
|
|
32
|
+
|
|
33
|
+
Slots are **zero-indexed**. The internal `StepperList` is always rendered with `itemCount="3"` and `:connected="false"`.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Usage examples
|
|
38
|
+
|
|
39
|
+
### Default (no slots)
|
|
40
|
+
|
|
41
|
+
```vue
|
|
42
|
+
<ContactSection />
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Renders three placeholder `<p>` tags and an empty form column.
|
|
46
|
+
|
|
47
|
+
### With info content and a form
|
|
48
|
+
|
|
49
|
+
```vue
|
|
50
|
+
<ContactSection tag="section" :stepper-indicator-size="'2.4rem'">
|
|
51
|
+
<template #item-0>
|
|
52
|
+
<div>
|
|
53
|
+
<strong>Get in touch</strong>
|
|
54
|
+
<p class="page-body-normal">We'd love to hear from you.</p>
|
|
55
|
+
</div>
|
|
56
|
+
</template>
|
|
57
|
+
<template #item-1>
|
|
58
|
+
<div>
|
|
59
|
+
<strong>Email us</strong>
|
|
60
|
+
<p class="page-body-normal"><a href="mailto:hello@example.com">hello@example.com</a></p>
|
|
61
|
+
</div>
|
|
62
|
+
</template>
|
|
63
|
+
<template #item-2>
|
|
64
|
+
<div>
|
|
65
|
+
<strong>Call us</strong>
|
|
66
|
+
<p class="page-body-normal"><a href="tel:+441234567890">+44 1234 567 890</a></p>
|
|
67
|
+
</div>
|
|
68
|
+
</template>
|
|
69
|
+
<template #form>
|
|
70
|
+
<form>...</form>
|
|
71
|
+
</template>
|
|
72
|
+
</ContactSection>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### With custom indicator icons
|
|
76
|
+
|
|
77
|
+
```vue
|
|
78
|
+
<ContactSection>
|
|
79
|
+
<template #indicator-0>
|
|
80
|
+
<Icon name="lucide-map-pin" class="indicator-icon" />
|
|
81
|
+
</template>
|
|
82
|
+
<template #item-0>
|
|
83
|
+
<div>
|
|
84
|
+
<strong>Location</strong>
|
|
85
|
+
<p class="page-body-normal">123 High Street, Bath, BA1 1AA</p>
|
|
86
|
+
</div>
|
|
87
|
+
</template>
|
|
88
|
+
<!-- repeat for indicator-1/item-1, indicator-2/item-2 -->
|
|
89
|
+
</ContactSection>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Custom indicator content should use the `indicator-icon` class so the icon inherits the correct size and colour from `StepperList`.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Notes
|
|
97
|
+
|
|
98
|
+
- `stepperIndicatorSize` passes directly to the internal `StepperList` `indicatorSize` — use it to scale the indicator bubble/icon area. Default `"3rem"` is 30 px at the project's `62.5%` rem base.
|
|
99
|
+
- The internal `StepperList` always uses `tag="ul"`, `:connected="false"`, and `indicator-alignment="top"`. These are not configurable from `ContactSection`.
|
|
100
|
+
- Auto-imported in Nuxt — no manual import needed.
|
|
101
|
+
- See [stepper-list.md](stepper-list.md) for `StepperList` prop details and CSS custom property theming.
|