srcdev-nuxt-components 9.3.0 → 9.3.1
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/hooks/check-component-update.sh +66 -0
- package/.claude/settings.json +1 -1
- package/.claude/skills/component-local-style-override.md +45 -0
- package/.claude/skills/components/accordian-core.md +26 -5
- package/.claude/skills/components/expanding-panel-classic.md +221 -0
- package/.claude/skills/components/expanding-panel.md +8 -8
- package/.claude/skills/components/navigation-items.md +51 -0
- package/.claude/skills/components/responsive-header.md +161 -0
- package/.claude/skills/components/site-header.md +98 -0
- package/.claude/skills/index.md +6 -1
- package/.claude/skills/page-transitions.md +116 -0
- package/.claude/skills/theming-form-geometry-tokens.md +12 -0
- package/.claude/skills/theming-override-default.md +14 -0
- package/.claude/skills/theming-partial-override.md +17 -0
- package/.vscode/srcdev-component-accordian-core.code-snippets +74 -0
- package/.vscode/srcdev-component-expanding-panel-classic.code-snippets +121 -0
- package/.vscode/srcdev-component-expanding-panel.code-snippets +1 -3
- package/.vscode/srcdev-component-responsive-header.code-snippets +46 -0
- package/.vscode/srcdev-component-site-header.code-snippets +74 -0
- package/app/components/02.molecules/expandable/accordian/AccordianCore.vue +19 -11
- package/app/components/02.molecules/expandable/accordian/CONSUMER-STYLING.md +93 -0
- package/app/components/02.molecules/expandable/accordian/stories/AccordianCore.stories.ts +31 -0
- package/app/components/02.molecules/expandable/accordian/tests/AccordianCore.spec.ts +31 -0
- package/app/components/02.molecules/expandable/accordian/tests/__snapshots__/AccordianCore.spec.ts.snap +7 -21
- package/app/components/02.molecules/expandable/expanding-panel/CONSUMER-STYLING.md +10 -10
- package/app/components/02.molecules/expandable/expanding-panel/ExpandingPanel.vue +35 -37
- package/app/components/02.molecules/expandable/expanding-panel/stories/ExpandingPanel.stories.ts +5 -6
- package/app/components/02.molecules/expandable/expanding-panel/tests/__snapshots__/ExpandingPanel.spec.ts.snap +5 -13
- package/app/components/02.molecules/expandable/expanding-panel-classic/CONSUMER-STYLING.md +103 -0
- package/app/components/02.molecules/expandable/expanding-panel-classic/ExpandingPanelClassic.vue +191 -0
- package/app/components/02.molecules/expandable/expanding-panel-classic/stories/ExpandingPanelClassic.stories.ts +293 -0
- package/app/components/02.molecules/expandable/expanding-panel-classic/tests/ExpandingPanelClassic.spec.ts +514 -0
- package/app/components/02.molecules/expandable/expanding-panel-classic/tests/__snapshots__/ExpandingPanelClassic.spec.ts.snap +59 -0
- package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +66 -0
- package/app/components/{responsive-header → 03.organisms/responsive-header}/NavigationItems.vue +21 -28
- package/app/components/{responsive-header → 03.organisms/responsive-header}/ResponsiveHeader.vue +141 -43
- package/app/components/03.organisms/responsive-header/stories/NavigationItems.stories.ts +80 -0
- package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +122 -0
- package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +155 -0
- package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +319 -0
- package/app/components/03.organisms/responsive-header/tests/__snapshots__/NavigationItems.spec.ts.snap +34 -0
- package/app/components/03.organisms/responsive-header/tests/__snapshots__/ResponsiveHeader.spec.ts.snap +72 -0
- package/app/components/03.organisms/site-header/CONSUMER-STYLING.md +60 -0
- package/app/components/03.organisms/site-header/SiteHeader.vue +96 -0
- package/app/components/03.organisms/site-header/stories/SiteHeader.stories.ts +135 -0
- package/app/components/03.organisms/site-header/tests/SiteHeader.spec.ts +103 -0
- package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +15 -0
- package/app/layouts/default.vue +64 -67
- package/package.json +4 -1
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# PostToolUse hook (Write|Edit) for app/components/*.vue files. Advisory nudges only
|
|
3
|
+
# (missing != wrong): skill doc, story, test, CONSUMER-STYLING.md, .vscode snippet,
|
|
4
|
+
# legacy tier-folder location, options-style defineProps.
|
|
5
|
+
|
|
6
|
+
root="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
7
|
+
|
|
8
|
+
f=$(jq -r '.tool_input.file_path // empty')
|
|
9
|
+
|
|
10
|
+
case "$f" in
|
|
11
|
+
*/app/components/*.vue) ;;
|
|
12
|
+
*) exit 0 ;;
|
|
13
|
+
esac
|
|
14
|
+
case "$f" in
|
|
15
|
+
*/tests/*|*/stories/*) exit 0 ;;
|
|
16
|
+
esac
|
|
17
|
+
|
|
18
|
+
name=$(basename "$f" .vue)
|
|
19
|
+
skill=$(printf '%s' "$name" | sed -E 's/([a-z0-9])([A-Z])/\1-\2/g; s/([A-Z]+)([A-Z][a-z])/\1-\2/g' | tr '[:upper:]' '[:lower:]')
|
|
20
|
+
dir=$(dirname "$f")
|
|
21
|
+
pdir=$(dirname "$dir")
|
|
22
|
+
|
|
23
|
+
# variants/ files are documented under their parent component, not standalone.
|
|
24
|
+
is_variant=false
|
|
25
|
+
if [[ "$(basename "$dir")" == "variants" ]]; then
|
|
26
|
+
is_variant=true
|
|
27
|
+
parent_name=$(basename "$pdir")
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
if [[ "$is_variant" == true ]]; then
|
|
31
|
+
msg="Component file $f was edited/written. This is a variant of $parent_name — per project convention, document it inside .claude/skills/components/$parent_name.md (e.g. a \"Variants\" section) rather than creating a separate .claude/skills/components/$skill.md for it."
|
|
32
|
+
else
|
|
33
|
+
msg="Component file $f was edited/written. Per Claude.md Development Workflow step 7, check whether .claude/skills/components/$skill.md needs updating to reflect this change (props/slots/models/defaults/behaviour)."
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
shopt -s nullglob
|
|
37
|
+
stories=("$dir"/stories/*.stories.ts "$pdir"/stories/*.stories.ts)
|
|
38
|
+
tests=("$dir"/tests/*.spec.ts "$pdir"/tests/*.spec.ts)
|
|
39
|
+
shopt -u nullglob
|
|
40
|
+
|
|
41
|
+
if [[ ${#stories[@]} -eq 0 ]]; then
|
|
42
|
+
msg="$msg No Storybook story found for this component (checked $dir/stories and $pdir/stories); create one."
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
if [[ ${#tests[@]} -eq 0 ]]; then
|
|
46
|
+
msg="$msg No test spec found for this component (checked $dir/tests and $pdir/tests); create one per the Testing Requirements."
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
if [[ "$is_variant" != true && ! -f "$dir/CONSUMER-STYLING.md" && ! -f "$pdir/CONSUMER-STYLING.md" ]]; then
|
|
50
|
+
msg="$msg No CONSUMER-STYLING.md found for this component; create one if it exposes a real --token API or class override hook (skip it if the component genuinely has neither)."
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
if [[ ! -f "$root/.vscode/srcdev-component-$skill.code-snippets" ]]; then
|
|
54
|
+
msg="$msg No .vscode/srcdev-component-$skill.code-snippets found; create/update it per Development Workflow step 6."
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
case "$f" in
|
|
58
|
+
*/app/components/01.atoms/*|*/app/components/02.molecules/*|*/app/components/03.organisms/*|*/app/components/04.templates/*|*/app/components/05.forms/*) ;;
|
|
59
|
+
*) msg="$msg This component lives outside the 01-05 tier folders, a legacy location; move it into the matching tier (01.atoms, 02.molecules, 03.organisms, 04.templates, 05.forms) or propose a new tier folder if none fit." ;;
|
|
60
|
+
esac
|
|
61
|
+
|
|
62
|
+
if grep -qE 'defineProps\(\s*\{' "$f" && ! grep -q 'defineProps<' "$f"; then
|
|
63
|
+
msg="$msg This component uses the options-style defineProps({...}) pattern, an outdated-pattern signal; migrate to interface Props + withDefaults(defineProps<Props>(), {...}) per the Props Pattern in Claude.md."
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
jq -n --arg msg "$msg" '{hookSpecificOutput:{hookEventName:"PostToolUse",additionalContext:$msg}}'
|
package/.claude/settings.json
CHANGED
|
@@ -114,7 +114,7 @@
|
|
|
114
114
|
"hooks": [
|
|
115
115
|
{
|
|
116
116
|
"type": "command",
|
|
117
|
-
"command": "
|
|
117
|
+
"command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/check-component-update.sh\"",
|
|
118
118
|
"timeout": 15
|
|
119
119
|
}
|
|
120
120
|
]
|
|
@@ -8,6 +8,19 @@ on context.
|
|
|
8
8
|
|
|
9
9
|
No changes to the layer component are required for either pattern.
|
|
10
10
|
|
|
11
|
+
> **⚠️ Never wrap consumer-app override `<style>` blocks in a named `@layer`** (e.g.
|
|
12
|
+
> `@layer consumer`), even though library components wrap their own styles in `@layer components`
|
|
13
|
+
> and it's tempting to mirror that. Cascade layer priority is fixed by whichever layer name is
|
|
14
|
+
> first referenced anywhere in the document, not by its position in the library's master
|
|
15
|
+
> `@layer reset, colours, theming, form-tokens, typography, a11y, components, utilities, consumer;`
|
|
16
|
+
> statement. Nuxt/Nitro inlines many small per-component/per-page CSS chunks as `<style>` tags
|
|
17
|
+
> directly in `<head>` for SSR performance — if an inlined page/component chunk declares its own
|
|
18
|
+
> `@layer` before the library's main stylesheet (carrying that master statement) loads via its
|
|
19
|
+
> `<link>`, layer order gets scrambled and the library's own layers can silently win instead, with
|
|
20
|
+
> no build error (confirmed against a real `npm run build` + preview, not just `nuxt dev`). Keep
|
|
21
|
+
> override `<style>` blocks unlayered — unlayered CSS always beats every named layer regardless of
|
|
22
|
+
> document order, which is what makes both patterns below work reliably.
|
|
23
|
+
|
|
11
24
|
---
|
|
12
25
|
|
|
13
26
|
## Pattern 1 — Page-level scoping (preferred for single-use or section-scoped instances)
|
|
@@ -64,6 +77,38 @@ useHead({ bodyAttrs: { class: "contact-page" } })
|
|
|
64
77
|
}
|
|
65
78
|
```
|
|
66
79
|
|
|
80
|
+
> **⚠️ Do not use `bodyAttrs.class` as the scope for a page's *own* local `<style>` overrides if the
|
|
81
|
+
> app uses `pageTransition`/`layoutTransition` (see `page-transitions.md`) — it races and breaks
|
|
82
|
+
> mid-transition. `unhead` swaps `<body>`'s class the instant the *incoming* route's component sets
|
|
83
|
+
> up, which happens as soon as navigation starts — not when the *outgoing* page's leave-transition
|
|
84
|
+
> finishes animating. For a real transition duration (not an instant swap), the outgoing page is
|
|
85
|
+
> still visible and mid-fade while `<body>` already carries the *new* page's class. Any selector
|
|
86
|
+
> that depends on the old body class as an ancestor (`.contact-page .hero-bg-image { position:
|
|
87
|
+
> absolute; ... }`) stops matching mid-fade, and the affected element snaps to unstyled/intrinsic
|
|
88
|
+
> sizing for the rest of the transition — confirmed via direct DOM/computed-style polling against a
|
|
89
|
+
> real `npm run build` + preview, not dev-server guesswork.
|
|
90
|
+
>
|
|
91
|
+
> Keep `bodyAttrs.class` for its legitimate use — a hook for *persistent* components (header, nav)
|
|
92
|
+
> that live outside the transitioning page to react to "which page is active." For a page's own
|
|
93
|
+
> local overrides, put a matching class directly on the page's own template root instead, so the
|
|
94
|
+
> scope lives on the exact element that's fading and can never desync from what's rendered:
|
|
95
|
+
>
|
|
96
|
+
> ```vue
|
|
97
|
+
> <template>
|
|
98
|
+
> <div class="contact-page-content">
|
|
99
|
+
> <div class="hero-section">...</div>
|
|
100
|
+
> </div>
|
|
101
|
+
> </template>
|
|
102
|
+
> <style lang="css">
|
|
103
|
+
> .contact-page-content {
|
|
104
|
+
> .hero-section { ... }
|
|
105
|
+
> }
|
|
106
|
+
> </style>
|
|
107
|
+
> ```
|
|
108
|
+
>
|
|
109
|
+
> Naming convention: `{name}-page-content` alongside the existing `{name}-page` body class keeps
|
|
110
|
+
> the two purposes visually distinct.
|
|
111
|
+
|
|
67
112
|
---
|
|
68
113
|
|
|
69
114
|
## Pattern 2 — Per-instance modifier via styleClassPassthrough
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
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`.
|
|
5
|
+
`AccordianCore` renders a group of `ExpandingPanel` components (or `ExpandingPanelClassic`, via the `variant` prop). 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
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
@@ -35,6 +35,7 @@ For `itemCount="3"` the following slots exist:
|
|
|
35
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
36
|
| `itemCount` | `number` | `0` | Number of `ExpandingPanel` components to render. |
|
|
37
37
|
| `animationDuration` | `number` | `300` | Expand/collapse animation duration in ms, forwarded to every panel. |
|
|
38
|
+
| `variant` | `"modern" \| "classic"` | `"modern"` | `"modern"` renders each panel as `ExpandingPanel` (`::details-content`-based, Baseline "newly available" Sept 2025 — the animation itself only runs where `interpolate-size` is supported, Chromium only as of 2026). `"classic"` renders `ExpandingPanelClassic` instead (`grid-template-rows`-based, animates identically in every browser). Applies to every panel in the group — mixing variants within one `AccordianCore` isn't supported. See [expanding-panel-classic.md](expanding-panel-classic.md). |
|
|
38
39
|
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root `.display-accordian` element. |
|
|
39
40
|
|
|
40
41
|
---
|
|
@@ -90,6 +91,18 @@ Passing `name="faq"` groups all panels so only one can be open at a time.
|
|
|
90
91
|
</AccordianCore>
|
|
91
92
|
```
|
|
92
93
|
|
|
94
|
+
### Classic variant (animation must run in every browser)
|
|
95
|
+
|
|
96
|
+
```vue
|
|
97
|
+
<AccordianCore name="faq" :item-count="2" variant="classic">
|
|
98
|
+
<template #accordian-0-summary><span>Question one?</span></template>
|
|
99
|
+
<template #accordian-0-content><p>Answer one.</p></template>
|
|
100
|
+
|
|
101
|
+
<template #accordian-1-summary><span>Question two?</span></template>
|
|
102
|
+
<template #accordian-1-content><p>Answer two.</p></template>
|
|
103
|
+
</AccordianCore>
|
|
104
|
+
```
|
|
105
|
+
|
|
93
106
|
### Programmatic slot rendering (many items)
|
|
94
107
|
|
|
95
108
|
```vue
|
|
@@ -107,11 +120,16 @@ Passing `name="faq"` groups all panels so only one can be open at a time.
|
|
|
107
120
|
|
|
108
121
|
## CSS custom properties
|
|
109
122
|
|
|
123
|
+
`AccordianCore` has no `--accordian-*` tokens of its own — its panels are `ExpandingPanel`
|
|
124
|
+
(or `ExpandingPanelClassic`, per `variant`) underneath, so `--expanding-panel-*` tokens apply
|
|
125
|
+
directly. See `CONSUMER-STYLING.md` in the component directory for the full override reference,
|
|
126
|
+
including the `.accordian-item` hook and the variant-dependent root class.
|
|
127
|
+
|
|
110
128
|
Override in a consuming component or theme block:
|
|
111
129
|
|
|
112
130
|
| Property | Effect |
|
|
113
131
|
|----------|--------|
|
|
114
|
-
| Applied via `accordian-item` class on each
|
|
132
|
+
| Applied via `accordian-item` class on each panel | Panels animate `margin-block-end` and `border-radius` alongside the expand transition — duration follows `animationDuration`. |
|
|
115
133
|
|
|
116
134
|
---
|
|
117
135
|
|
|
@@ -136,7 +154,9 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
|
|
|
136
154
|
/* Geometry */
|
|
137
155
|
/* max-width: none; */ /* default is 600px — remove the width cap */
|
|
138
156
|
|
|
139
|
-
/* Panel-level overrides via the .accordian-item hook
|
|
157
|
+
/* Panel-level overrides via the .accordian-item hook — .expanding-panel is the
|
|
158
|
+
root class rendered by the default "modern" variant; swap in
|
|
159
|
+
.expanding-panel-classic if this instance uses variant="classic" */
|
|
140
160
|
.accordian-item.expanding-panel {
|
|
141
161
|
/* Border */
|
|
142
162
|
/* border-block-end: 1px solid currentColor; */
|
|
@@ -153,7 +173,8 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
|
|
|
153
173
|
|
|
154
174
|
## Notes
|
|
155
175
|
|
|
156
|
-
- `AccordianCore` always passes `style-class-passthrough="['accordian-item']"` to every inner
|
|
157
|
-
-
|
|
176
|
+
- `AccordianCore` always passes `style-class-passthrough="['accordian-item']"` to every inner panel — use `.accordian-item` as the hook for per-panel styling overrides.
|
|
177
|
+
- `variant` applies to the whole group — every panel in a given `AccordianCore` renders as the same component. Switching `variant` at runtime swaps every panel's underlying component (via `<component :is>`), which resets each panel's open/closed state.
|
|
178
|
+
- For a single standalone expand/collapse panel, use `ExpandingPanel` (or `ExpandingPanelClassic`) directly instead.
|
|
158
179
|
- Auto-imported in Nuxt — no manual import needed.
|
|
159
180
|
- File: `app/components/02.molecules/expandable/accordian/AccordianCore.vue`
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# ExpandingPanelClassic Component
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
**Prefer [`ExpandingPanel`](expanding-panel.md) unless you have a concrete reason not to.** `ExpandingPanelClassic` is the pre-`::details-content` implementation, kept only as a fallback for consumers who need the open/close animation to work identically in every browser today (its `grid-template-rows: 0fr → 1fr` sibling trick has no Baseline-2025 feature dependency). `ExpandingPanel` looks and behaves identically everywhere too — the only difference is that its animation degrades to an instant (unanimated) open/close in browsers without `interpolate-size` support (Chromium only, as of 2026), where this component still animates.
|
|
6
|
+
|
|
7
|
+
`ExpandingPanelClassic` is a single expand/collapse panel built on the native `<details>`/`<summary>` element. It animates open/close via a CSS grid-template-rows trick, supports `v-model` for controlled state, and can be locked open with `forceOpened`. Multiple panels can be grouped into a native accordion by sharing the same `name` prop (see `AccordianCore`).
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Props reference
|
|
12
|
+
|
|
13
|
+
| Prop | Type | Default | Notes |
|
|
14
|
+
|------|------|---------|-------|
|
|
15
|
+
| `name` | `string` | `useId()` | Identifies the panel. Used in ARIA attributes (`id-{name}-trigger`, `id-{name}-content`). If omitted, a unique id is generated automatically. |
|
|
16
|
+
| `animationDuration` | `number` | `400` | Expand/collapse transition duration in milliseconds. Pass `0` to disable animation. |
|
|
17
|
+
| `forceOpened` | `boolean` | `false` | When `true`, the panel is always open. The toggle icon is hidden and clicks do not close the panel. |
|
|
18
|
+
| `contentIsOnTop` | `boolean` | `false` | When `true`, the content region is taken out of flow and absolutely positioned directly below the summary, raised above surrounding page content via `z-index` — instead of pushing layout down when it opens. Applies a `content-is-on-top` class to the root `.expanding-panel-classic` element (not the content div). Also enables click-outside-to-close (see below) — not applied when `forceOpened` is `true`. |
|
|
19
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root `.expanding-panel-classic` element. |
|
|
20
|
+
|
|
21
|
+
## Model
|
|
22
|
+
|
|
23
|
+
| Model | Type | Default | Notes |
|
|
24
|
+
|-------|------|---------|-------|
|
|
25
|
+
| `v-model` | `boolean` | `false` | Controls open/closed state. Bind to a `ref<boolean>` to manage state externally. |
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Slots
|
|
30
|
+
|
|
31
|
+
| Slot | Purpose |
|
|
32
|
+
|------|---------|
|
|
33
|
+
| `#summary` | Content rendered inside the clickable `<summary>` row (label area). |
|
|
34
|
+
| `#icon` | Custom toggle icon. Defaults to a `bi:caret-down-fill` icon that flips on open. Hidden when `forceOpened` is `true`. |
|
|
35
|
+
| `#content` | Content revealed when the panel is open. Can contain any markup. |
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
## Usage examples
|
|
40
|
+
|
|
41
|
+
### Basic uncontrolled panel
|
|
42
|
+
|
|
43
|
+
```vue
|
|
44
|
+
<ExpandingPanelClassic name="delivery">
|
|
45
|
+
<template #summary>
|
|
46
|
+
<span>Delivery & Returns</span>
|
|
47
|
+
</template>
|
|
48
|
+
<template #content>
|
|
49
|
+
<p>Free standard delivery on orders over £50.</p>
|
|
50
|
+
</template>
|
|
51
|
+
</ExpandingPanelClassic>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Controlled via v-model
|
|
55
|
+
|
|
56
|
+
```vue
|
|
57
|
+
<script setup lang="ts">
|
|
58
|
+
const isOpen = ref(false);
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<template>
|
|
62
|
+
<ExpandingPanelClassic name="faq-1" v-model="isOpen">
|
|
63
|
+
<template #summary><span>What is your returns policy?</span></template>
|
|
64
|
+
<template #content><p>You can return any item within 30 days.</p></template>
|
|
65
|
+
</ExpandingPanelClassic>
|
|
66
|
+
<button @click="isOpen = !isOpen">Toggle externally</button>
|
|
67
|
+
</template>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Force opened (always visible, no toggle)
|
|
71
|
+
|
|
72
|
+
```vue
|
|
73
|
+
<ExpandingPanelClassic name="notice" :force-opened="true">
|
|
74
|
+
<template #summary><strong>Important notice</strong></template>
|
|
75
|
+
<template #content>
|
|
76
|
+
<p>This panel cannot be collapsed.</p>
|
|
77
|
+
</template>
|
|
78
|
+
</ExpandingPanelClassic>
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Custom icon
|
|
82
|
+
|
|
83
|
+
```vue
|
|
84
|
+
<ExpandingPanelClassic name="custom-icon">
|
|
85
|
+
<template #summary><span>Section title</span></template>
|
|
86
|
+
<template #icon>
|
|
87
|
+
<svg width="12" height="12" viewBox="0 0 12 12">
|
|
88
|
+
<path d="M6 9L1 3h10z" fill="currentColor" />
|
|
89
|
+
</svg>
|
|
90
|
+
</template>
|
|
91
|
+
<template #content><p>Content here.</p></template>
|
|
92
|
+
</ExpandingPanelClassic>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Slow animation
|
|
96
|
+
|
|
97
|
+
```vue
|
|
98
|
+
<ExpandingPanelClassic name="slow" :animation-duration="800">
|
|
99
|
+
<template #summary><span>Slow panel</span></template>
|
|
100
|
+
<template #content><p>Opens and closes over 800 ms.</p></template>
|
|
101
|
+
</ExpandingPanelClassic>
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Content on top (overlay instead of pushing layout)
|
|
105
|
+
|
|
106
|
+
```vue
|
|
107
|
+
<ExpandingPanelClassic name="overlay" :content-is-on-top="true" :style-class-passthrough="['my-overlay-panel']">
|
|
108
|
+
<template #summary><span>Open me — content overlays what's below</span></template>
|
|
109
|
+
<template #content>
|
|
110
|
+
<!-- Wrapper INSIDE the slot carries the visual styling — see
|
|
111
|
+
"Styling the content when contentIsOnTop" below for why it can't go on .inner -->
|
|
112
|
+
<div class="my-overlay-panel-body">
|
|
113
|
+
<p>Positioned absolutely below the summary, doesn't push page content down.</p>
|
|
114
|
+
</div>
|
|
115
|
+
</template>
|
|
116
|
+
</ExpandingPanelClassic>
|
|
117
|
+
|
|
118
|
+
<style>
|
|
119
|
+
.my-overlay-panel-body {
|
|
120
|
+
background-color: white;
|
|
121
|
+
padding: 1rem;
|
|
122
|
+
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
|
|
123
|
+
}
|
|
124
|
+
</style>
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## ARIA / accessibility
|
|
130
|
+
|
|
131
|
+
The component wires ARIA automatically from the `name` prop:
|
|
132
|
+
|
|
133
|
+
| Element | Attribute | Value |
|
|
134
|
+
|---------|-----------|-------|
|
|
135
|
+
| `<summary>` | `id` | `id-{name}-trigger` |
|
|
136
|
+
| `<summary>` | `aria-controls` | `id-{name}-content` |
|
|
137
|
+
| `<summary>` | `aria-expanded` | `true` / `false` |
|
|
138
|
+
| content div | `id` | `id-{name}-content` |
|
|
139
|
+
| content div | `aria-labelledby` | `id-{name}-trigger` |
|
|
140
|
+
| content div | `role` | `region` |
|
|
141
|
+
|
|
142
|
+
Always supply a meaningful `name` prop when using multiple panels on the same page to avoid duplicate IDs.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Styling the content when contentIsOnTop
|
|
147
|
+
|
|
148
|
+
When `contentIsOnTop` is `true`, the component deliberately does **not** set `background-color`, `padding`, or a shadow on `.inner` — and consumers must not set them on `.inner` either. Always style a wrapper element placed *inside* the `#content` slot (see example above).
|
|
149
|
+
|
|
150
|
+
Why: `.expanding-panel-classic-content` collapses via `grid-template-rows: 0fr → 1fr`, and `.inner` relies on `overflow: hidden` on its own box to clip its *children* to 0px when collapsed. `overflow: hidden` clips overflow content, but does not shrink the element's own padding/border/background — those are part of `.inner`'s own box model and still render at full size even while the row track is `0fr` and the panel is closed, producing a visible gap under the summary. A wrapper placed inside the slot is a *child* of `.inner`, so its box — including any padding/background/shadow — is correctly clipped to 0px by `.inner`'s `overflow: hidden` while closed. Baking styling into `.inner` itself would require also gating it on the open state (e.g. `.expanding-panel-classic-details[open] ~ .expanding-panel-classic-content .inner`), which is unnecessary complexity — styling the slot content is the correct fix, not a workaround.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Constraint: don't stack contentIsOnTop panels as direct siblings
|
|
155
|
+
|
|
156
|
+
`contentIsOnTop` takes the content out of document flow (`position: absolute`) specifically so opening the panel does **not** push whatever comes after it down the page — that's the entire point of the prop. The tradeoff: the panel's own container still only occupies the height of its `<summary>` row, so a *sibling* element positioned directly after it in the DOM sits exactly where the overlay renders. When that sibling is another `ExpandingPanelClassic`, opening the first one visually covers the second one's summary — this happens whether or not the two are grouped via a shared `name` (linked accordion) or opened simultaneously; it isn't specific to linking.
|
|
157
|
+
|
|
158
|
+
`contentIsOnTop` is designed for a **single** panel overlaying unrelated trailing page content (e.g. a promo banner, a footer strip) — not for stacking multiple `contentIsOnTop` panels beside each other expecting normal accordion behaviour. If you need several linked/stacked panels, leave `contentIsOnTop` off (the default, in-flow layout handles that case correctly).
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Click-outside-to-close (contentIsOnTop only)
|
|
163
|
+
|
|
164
|
+
When `contentIsOnTop` is `true` and the panel is open, clicking anywhere outside the panel's root element closes it (via `@vueuse/core`'s `onClickOutside`, sets `v-model` to `false`) — matching the dismissal behaviour of a native `<select>` or dropdown menu, since that's what an overlay panel functionally is. This does **not** apply to ordinary in-flow panels (`contentIsOnTop: false`, the default) — an inline accordion staying open when you click elsewhere on the page is expected, not a bug. It also never applies when `forceOpened` is `true`, regardless of `contentIsOnTop` — a forced-open panel isn't dismissible by any interaction.
|
|
165
|
+
|
|
166
|
+
If you also want the panel to close when something *inside* it is activated (e.g. a nav link) — clicking outside doesn't cover that case — set `v-model` to `false` from that element's own click handler, same as any other controlled usage. See `ContentDocs`' `docsNav`/`docsPageNav` panels for a real example: both are `contentIsOnTop` on mobile, and each nav link closes its panel on click in addition to relying on click-outside.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## CSS Token Customization
|
|
171
|
+
|
|
172
|
+
All `--expanding-panel-classic-*` tokens can be overridden at global, page, or instance scope. See `CONSUMER-STYLING.md` in the component directory for full token documentation and examples.
|
|
173
|
+
|
|
174
|
+
**Tokens:**
|
|
175
|
+
|
|
176
|
+
- `--expanding-panel-classic-summary-gap` — gap between summary label and icon
|
|
177
|
+
- `--expanding-panel-classic-summary-padding-block` — summary row vertical padding
|
|
178
|
+
- `--expanding-panel-classic-icon-size` — toggle icon size
|
|
179
|
+
- `--expanding-panel-classic-content-z-index` — stacking order when `contentIsOnTop` is `true`
|
|
180
|
+
- `--expanding-panel-classic-content-gap` — space between summary and content when `contentIsOnTop` is `true`
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Local style override scaffold
|
|
185
|
+
|
|
186
|
+
When consuming this component, scaffold a style block using `styleClassPassthrough`. Delete the block if unused.
|
|
187
|
+
|
|
188
|
+
See [component-local-style-override.md](../component-local-style-override.md) for the full pattern.
|
|
189
|
+
|
|
190
|
+
```vue
|
|
191
|
+
<ExpandingPanelClassic name="my-item" :style-class-passthrough="['my-panel']">
|
|
192
|
+
...
|
|
193
|
+
</ExpandingPanelClassic>
|
|
194
|
+
|
|
195
|
+
<style>
|
|
196
|
+
/* ─── ExpandingPanelClassic local overrides ───────────────────────────────
|
|
197
|
+
Colours, borders, geometry only — do not override behaviour.
|
|
198
|
+
Delete this block if no overrides are needed.
|
|
199
|
+
─────────────────────────────────────────────────────────────────── */
|
|
200
|
+
.expanding-panel-classic {
|
|
201
|
+
&.my-panel {
|
|
202
|
+
/* Border */
|
|
203
|
+
/* border-block-end: 1px solid currentColor; */
|
|
204
|
+
|
|
205
|
+
/* Summary row geometry */
|
|
206
|
+
/* .expanding-panel-classic-details .expanding-panel-classic-summary { padding-block: 1.2rem; } */
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
</style>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
---
|
|
213
|
+
|
|
214
|
+
## Notes
|
|
215
|
+
|
|
216
|
+
- The open/close animation uses `grid-template-rows: 0fr → 1fr` — no JS height measurement needed.
|
|
217
|
+
- `content-is-on-top` is applied to the root `.expanding-panel-classic` element, not `.expanding-panel-classic-content` — style overrides must scope through it, e.g. `.expanding-panel-classic.my-panel .expanding-panel-classic-content .inner { ... }`.
|
|
218
|
+
- When `forceOpened` is `true`, `open` stays `true` regardless of `v-model`, but `v-model` still updates internally on clicks (useful if you later set `forceOpened` back to `false`).
|
|
219
|
+
- Group panels into a native accordion (only one open at a time) by passing the same `name` to multiple panels or use `AccordianCore` which handles this automatically.
|
|
220
|
+
- Auto-imported in Nuxt — no manual import needed.
|
|
221
|
+
- File: `app/components/02.molecules/expandable/expanding-panel-classic/ExpandingPanelClassic.vue`
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## Overview
|
|
4
4
|
|
|
5
|
-
`ExpandingPanel` is a single expand/collapse panel built on the native `<details>`/`<summary>` element. It animates open/close
|
|
5
|
+
`ExpandingPanel` is a single expand/collapse panel built on the native `<details>`/`<summary>` element. It animates open/close by styling the `::details-content` pseudo-element (the browser's own anonymous box wrapping everything after `<summary>`), supports `v-model` for controlled state, and can be locked open with `forceOpened`. Multiple panels can be grouped into a native accordion by sharing the same `name` prop (see `AccordianCore`).
|
|
6
|
+
|
|
7
|
+
`::details-content` is Baseline "newly available" (Sept 2025) — Chrome, Firefox, and Safari all generate the box. The height *animation* itself depends on `interpolate-size: allow-keywords`, which as of 2026 only runs in Chromium; other browsers still open/close correctly, just without the transition (accepted progressive-enhancement trade-off, not a bug).
|
|
8
|
+
|
|
9
|
+
If a consumer needs the animation to run identically in every browser today, use [`ExpandingPanelClassic`](expanding-panel-classic.md) instead — same props/model/slots API, older `grid-template-rows` implementation with no Baseline-2025 dependency.
|
|
6
10
|
|
|
7
11
|
---
|
|
8
12
|
|
|
@@ -105,8 +109,6 @@ const isOpen = ref(false);
|
|
|
105
109
|
<ExpandingPanel name="overlay" :content-is-on-top="true" :style-class-passthrough="['my-overlay-panel']">
|
|
106
110
|
<template #summary><span>Open me — content overlays what's below</span></template>
|
|
107
111
|
<template #content>
|
|
108
|
-
<!-- Wrapper INSIDE the slot carries the visual styling — see
|
|
109
|
-
"Styling the content when contentIsOnTop" below for why it can't go on .inner -->
|
|
110
112
|
<div class="my-overlay-panel-body">
|
|
111
113
|
<p>Positioned absolutely below the summary, doesn't push page content down.</p>
|
|
112
114
|
</div>
|
|
@@ -143,9 +145,7 @@ Always supply a meaningful `name` prop when using multiple panels on the same pa
|
|
|
143
145
|
|
|
144
146
|
## Styling the content when contentIsOnTop
|
|
145
147
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
Why: `.expanding-panel-content` collapses via `grid-template-rows: 0fr → 1fr`, and `.inner` relies on `overflow: hidden` on its own box to clip its *children* to 0px when collapsed. `overflow: hidden` clips overflow content, but does not shrink the element's own padding/border/background — those are part of `.inner`'s own box model and still render at full size even while the row track is `0fr` and the panel is closed, producing a visible gap under the summary. A wrapper placed inside the slot is a *child* of `.inner`, so its box — including any padding/background/shadow — is correctly clipped to 0px by `.inner`'s `overflow: hidden` while closed. Baking styling into `.inner` itself would require also gating it on the open state (e.g. `.expanding-panel-details[open] ~ .expanding-panel-content .inner`), which is unnecessary complexity — styling the slot content is the correct fix, not a workaround.
|
|
148
|
+
`background-color`, `padding`, and a shadow can be set directly on `.expanding-panel-content` (or on a wrapper inside the `#content` slot — either works). The clipping boundary is now `::details-content` itself (`overflow: clip`, animated `height`), and `.expanding-panel-content` is a *child* of that box — so its own box, including padding/border/background, is correctly clipped to the animated height regardless of where the styling lives. This is a change from the previous `.inner`-based implementation, which had to clip its own box and so couldn't carry visual styling directly.
|
|
149
149
|
|
|
150
150
|
---
|
|
151
151
|
|
|
@@ -211,8 +211,8 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
|
|
|
211
211
|
|
|
212
212
|
## Notes
|
|
213
213
|
|
|
214
|
-
- The open/close animation
|
|
215
|
-
- `content-is-on-top` is applied to the root `.expanding-panel` element, not `.expanding-panel-content` — style overrides must scope through it, e.g. `.expanding-panel.my-panel .expanding-panel-content
|
|
214
|
+
- The open/close animation targets `::details-content` (`height: 0 → auto`, `overflow: clip`) — no JS height measurement needed. Requires `interpolate-size: allow-keywords` support (Chromium only as of 2026); other browsers still toggle correctly, just instantly.
|
|
215
|
+
- `content-is-on-top` is applied to the root `.expanding-panel` element, not `.expanding-panel-content` — style overrides must scope through it, e.g. `.expanding-panel.my-panel .expanding-panel-content { ... }`.
|
|
216
216
|
- When `forceOpened` is `true`, `open` stays `true` regardless of `v-model`, but `v-model` still updates internally on clicks (useful if you later set `forceOpened` back to `false`).
|
|
217
217
|
- Group panels into a native accordion (only one open at a time) by passing the same `name` to multiple panels or use `AccordianCore` which handles this automatically.
|
|
218
218
|
- Auto-imported in Nuxt — no manual import needed.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# NavigationItems Component
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Internal panel rendered inside [`ResponsiveHeader`](responsive-header.md)'s overflow burger
|
|
6
|
+
dropdown — renders the **complement** of what's visible in the main bar. An item appears here
|
|
7
|
+
only when its `config.visible` (set by `ResponsiveHeader`'s measurement pass) is `false`. Not
|
|
8
|
+
meant to be mounted standalone in a real app, but is independently tested and storyable since
|
|
9
|
+
it's a distinct piece of rendering logic (dropdown-within-dropdown via `ExpandingPanel`,
|
|
10
|
+
active-route highlighting, hover indicator).
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Props reference
|
|
15
|
+
|
|
16
|
+
| Prop | Type | Default | Notes |
|
|
17
|
+
|------|------|---------|-------|
|
|
18
|
+
| `mainNavigationState` | `ResponsiveHeaderState` | `{ clonedNavLinks: {}, navListVisibility: {}, hasSecondNav: false }` | Shared geometry/visibility state, normally supplied by the parent `ResponsiveHeader` — not something a consumer constructs by hand outside of tests/stories. |
|
|
19
|
+
| `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root `.overflow-navigation-wrapper` element. |
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Behaviour notes
|
|
24
|
+
|
|
25
|
+
- A group's `<ul>` renders with the `visible` class (and thus becomes visible in the CSS)
|
|
26
|
+
only when that group's `navListVisibility[groupKey]` is `false` — i.e. only when at least
|
|
27
|
+
one item in it is collapsed.
|
|
28
|
+
- Plain links render as `NuxtLink`; items with `childLinks` render an `ExpandingPanel` nested
|
|
29
|
+
dropdown (its own expand/collapse, independent of the outer overflow `<details>`).
|
|
30
|
+
- Active-route highlighting reuses the same `isActiveNavItem` logic as `ResponsiveHeader`
|
|
31
|
+
(path match, or any `childLinks` path match).
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Usage example
|
|
36
|
+
|
|
37
|
+
`NavigationItems` is rendered automatically by `ResponsiveHeader` — you don't place it
|
|
38
|
+
yourself in a real app. For a test or story, construct the shared state directly:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
const mainNavigationState: ResponsiveHeaderState = {
|
|
42
|
+
hasSecondNav: false,
|
|
43
|
+
navListVisibility: { firstNav: false },
|
|
44
|
+
clonedNavLinks: {
|
|
45
|
+
firstNav: [
|
|
46
|
+
{ name: "Home", path: "/", config: { left: 0, right: 0, width: 0, visible: true } },
|
|
47
|
+
{ name: "About", path: "/about", config: { left: 0, right: 0, width: 0, visible: false } },
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
```
|