srcdev-nuxt-components 9.1.50 → 9.1.52

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 (34) hide show
  1. package/.claude/settings.json +8 -27
  2. package/.claude/settings.local.json +24 -52
  3. package/.claude/skills/components/action-menu.md +141 -0
  4. package/.claude/skills/components/stepper-list.md +52 -18
  5. package/.claude/skills/css-nesting-conventions.md +121 -0
  6. package/.claude/skills/index.md +3 -0
  7. package/.claude/skills/pull-request-description.md +48 -0
  8. package/.claude/skills/testing-add-unit-test.md +40 -2
  9. package/README.md +26 -5
  10. package/app/components/02.molecules/action-menu/ActionMenu.vue +218 -0
  11. package/app/components/02.molecules/action-menu/ActionMenuItemCore.vue +123 -0
  12. package/app/components/02.molecules/action-menu/CONSUMER-STYLING.md +125 -0
  13. package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +326 -0
  14. package/app/components/02.molecules/action-menu/tests/ActionMenu.spec.ts +458 -0
  15. package/app/components/02.molecules/action-menu/tests/ActionMenuItemCore.spec.ts +199 -0
  16. package/app/components/02.molecules/action-menu/tests/__snapshots__/ActionMenu.spec.ts.snap +28 -0
  17. package/app/components/02.molecules/action-menu/tests/__snapshots__/ActionMenuItemCore.spec.ts.snap +27 -0
  18. package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +5 -1
  19. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +2 -1
  20. package/app/components/02.molecules/stepper-list/CONSUMER-STYLING.md +138 -0
  21. package/app/components/02.molecules/stepper-list/StepperList.vue +50 -24
  22. package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +5 -1
  23. package/app/components/05.forms/input-button/InputButtonCore.vue +1 -1
  24. package/app/components/carousels/tests/CarouselFlip.spec.ts +1 -0
  25. package/app/components/responsive-header/NavigationItems.vue +302 -81
  26. package/app/components/responsive-header/ResponsiveHeader.vue +360 -56
  27. package/app/layouts/default.vue +43 -352
  28. package/app/layouts/site-navigation-demo.vue +36 -34
  29. package/app/pages/page-hero-highlights.vue +3 -3
  30. package/app/pages/ui/scroll-reveal-image.vue +3 -3
  31. package/app/pages/ui/simple-grid.vue +9 -20
  32. package/modules/colour-scheme.ts +1 -1
  33. package/nuxt.config.ts +11 -2
  34. package/package.json +17 -16
@@ -1,33 +1,14 @@
1
1
  {
2
2
  "permissions": {
3
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
- "Edit(/.claude/skills/components/**)",
20
- "Bash(npx nuxi:*)",
21
- "Bash(npx vue-tsc:*)",
22
- "Bash(npm install:*)",
23
- "Bash(npm run:*)",
24
- "Bash(git add:*)",
25
- "Bash(gh release:*)",
26
- "Bash(node -e ':*)",
27
- "Bash(git ls-tree *)",
28
- "Bash(node -p \"require\\('./package.json'\\).version\")",
29
- "Bash(git status *)",
30
- "Bash(xargs ls -1)"
4
+ "Bash(grep -v \"^+\\\\s*$\\\\|^-\\\\s*$\")",
5
+ "Bash(python3 -c \"import json,sys; p=json.load\\(sys.stdin\\); print\\('main:', p.get\\('main'\\)\\); exp=p.get\\('exports',{}\\).get\\('.',{}\\); print\\('exports .:', exp\\)\")",
6
+ "Bash(node -e \"const p = require\\('/Users/simoncornforth/websites/nuxt-components/.output/server/node_modules/vue/package.json'\\); console.log\\(JSON.stringify\\(p.exports, null, 2\\)\\)\")",
7
+ "Bash(node .output/server/index.mjs)",
8
+ "Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:3000/)",
9
+ "Bash(kill %1)",
10
+ "Bash(xargs kill -9)",
11
+ "Edit(/.claude/skills/components/**)"
31
12
  ]
32
13
  }
33
14
  }
@@ -1,66 +1,38 @@
1
1
  {
2
- "hooks": {
3
- "PostToolUse": [
4
- {
5
- "matcher": "Bash",
6
- "hooks": [
7
- {
8
- "type": "command",
9
- "command": "grep -q '/Users/' .claude/settings.json 2>/dev/null && { echo 'settings.json contains absolute paths — move them to settings.local.json' >&2; exit 1; } || exit 0"
10
- }
11
- ]
12
- }
13
- ]
14
- },
15
2
  "permissions": {
16
3
  "allow": [
17
4
  "WebSearch",
18
- "mcp__ide__getDiagnostics",
19
5
  "WebFetch(domain:drafts.csswg.org)",
20
- "Bash(grep '\"\"typescript\"\"' package.json)",
21
- "Bash(grep '\"\"@vue/language-server\\\\|@volar\\\\|vue-tsc\"\"' package.json)",
22
- "Bash(npx nuxi prepare)",
6
+ "mcp__ide__getDiagnostics",
7
+ "Bash(npm run:*)",
8
+ "Bash(npm install:*)",
9
+ "Bash(npm audit *)",
10
+ "Bash(npm ls *)",
11
+ "Bash(npx vitest:*)",
12
+ "Bash(npx tsc:*)",
13
+ "Bash(npx vue-tsc:*)",
14
+ "Bash(npx nuxi:*)",
15
+ "Bash(git add:*)",
16
+ "Bash(git mv:*)",
17
+ "Bash(git show:*)",
18
+ "Bash(git status *)",
19
+ "Bash(git log:*)",
20
+ "Bash(git diff:*)",
21
+ "Bash(git ls-tree *)",
22
+ "Bash(git commit:*)",
23
+ "Bash(gh release:*)",
24
+ "Edit(/.claude/skills/components/**)",
25
+ "Bash(git -C /Users/simoncornforth/websites/nuxt-components *)",
23
26
  "Bash(ls -la /Users/simoncornforth/websites/luxury-locs-by-natasha-nuxt3/.env*)",
24
- "Bash(node -e \"const t = require\\('/Users/simoncornforth/websites/nuxt-components/node_modules/pinia-plugin-persistedstate'\\); console.log\\(Object.keys\\(t\\)\\)\")",
25
- "Bash(git -C /Users/simoncornforth/websites/nuxt-components log --oneline -5)",
26
- "Bash(git -C /Users/simoncornforth/websites/nuxt-components show 16ac4ef --stat)",
27
- "Bash(git -C /Users/simoncornforth/websites/nuxt-components log --oneline -8)",
28
- "Bash(git -C /Users/simoncornforth/websites/nuxt-components show 5597a15 --stat)",
29
- "Bash(git -C /Users/simoncornforth/websites/nuxt-components show 5597a15 -- \"*.vue\" \"*.css\")",
30
- "Bash(ls /Users/simoncornforth/websites/nuxt-components/*.md)",
31
- "Bash(git -C /Users/simoncornforth/websites/nuxt-components status)",
32
- "Bash(git -C /Users/simoncornforth/websites/nuxt-components add .claude/skills/components/services-section.md .claude/skills/components/services-section-grid.md .claude/skills/index.md app/components/03.organisms/services/services-grids/ServicesSectionGrid.vue app/components/03.organisms/services/services-section/ServicesSection.vue app/components/03.organisms/services/services-card/stories/ app/components/03.organisms/services/services-card/tests/ app/components/03.organisms/services/services-grids/stories/ app/components/03.organisms/services/services-grids/tests/ app/components/03.organisms/services/services-section/stories/ app/components/03.organisms/services/services-section/tests/)",
33
- "Bash(git -C /Users/simoncornforth/websites/nuxt-components commit -m ':*)",
34
- "Bash(git -C /Users/simoncornforth/websites/nuxt-components log --oneline v9.1.22..HEAD)",
35
- "Bash(git -C /Users/simoncornforth/websites/nuxt-components log --oneline c401cea..HEAD)",
36
- "Bash(git -C /Users/simoncornforth/websites/nuxt-components show 3058d63 --stat)",
37
- "Bash(git -C /Users/simoncornforth/websites/nuxt-components show 0f99d7a --stat)",
38
- "Bash(git -C /Users/simoncornforth/websites/nuxt-components diff app/components/parallax/SectionParallax.vue)",
39
- "Bash(git -C /Users/simoncornforth/websites/nuxt-components diff HEAD app/components/parallax/SectionParallax.vue)",
40
- "Bash(git -C /Users/simoncornforth/websites/nuxt-components diff --cached)",
41
- "Bash(SRCDEV_STANDALONE=true npx vitest --run app/components/02.molecules/samaritan-prompt)",
42
- "Bash(SRCDEV_STANDALONE=true npx vitest --run app/composables/tests/useCancellableTimer app/components/02.molecules/samaritan-prompt/tests/SamaritanPromptMixed)",
43
- "Bash(SRCDEV_STANDALONE=true npx vitest --run app/components/02.molecules/samaritan-prompt/tests/SamaritanPromptMixed)",
44
- "Bash(SRCDEV_STANDALONE=true npx vitest --run app/composables/tests/useCancellableTimer)",
45
- "Bash(SRCDEV_STANDALONE=true npm run test:run -- --reporter=verbose)",
46
- "Bash(cp /Users/simoncornforth/websites/nuxt-components/.claude/skills/components/data-grid.md /Users/simoncornforth/websites/nuxt-components/.claude/skills/components/auto-grid.md)",
47
- "Bash(rm /Users/simoncornforth/websites/nuxt-components/.claude/skills/components/data-grid.md)",
48
- "Bash(git -C /Users/simoncornforth/websites/nuxt-components log --oneline -10)",
49
- "Bash(git -C /Users/simoncornforth/websites/nuxt-components log 5a80426..HEAD --oneline)",
50
- "Bash(git -C /Users/simoncornforth/websites/nuxt-components show 71c276e --stat)",
51
- "Bash(git -C /Users/simoncornforth/websites/nuxt-components show a1e1641 --stat)"
27
+ "Bash(ls /Users/simoncornforth/websites/nuxt-components/*.md)"
52
28
  ],
53
29
  "additionalDirectories": [
54
- "/Users/simoncornforth/websites/nuxt-components/app/components/01.atoms/content-wrappers/content-width",
55
30
  "/Users/simoncornforth/websites/nuxt-components/.claude/skills",
56
31
  "/Users/simoncornforth/websites/nuxt-components/.claude/skills/components",
57
- "/Users/simoncornforth/websites/nuxt-components/app/components/02.molecules/navigation/site-navigation/tests",
58
- "/Users/simoncornforth/websites/luxury-locs-by-natasha-nuxt3/app/pages",
59
- "/Users/simoncornforth/websites/nuxt-components/app/assets/styles/extends-layer",
60
32
  "/Users/simoncornforth/websites/nuxt-components/app/components/01.atoms",
61
- "/private/tmp",
62
- "/Users/simoncornforth/websites/nuxt-components/app/components/01.atoms/banner-video",
63
- "/Users/simoncornforth/websites/nuxt-components/app/components/01.atoms/page-row"
33
+ "/Users/simoncornforth/websites/nuxt-components/app/assets/styles/extends-layer",
34
+ "/Users/simoncornforth/websites/luxury-locs-by-natasha-nuxt3/app/pages",
35
+ "/private/tmp"
64
36
  ]
65
37
  }
66
38
  }
@@ -0,0 +1,141 @@
1
+ # ActionMenu
2
+
3
+ ## Overview
4
+
5
+ `ActionMenu` is a trigger-and-popover component that shows a compact ellipsis button (`lucide:ellipsis`).
6
+ Clicking it opens an anchored menu list populated via indexed dynamic slots (`item-{n}`). Each slot
7
+ should contain a single `ActionMenuItemCore` — either a `<button>` (for actions) or a link (for
8
+ navigation). The popover API and CSS anchor positioning handle positioning and dismiss behaviour
9
+ natively; no JavaScript click-outside logic is needed.
10
+
11
+ **Location**: `app/components/02.molecules/action-menu/`
12
+
13
+ ---
14
+
15
+ ## Components
16
+
17
+ ### ActionMenu
18
+
19
+ | Prop | Type | Default | Notes |
20
+ |---|---|---|---|
21
+ | `itemCount` | `number` | `0` | Number of `item-{n}` slots to render. |
22
+ | `label` | `string` | `"Open actions menu"` | Used as `aria-label` on the trigger and `aria-label` on the menu list. |
23
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes on the root `<div>`. |
24
+
25
+ **Slots**
26
+
27
+ | Slot | When used |
28
+ |---|---|
29
+ | `item-{n}` | One per item, where `n` is 0-indexed up to `itemCount - 1`. Should contain one `ActionMenuItemCore`. |
30
+
31
+ ---
32
+
33
+ ### ActionMenuItemCore
34
+
35
+ | Prop | Type | Default | Notes |
36
+ |---|---|---|---|
37
+ | `label` | `string` | — | **Required.** Visible text for the row. |
38
+ | `href` | `string` | `undefined` | If set, renders as `<a>` (external) or `NuxtLink` (internal `/…` path). Omit for a `<button>`. |
39
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes on the root element. |
40
+
41
+ **Slots**
42
+
43
+ | Slot | Content |
44
+ |---|---|
45
+ | `#icon` | Optional left icon (e.g. `<Icon name="lucide:pencil" />`). Wrapped in `aria-hidden` span. |
46
+
47
+ **Emits**
48
+
49
+ | Event | Payload | Notes |
50
+ |---|---|---|
51
+ | `click` | `MouseEvent` | Fired on every click regardless of whether the item is a button or link. |
52
+
53
+ **Notes on routing**
54
+ - Internal paths (`/…`) resolve to `<NuxtLink>` via `resolveComponent`.
55
+ - External URLs or relative paths without a leading `/` render as plain `<a>`.
56
+ - `type="button"` is set automatically on `<button>` elements to prevent accidental form submission.
57
+
58
+ ---
59
+
60
+ ## Basic usage
61
+
62
+ ```vue
63
+ <ActionMenu :item-count="3" label="Row actions">
64
+ <template #item-0>
65
+ <ActionMenuItemCore label="Edit" @click="handleEdit">
66
+ <template #icon><Icon name="lucide:pencil" /></template>
67
+ </ActionMenuItemCore>
68
+ </template>
69
+ <template #item-1>
70
+ <ActionMenuItemCore label="View detail" href="/records/123">
71
+ <template #icon><Icon name="lucide:eye" /></template>
72
+ </ActionMenuItemCore>
73
+ </template>
74
+ <template #item-2>
75
+ <ActionMenuItemCore label="Delete" @click="handleDelete">
76
+ <template #icon><Icon name="lucide:trash-2" /></template>
77
+ </ActionMenuItemCore>
78
+ </template>
79
+ </ActionMenu>
80
+ ```
81
+
82
+ ---
83
+
84
+ ## Link vs button items
85
+
86
+ | Scenario | Use |
87
+ |---|---|
88
+ | Triggers a JS handler (delete, share, copy…) | Omit `href` — renders as `<button>` |
89
+ | Navigates to an internal Nuxt route | `href="/path"` — renders as `<NuxtLink>` |
90
+ | Navigates to an external URL | `href="https://…"` — renders as `<a>` |
91
+
92
+ ---
93
+
94
+ ## CSS token API
95
+
96
+ See `CONSUMER-STYLING.md` in the component folder for the full token reference and override
97
+ examples. Prefer global CSS for action menus — they appear site-wide in tables, cards, and lists.
98
+
99
+ Quick reference:
100
+
101
+ ```css
102
+ /* assets/styles/setup/07.components/action-menu.css */
103
+ :root {
104
+ --action-menu-block-distance: 0.6rem;
105
+ --action-menu-trigger-border-radius: 0.4rem;
106
+ --action-menu-trigger-surface-hover: var(--brand-surface-subtle);
107
+ --action-menu-trigger-icon-color: var(--brand-text-muted);
108
+
109
+ --action-menu-popover-background: var(--brand-surface);
110
+ --action-menu-popover-border: 0.1rem solid var(--brand-border);
111
+ --action-menu-popover-border-radius: 0.6rem;
112
+
113
+ --action-menu-item-surface-hover: var(--brand-surface-subtle);
114
+ --action-menu-item-text-color: var(--brand-text);
115
+ }
116
+ ```
117
+
118
+ ---
119
+
120
+ ## Notes
121
+
122
+ - **Popover API + CSS anchor positioning** — the menu uses `popover` attribute and `position-anchor`.
123
+ Both are broadly supported (Chrome 114+, Firefox 125+, Safari 17+). No polyfill is included.
124
+ - **Auto-close** — clicking any `<li>` row fires `hidePopover()` on the menu. The `ActionMenuItemCore`
125
+ emitting `click` triggers normally before the menu closes.
126
+ - **Focus management** — on open the `toggle` event fires `handleToggle`, which moves focus to the
127
+ first `[role="menuitem"]` inside the popover.
128
+ - **Keyboard navigation `currentIndex === -1` guard** — `handleKeydown` computes the current
129
+ position via `items.indexOf(document.activeElement)`. When focus is outside the menu this returns
130
+ `-1`. Always guard explicitly before applying wrap-around math: `ArrowDown` should focus
131
+ `items[0]`; `ArrowUp` should focus `items[items.length - 1]`. Without the guard, the modulo
132
+ formula gives `items[n-2]` for `ArrowUp` — the second-to-last item instead of the last.
133
+ - **Right-aligned by default** — the menu's right edge aligns with the trigger's right edge
134
+ (`right: anchor(right)`). Flips above the trigger near the bottom of the viewport
135
+ (`position-try-fallbacks: flip-block`).
136
+ - **`anchorName` format** — internally generated as `--action-menu-anchor-{id}` (a valid CSS
137
+ `<dashed-ident>`). Set via a CSS custom property on the root element so both the trigger's
138
+ `anchor-name` and the popover's `position-anchor` can reference the same value.
139
+ - **Dynamic slots stability** — `item-{n}` slots enforce that only `ActionMenuItemCore` content
140
+ enters the list; arbitrary HTML inside the popover is not supported and will break the ARIA
141
+ `menu` / `menuitem` pattern.
@@ -94,7 +94,7 @@ Counter bubbles are rendered automatically via CSS — no `#indicator-*` slots n
94
94
  </StepperList>
95
95
  ```
96
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`.
97
+ Custom indicator SVGs should use the `indicator-icon` class — it applies `color: var(--stepper-list-icon-color)` and sizes the icon to match `indicatorSize`.
98
98
 
99
99
  ### Mixed — custom icons for completed steps, counters for pending
100
100
 
@@ -146,21 +146,44 @@ Custom indicator SVGs should use the `indicator-icon` class — it applies `colo
146
146
 
147
147
  ## CSS custom properties
148
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 |
149
+ All `--stepper-list-*` tokens are the stable consumer override surface. See
150
+ [CONSUMER-STYLING.md](../../../app/components/02.molecules/stepper-list/CONSUMER-STYLING.md)
151
+ for full examples (global theming, page-scoped, and per-instance overrides).
152
+
153
+ ### Layout & sizing
154
+
155
+ | Token | Default | Controls |
156
+ |---|---|---|
157
+ | `--stepper-list-padding-block` | `1.2rem` | Vertical padding for each list item |
158
+ | `--stepper-list-gap` | `2.2rem` | Horizontal gap between indicator and content |
159
+ | `--stepper-list-counter-size` | prop `indicatorSize` (`3rem`) | Counter bubble / icon width and height |
160
+ | `--stepper-list-counter-font-size` | `1.4rem` | Counter number font size |
161
+ | `--stepper-list-counter-font-weight` | `600` | Counter number font weight |
162
+ | `--stepper-list-connector-width` | `0.2rem` | Connector line thickness |
163
+
164
+ ### Colours
165
+
166
+ | Token | Default | Controls |
167
+ |---|---|---|
168
+ | `--stepper-list-connector-color` | `currentColor` | Connector line colour |
169
+ | `--stepper-list-icon-color` | `currentColor` | Icon colour for custom `indicator-icon` elements |
170
+
171
+ ### Per-variant tokens
172
+
173
+ | Token | Default | Controls |
174
+ |---|---|---|
175
+ | `--stepper-list-counter-circle-background` | `transparent` | Circle bubble fill |
176
+ | `--stepper-list-counter-circle-text` | `currentColor` | Circle counter number colour |
177
+ | `--stepper-list-counter-circle-border` | `currentColor` | Circle bubble border colour |
178
+ | `--stepper-list-counter-circle-border-radius` | `100vw` | Circle bubble corner rounding |
179
+ | `--stepper-list-counter-disc-background` | `transparent` | Disc bubble fill |
180
+ | `--stepper-list-counter-disc-text` | `currentColor` | Disc counter number colour |
181
+ | `--stepper-list-counter-disc-border` | `transparent` | Disc bubble border colour |
182
+ | `--stepper-list-counter-disc-border-radius` | `100vw` | Disc bubble corner rounding |
183
+ | `--stepper-list-counter-square-background` | `transparent` | Square bubble fill |
184
+ | `--stepper-list-counter-square-text` | `currentColor` | Square counter number colour |
185
+ | `--stepper-list-counter-square-border` | `transparent` | Square bubble border colour |
186
+ | `--stepper-list-counter-square-border-radius` | `0.25rem` | Square bubble corner rounding |
164
187
 
165
188
  ---
166
189
 
@@ -192,23 +215,34 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
192
215
  ─────────────────────────────────────────────────────────────────── */
193
216
  .stepper-list {
194
217
  &.my-stepper {
218
+ /* Layout & sizing */
219
+ /* --stepper-list-padding-block: 1.2rem; */
220
+ /* --stepper-list-gap: 2.2rem; */
221
+ /* --stepper-list-counter-size: 3rem; */
222
+ /* --stepper-list-counter-font-size: 1.4rem; */
223
+ /* --stepper-list-counter-font-weight: 600; */
224
+ /* --stepper-list-connector-width: 0.2rem; */
225
+
195
226
  /* Counter bubble — disc variant */
196
227
  /* --stepper-list-counter-disc-background: var(--brand-primary); */
197
228
  /* --stepper-list-counter-disc-text: white; */
198
229
  /* --stepper-list-counter-disc-border: transparent; */
230
+ /* --stepper-list-counter-disc-border-radius: 100vw; */
199
231
 
200
232
  /* Counter bubble — circle variant */
201
233
  /* --stepper-list-counter-circle-background: transparent; */
202
234
  /* --stepper-list-counter-circle-text: var(--brand-primary); */
203
235
  /* --stepper-list-counter-circle-border: var(--brand-primary); */
236
+ /* --stepper-list-counter-circle-border-radius: 100vw; */
204
237
 
205
238
  /* Counter bubble — square variant */
206
239
  /* --stepper-list-counter-square-background: var(--brand-primary); */
207
240
  /* --stepper-list-counter-square-text: white; */
208
241
  /* --stepper-list-counter-square-border: transparent; */
242
+ /* --stepper-list-counter-square-border-radius: 0.25rem; */
209
243
 
210
244
  /* Custom indicator icon colour */
211
- /* --stepper-list-icon: var(--brand-primary); */
245
+ /* --stepper-list-icon-color: var(--brand-primary); */
212
246
 
213
247
  /* Connector line */
214
248
  /* --stepper-list-connector-color: var(--brand-primary); */
@@ -223,5 +257,5 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
223
257
 
224
258
  - Always keep `itemCount` in sync with the number of `#item-*` slots you provide — mismatches will render empty `<li>` rows.
225
259
  - 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.
260
+ - The `indicator-icon` class is defined inside the component CSS and sizes the SVG to `var(--_counter-size)` (driven by `indicatorSize` or `--stepper-list-counter-size`). Always add it to custom SVGs.
227
261
  - Auto-imported in Nuxt — no manual import needed.
@@ -0,0 +1,121 @@
1
+ # CSS Nesting Conventions
2
+
3
+ ## Overview
4
+
5
+ This project uses **native CSS nesting** (W3C CSS Nesting spec), not Sass/SCSS. They look similar but behave differently in one important way: native CSS does not support BEM-style `&` concatenation for child element selectors.
6
+
7
+ ## The key rule
8
+
9
+ > `&` must be followed by a combinator (space, `>`, `~`, `+`) or a selector starting with `.`, `#`, `:`, `[`, or `*`. It **cannot** be followed by a bare identifier or `__` prefix.
10
+
11
+ ## ❌ What NOT to do — Sass-style BEM concatenation
12
+
13
+ ```css
14
+ /* Sass/SCSS — does NOT work in native CSS */
15
+ .demo-controls {
16
+ padding: 1.6rem;
17
+
18
+ &__heading { /* ← Invalid native CSS — esbuild converts to :is(__heading) */
19
+ font-size: 1.1rem;
20
+ }
21
+
22
+ &__fields { /* ← Invalid native CSS */
23
+ display: flex;
24
+ }
25
+ }
26
+ ```
27
+
28
+ **Why it silently fails**: esbuild converts `&__heading` to `:is(__heading)`, which tries to match an HTML element named `__heading`. No such element exists, so the styles are never applied. There's no error — just missing styles.
29
+
30
+ The build will show this warning:
31
+ ```
32
+ ▲ [WARNING] Cannot use type selector "__heading" directly after nesting selector "&" [css-syntax-error]
33
+ CSS nesting syntax does not allow the "&" selector to come before a type selector.
34
+ ```
35
+
36
+ ## ✅ Correct patterns
37
+
38
+ ### Nested descendant (preferred for BEM child elements)
39
+
40
+ ```css
41
+ .demo-controls {
42
+ padding: 1.6rem;
43
+
44
+ & .demo-controls__heading { /* space + full class name */
45
+ font-size: 1.1rem;
46
+ }
47
+
48
+ & .demo-controls__fields {
49
+ display: flex;
50
+ }
51
+ }
52
+ ```
53
+
54
+ ### Flat top-level rules (also valid — avoids repetition for deeply nested structures)
55
+
56
+ ```css
57
+ .demo-controls {
58
+ padding: 1.6rem;
59
+ }
60
+
61
+ .demo-controls__heading {
62
+ font-size: 1.1rem;
63
+ }
64
+
65
+ .demo-controls__fields {
66
+ display: flex;
67
+ }
68
+ ```
69
+
70
+ ### Same-element modifier (this IS valid)
71
+
72
+ ```css
73
+ /* & followed by a class — matches the same element that also has this class */
74
+ .button {
75
+ background: blue;
76
+
77
+ &.button--large { /* ← valid: & immediately followed by . */
78
+ padding: 2rem;
79
+ }
80
+
81
+ &:hover { /* ← valid: & immediately followed by : */
82
+ background: darkblue;
83
+ }
84
+
85
+ &[disabled] { /* ← valid: & immediately followed by [ */
86
+ opacity: 0.5;
87
+ }
88
+ }
89
+ ```
90
+
91
+ ### Pseudo-elements and pseudo-classes
92
+
93
+ ```css
94
+ .component {
95
+ &::before { content: ""; } /* ✅ valid */
96
+ &::after { content: ""; } /* ✅ valid */
97
+ &:focus { outline: auto; } /* ✅ valid */
98
+ &:not(.active) { opacity: 0.5; } /* ✅ valid */
99
+ }
100
+ ```
101
+
102
+ ### Media / container queries inside a rule
103
+
104
+ ```css
105
+ .component {
106
+ grid-template-columns: 1fr;
107
+
108
+ @media (width >= 768px) {
109
+ grid-template-columns: 1fr 2fr; /* ✅ valid — query wraps the property */
110
+ }
111
+ }
112
+ ```
113
+
114
+ ## Spot-check during review
115
+
116
+ If you see `&__` or `&-` in a `.vue` `<style lang="css">` block, it's Sass syntax and will not work. Convert it to `& .full-class-name` or lift it to a top-level rule.
117
+
118
+ ## Related
119
+
120
+ - `CLAUDE.md` → Styling Methodology section
121
+ - `css-grid-max-width-gutters.md` — example of correct native nesting in a grid utility
@@ -33,6 +33,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
33
33
  ├── component-dynamic-slots.md — named dynamic slots ($slots iteration) vs indexed dynamic slots (itemCount pattern)
34
34
  ├── component-local-style-override.md — styleClassPassthrough + scoped style block for per-usage visual customisation
35
35
  ├── component-prop-driven-container-layout.md — vary CSS grid layout inside @container queries using data-* attribute selectors
36
+ ├── css-nesting-conventions.md — native CSS nesting rules: why &__child Sass BEM concatenation silently breaks, correct patterns
36
37
  ├── css-grid-max-width-gutters.md — cap a centre grid column width by growing gutters, with start/center alignment variants
37
38
  ├── css-animation-utilities.md — scroll-driven animation utility classes: scroller-x (carousel), entry-zoom-reveal, entry-slide-in, entry-exit-blur, auto-rotate
38
39
  ├── component-aria-landmark.md — useAriaLabelledById composable: aria-labelledby for section/main/article/aside tags
@@ -44,6 +45,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
44
45
  ├── new-app-scaffold.md — scaffold a new Nuxt consumer app extending this layer (package.json, nuxt.config, app structure, CLAUDE.md)
45
46
  ├── qa-panel.md — collapsible dev-only panel for toggling component props live on a page (demo pages and consuming apps)
46
47
  ├── release-notes.md — produce release notes as a fenced markdown block from git log
48
+ ├── pull-request-description.md — produce a PR description as a fenced markdown block from git diff vs main
47
49
  ├── composable-canonical-url.md — useCanonicalUrl: set <link rel="canonical"> from runtimeConfig.public.canonicalHost; layout setup, node types
48
50
  ├── composable-whatsapp.md — useWhatsApp: open pre-filled wa.me link from form payload; runtime config, security, usage
49
51
  ├── composable-zod-validation.md — useZodValidation: schema-driven form validation, error binding, submit flow, API error push
@@ -82,6 +84,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
82
84
  ├── auto-grid.md — AutoGrid: auto-fit responsive grid, $slots iteration, --auto-grid-min-col-size/gap tokens, semantic tag + aria
83
85
  ├── display-avatar.md — DisplayAvatar: circular avatar with image/initials fallback, size variants, chip badge, icon slot, styleClassPassthrough
84
86
  ├── card-core.md — CardCore: generic card container, dynamic named slots as rows, 4 variants, blurred backdrop layer, full CSS token API
87
+ ├── action-menu.md — ActionMenu + ActionMenuItemCore: ellipsis trigger + anchored popover menu, indexed item-{n} slots, link/button items, full CSS token API
85
88
  ├── display-dialog.md — DisplayDialog: native <dialog> overlay, 5 variants (dialog/modal/confirm/alert/fullscreen), useDialogControls integration, CSS token API
86
89
  ├── display-chip.md — DisplayChip: status indicator chip overlay, CSS trig positioning, circle/square shapes, status colours, icon/label content
87
90
  ├── display-pill.md — DisplayPill: pill/badge label with icon slot, 6 variants, 3 sizes, reversible order, full CSS token API for border/outline/colour
@@ -0,0 +1,48 @@
1
+ # Pull Request Description
2
+
3
+ ## Overview
4
+
5
+ When asked to create a pull request description, produce a fenced markdown code block (` ```markdown `) so the content can be copied and pasted directly into GitHub's PR body field without formatting being stripped.
6
+
7
+ ## Steps
8
+
9
+ ### 1. Identify the base branch
10
+
11
+ Run `git log --oneline main..HEAD` to list all commits on the current branch since it diverged from `main`.
12
+
13
+ ### 2. Review what changed
14
+
15
+ Run `git diff main...HEAD --stat` for a file-level summary, then `git diff main...HEAD` for the full diff. Focus on intent, not just mechanics.
16
+
17
+ ### 3. Produce a fenced markdown block
18
+
19
+ Always wrap the output in a ` ```markdown ` code fence — never render it as plain markdown.
20
+
21
+ ## Format
22
+
23
+ ```markdown
24
+ ## Summary
25
+
26
+ One or two sentences explaining what this PR does and why. No bullet points here — write it as prose.
27
+
28
+ ## Changes
29
+
30
+ - **`FileOrComponentName`** — what changed and why
31
+ - Keep each bullet to one line where possible
32
+
33
+ ## Testing
34
+
35
+ - How the change was verified (unit tests, manual check, build passing, etc.)
36
+ - Note any areas that couldn't be covered automatically
37
+
38
+ ## Notes
39
+
40
+ Any caveats, follow-up tickets, or decisions worth flagging for reviewers (optional — omit if nothing to say).
41
+ ```
42
+
43
+ ## Notes
44
+
45
+ - Only include sections that have content — omit empty headings (especially `Notes` if there's nothing to flag)
46
+ - Lead with the user-facing or functional change; CSS/test/doc tidy-ups can be secondary bullets
47
+ - Keep the tone factual — describe what changed, not the effort involved
48
+ - Do not include "Co-Authored-By" or other git trailer lines — those belong in the commit message, not the PR body
@@ -37,6 +37,7 @@ describe("ComponentName", () => {
37
37
 
38
38
  afterEach(() => {
39
39
  wrapper?.unmount();
40
+ vi.restoreAllMocks(); // always restore vi.spyOn() stubs after each test
40
41
  });
41
42
 
42
43
  // -------------------------
@@ -162,7 +163,7 @@ it("exposes headingId via scoped slot", async () => {
162
163
  ## Key rules
163
164
 
164
165
  - Always `mountSuspended` — never `mount` or `shallowMount` from `@vue/test-utils` directly.
165
- - Always `afterEach(() => wrapper?.unmount())` to prevent test leaks.
166
+ - Always call both `wrapper?.unmount()` and `vi.restoreAllMocks()` in `afterEach`. The unmount cleans up Vue; the restore cleans up any `vi.spyOn()` stubs so they don't leak into later test files.
166
167
  - Use a `createWrapper` helper to keep individual tests short.
167
168
  - Include at least one snapshot test per meaningful visual state.
168
169
  - `nextTick` is **not** auto-imported in test files — always import it explicitly: `import { nextTick } from "vue"`.
@@ -285,7 +286,9 @@ Import the child component directly in the test file — it is not auto-imported
285
286
 
286
287
  ## Mocking browser APIs
287
288
 
288
- Mock before the `describe` block if the component uses ResizeObserver, IntersectionObserver, etc.:
289
+ ### Global constructors (ResizeObserver, IntersectionObserver, etc.)
290
+
291
+ Use `vi.stubGlobal` before the `describe` block. Do **not** call `vi.unstubAllGlobals()` in `afterEach` — it removes stubs from `vitest.setup.ts` (`$fetch`, etc.):
289
292
 
290
293
  ```ts
291
294
  const mockResizeObserver = vi.fn(() => ({
@@ -296,6 +299,41 @@ const mockResizeObserver = vi.fn(() => ({
296
299
  vi.stubGlobal("ResizeObserver", mockResizeObserver);
297
300
  ```
298
301
 
302
+ ### Prototype methods (Popover API, Canvas, etc.)
303
+
304
+ When an API is missing from jsdom entirely (e.g. `hidePopover`, `showPopover`) use `Object.defineProperty` in `beforeEach`. **`vi.restoreAllMocks()` does not clean these up** — delete them explicitly in `afterEach`:
305
+
306
+ ```ts
307
+ beforeEach(() => {
308
+ // vi.spyOn stubs are cleaned by vi.restoreAllMocks() in afterEach
309
+ vi.spyOn(HTMLCanvasElement.prototype, "getContext").mockReturnValue({} as never);
310
+
311
+ // Object.defineProperty stubs are NOT cleaned by vi.restoreAllMocks() —
312
+ // must be deleted explicitly to prevent leaking into other test files
313
+ Object.defineProperty(HTMLElement.prototype, "hidePopover", {
314
+ value: vi.fn(),
315
+ writable: true,
316
+ configurable: true,
317
+ });
318
+ Object.defineProperty(HTMLElement.prototype, "showPopover", {
319
+ value: vi.fn(),
320
+ writable: true,
321
+ configurable: true,
322
+ });
323
+ });
324
+
325
+ afterEach(() => {
326
+ wrapper?.unmount();
327
+ vi.restoreAllMocks(); // cleans up vi.spyOn stubs
328
+ // Remove Object.defineProperty prototype stubs — vi.restoreAllMocks() won't touch these
329
+ delete (HTMLElement.prototype as unknown as Record<string, unknown>)["hidePopover"];
330
+ delete (HTMLElement.prototype as unknown as Record<string, unknown>)["showPopover"];
331
+ });
332
+ ```
333
+
334
+ The `as unknown as Record<string, unknown>` double-cast is required because TypeScript's
335
+ `HTMLElement` type has no index signature — cast through `unknown` first.
336
+
299
337
  ## Describe section conventions
300
338
 
301
339
  Use these section names consistently so tests are easy to scan: