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.
Files changed (112) 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-export-types.md +61 -0
  5. package/.claude/skills/component-local-style-override.md +126 -0
  6. package/.claude/skills/component-prop-driven-container-layout.md +42 -0
  7. package/.claude/skills/components/accordian-core.md +159 -0
  8. package/.claude/skills/components/contact-section.md +101 -0
  9. package/.claude/skills/components/expanding-panel.md +156 -0
  10. package/.claude/skills/components/eyebrow-text.md +25 -0
  11. package/.claude/skills/components/hero-text.md +25 -0
  12. package/.claude/skills/components/layout-grid-by-cols.md +147 -0
  13. package/.claude/skills/components/layout-row.md +35 -0
  14. package/.claude/skills/components/link-text.md +33 -0
  15. package/.claude/skills/components/page-hero-highlights.md +224 -0
  16. package/.claude/skills/components/services-card.md +28 -0
  17. package/.claude/skills/components/services-section.md +25 -0
  18. package/.claude/skills/components/stepper-list.md +227 -0
  19. package/.claude/skills/css-grid-max-width-gutters.md +67 -0
  20. package/.claude/skills/index.md +15 -3
  21. package/.claude/skills/storybook-add-story.md +60 -0
  22. package/.claude/skills/testing-add-unit-test.md +56 -0
  23. package/app/assets/styles/setup/01.config/index.css +0 -1
  24. package/app/assets/styles/setup/03.theming/default/_dark.css +2 -2
  25. package/app/assets/styles/setup/04.elements/forms/02.typography.css +1 -0
  26. package/app/assets/styles/setup/05.typography/02.utility-classes/_font-classes-page-link.css +14 -14
  27. package/app/assets/styles/setup/index.css +0 -1
  28. package/app/components/01.atoms/card/CardCore.vue +92 -0
  29. package/app/components/01.atoms/card/stories/CardCore.stories.ts +132 -0
  30. package/app/components/01.atoms/card/tests/CardCore.spec.ts +207 -0
  31. package/app/components/01.atoms/card/tests/__snapshots__/CardCore.spec.ts.snap +43 -0
  32. package/app/components/01.atoms/content-wrappers/content-columns-2/ContentColumns2.vue +51 -0
  33. package/app/components/01.atoms/content-wrappers/content-columns-2/stories/ContentColumns2.stories.ts +110 -0
  34. package/app/components/01.atoms/content-wrappers/content-columns-2/tests/ContentColumns2.spec.ts +105 -0
  35. package/app/components/01.atoms/content-wrappers/content-columns-2/tests/__snapshots__/ContentColumns2.spec.ts.snap +14 -0
  36. package/app/components/01.atoms/content-wrappers/content-width/ContentWidth.vue +88 -0
  37. package/app/components/01.atoms/content-wrappers/content-width/stories/ContentWidth.stories.ts +362 -0
  38. package/app/components/01.atoms/content-wrappers/content-width/tests/ContentWidth.spec.ts +132 -0
  39. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/LayoutGridByCols.vue +71 -0
  40. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/stories/LayoutGridByCols.stories.ts +219 -0
  41. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/LayoutGridByCols.spec.ts +174 -0
  42. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/__snapshots__/LayoutGrid.spec.ts.snap +36 -0
  43. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/__snapshots__/LayoutGridByCols.spec.ts.snap +36 -0
  44. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/LayoutGridByWidth.vue +70 -0
  45. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/stories/LayoutGridByWidth.stories.ts +220 -0
  46. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/LayoutGridByWidth.spec.ts +174 -0
  47. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/__snapshots__/LayoutGrid.spec.ts.snap +36 -0
  48. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/__snapshots__/LayoutGridByCols.spec.ts.snap +36 -0
  49. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-width/tests/__snapshots__/LayoutGridByWidth.spec.ts.snap +36 -0
  50. package/app/components/01.atoms/text-blocks/eyebrow-text/stories/EyebrowText.stories.ts +1 -1
  51. package/app/components/01.atoms/text-blocks/hero-text/stories/HeroText.stories.ts +1 -1
  52. package/app/components/01.atoms/text-blocks/link-text/stories/LinkText.stories.ts +1 -1
  53. package/app/components/02.molecules/contact-section/stories/ContactSection.stories.ts +5 -0
  54. package/app/components/02.molecules/contact-section/tests/ContactSection.spec.ts +15 -0
  55. package/app/components/02.molecules/contact-section/tests/ContactSection.vue +25 -17
  56. package/app/components/{accordian → 02.molecules/expandable/accordian}/stories/AccordianCore.stories.ts +1 -1
  57. package/app/components/02.molecules/expandable/expanding-panel/stories/ExpandingPanel.stories.ts +245 -0
  58. package/app/components/02.molecules/expandable/expanding-panel/tests/ExpandingPanel.spec.ts +351 -0
  59. package/app/components/02.molecules/expandable/expanding-panel/tests/__snapshots__/ExpandingPanel.spec.ts.snap +38 -0
  60. package/app/components/02.molecules/navigation/navigation-horizontal/NavigationHorizontal.vue +162 -0
  61. package/app/components/02.molecules/navigation/navigation-horizontal/stories/NavigationHorizontal.stories.ts +373 -0
  62. package/app/components/02.molecules/navigation/navigation-horizontal/tests/NavigationHorizontal.spec.ts +152 -0
  63. package/app/components/02.molecules/navigation/navigation-horizontal/tests/__snapshots__/NavigationHorizontal.spec.ts.snap +17 -0
  64. package/app/components/02.molecules/profile-section/ProfileSection.vue +2 -3
  65. package/app/components/02.molecules/profile-section/tests/ProfileSection.spec.ts +2 -2
  66. package/app/components/02.molecules/stepper-list/StepperList.vue +131 -92
  67. package/app/components/02.molecules/stepper-list/stories/StepperList.stories.ts +31 -0
  68. package/app/components/02.molecules/stepper-list/tests/StepperList.spec.ts +24 -0
  69. package/app/components/02.molecules/stepper-list/tests/__snapshots__/StepperList.spec.ts.snap +22 -9
  70. package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +782 -0
  71. package/app/components/03.organisms/image-galleries/slider-gallery/stories/SliderGallery.stories.ts +233 -0
  72. package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +226 -0
  73. package/app/components/03.organisms/image-galleries/slider-gallery/tests/__snapshots__/SliderGallery.spec.ts.snap +69 -0
  74. package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +1 -1
  75. package/app/components/03.organisms/services/services-grids/ServicesSectionGrid.vue +1 -1
  76. package/app/components/03.organisms/services/services-section/ServicesSection.vue +2 -3
  77. package/app/components/04.templates/page-hero-highlights/PageHeroHighlights.vue +239 -0
  78. package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlights.stories.ts +404 -0
  79. package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlights.spec.ts +198 -0
  80. package/app/components/04.templates/page-hero-highlights/tests/__snapshots__/PageHeroHighlights.spec.ts.snap +19 -0
  81. package/app/components/container-glow/ContainerGlowCore.vue +20 -27
  82. package/app/components/forms/input-button/InputButtonCore.vue +105 -104
  83. package/app/components/glowing-border/stories/GlowingBorder.stories.ts +21 -21
  84. package/app/composables/useAriaLabelledById.ts +13 -0
  85. package/app/layouts/default.vue +8 -3
  86. package/app/pages/forms/examples/buttons/index.vue +6 -6
  87. package/app/pages/forms/examples/material/checkbox-radio-panels.vue +3 -3
  88. package/app/pages/forms/examples/material/text-fields.vue +607 -610
  89. package/app/pages/page-hero-highlights.vue +81 -0
  90. package/app/pages/ui/{display-card.vue → card-core.vue} +15 -15
  91. package/app/pages/ui/contact-section.vue +1 -1
  92. package/app/pages/ui/container-glow.vue +1 -1
  93. package/app/pages/ui/content-width.vue +126 -0
  94. package/app/pages/ui/glowing-border.vue +9 -9
  95. package/app/pages/ui/navigation/navigation-horizontal.vue +484 -0
  96. package/app/pages/ui/services/services-section/[slug].vue +3 -1
  97. package/app/types/components/index.ts +1 -0
  98. package/app/types/components/navigation-horizontal.d.ts +11 -0
  99. package/package.json +2 -2
  100. package/app/assets/styles/setup/01.config/_basic-resets.css +0 -9
  101. package/app/components/content-columns/TwoColumns.vue +0 -59
  102. package/app/components/content-columns/stories/TwoColumns.stories.ts +0 -561
  103. package/app/components/content-containers/ContentContainer.vue +0 -89
  104. package/app/components/content-containers/stories/ContentContainer.stories.ts +0 -465
  105. package/app/components/content-grid/ContentGrid.vue +0 -85
  106. package/app/components/display-card/DisplayCard.vue +0 -122
  107. package/app/components/image-galleries/SliderGallery.vue +0 -786
  108. package/app/pages/ui/content-container.vue +0 -112
  109. /package/app/components/{accordian → 02.molecules/expandable/accordian}/AccordianCore.vue +0 -0
  110. /package/app/components/{accordian → 02.molecules/expandable/accordian}/tests/AccordianCore.spec.ts +0 -0
  111. /package/app/components/{accordian → 02.molecules/expandable/accordian}/tests/__snapshots__/AccordianCore.spec.ts.snap +0 -0
  112. /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,61 @@
1
+ # Export Component Types for Consumers
2
+
3
+ ## Overview
4
+
5
+ Types defined inline in a `.vue` component file are not easily importable by consuming apps. This skill moves them into `app/types/components/` and re-exports via the barrel, so consumers can import from the package root.
6
+
7
+ ## Steps
8
+
9
+ ### 1. Create a types file
10
+
11
+ Create `app/types/components/<component-name>.d.ts` with the exported interfaces:
12
+
13
+ ```ts
14
+ // app/types/components/navigation-horizontal.d.ts
15
+ export interface NavItem {
16
+ text: string;
17
+ href?: string;
18
+ isExternal?: boolean;
19
+ iconName?: string;
20
+ cssName?: string;
21
+ }
22
+
23
+ export interface NavItemData {
24
+ [key: string]: NavItem[];
25
+ }
26
+ ```
27
+
28
+ ### 2. Add to the barrel
29
+
30
+ In `app/types/components/index.ts`, add an export line:
31
+
32
+ ```ts
33
+ export * from "./navigation-horizontal.d"
34
+ ```
35
+
36
+ ### 3. Update the component
37
+
38
+ Replace the inline `export interface` blocks in the `.vue` file with an import from the shared types file:
39
+
40
+ ```ts
41
+ // Before
42
+ export interface NavItem { ... }
43
+ export interface NavItemData { ... }
44
+
45
+ // After
46
+ import type { NavItem, NavItemData } from "~/types/components/navigation-horizontal.d";
47
+ ```
48
+
49
+ ## Consuming app usage
50
+
51
+ Once published, consumers import from the package barrel:
52
+
53
+ ```ts
54
+ import type { NavItem, NavItemData } from "nuxt-components/app/types/components";
55
+ ```
56
+
57
+ ## Notes
58
+
59
+ - The `.d.ts` extension is conventional for type-only files but is not required — plain `.ts` works too (see `hero-text.ts`).
60
+ - Keep the type file minimal: only types, no runtime code.
61
+ - If a type is already used by multiple components, consider a shared location like `app/types/components/shared.d.ts` rather than naming it after one component.
@@ -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`