srcdev-nuxt-components 9.3.0 → 9.3.2

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 (53) hide show
  1. package/.claude/hooks/check-component-update.sh +66 -0
  2. package/.claude/settings.json +1 -1
  3. package/.claude/skills/component-local-style-override.md +45 -0
  4. package/.claude/skills/components/accordian-core.md +26 -5
  5. package/.claude/skills/components/expanding-panel-classic.md +221 -0
  6. package/.claude/skills/components/expanding-panel.md +8 -8
  7. package/.claude/skills/components/navigation-items.md +51 -0
  8. package/.claude/skills/components/responsive-header.md +161 -0
  9. package/.claude/skills/components/site-header.md +98 -0
  10. package/.claude/skills/index.md +6 -1
  11. package/.claude/skills/page-transitions.md +116 -0
  12. package/.claude/skills/theming-form-geometry-tokens.md +12 -0
  13. package/.claude/skills/theming-override-default.md +14 -0
  14. package/.claude/skills/theming-partial-override.md +17 -0
  15. package/.vscode/srcdev-component-accordian-core.code-snippets +74 -0
  16. package/.vscode/srcdev-component-expanding-panel-classic.code-snippets +121 -0
  17. package/.vscode/srcdev-component-expanding-panel.code-snippets +1 -3
  18. package/.vscode/srcdev-component-responsive-header.code-snippets +46 -0
  19. package/.vscode/srcdev-component-site-header.code-snippets +74 -0
  20. package/app/components/02.molecules/expandable/accordian/AccordianCore.vue +19 -11
  21. package/app/components/02.molecules/expandable/accordian/CONSUMER-STYLING.md +93 -0
  22. package/app/components/02.molecules/expandable/accordian/stories/AccordianCore.stories.ts +31 -0
  23. package/app/components/02.molecules/expandable/accordian/tests/AccordianCore.spec.ts +31 -0
  24. package/app/components/02.molecules/expandable/accordian/tests/__snapshots__/AccordianCore.spec.ts.snap +7 -21
  25. package/app/components/02.molecules/expandable/expanding-panel/CONSUMER-STYLING.md +10 -10
  26. package/app/components/02.molecules/expandable/expanding-panel/ExpandingPanel.vue +35 -37
  27. package/app/components/02.molecules/expandable/expanding-panel/stories/ExpandingPanel.stories.ts +5 -6
  28. package/app/components/02.molecules/expandable/expanding-panel/tests/__snapshots__/ExpandingPanel.spec.ts.snap +5 -13
  29. package/app/components/02.molecules/expandable/expanding-panel-classic/CONSUMER-STYLING.md +103 -0
  30. package/app/components/02.molecules/expandable/expanding-panel-classic/ExpandingPanelClassic.vue +191 -0
  31. package/app/components/02.molecules/expandable/expanding-panel-classic/stories/ExpandingPanelClassic.stories.ts +293 -0
  32. package/app/components/02.molecules/expandable/expanding-panel-classic/tests/ExpandingPanelClassic.spec.ts +514 -0
  33. package/app/components/02.molecules/expandable/expanding-panel-classic/tests/__snapshots__/ExpandingPanelClassic.spec.ts.snap +59 -0
  34. package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +66 -0
  35. package/app/components/{responsive-header → 03.organisms/responsive-header}/NavigationItems.vue +21 -28
  36. package/app/components/{responsive-header → 03.organisms/responsive-header}/ResponsiveHeader.vue +141 -43
  37. package/app/components/03.organisms/responsive-header/stories/NavigationItems.stories.ts +80 -0
  38. package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +122 -0
  39. package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +155 -0
  40. package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +319 -0
  41. package/app/components/03.organisms/responsive-header/tests/__snapshots__/NavigationItems.spec.ts.snap +34 -0
  42. package/app/components/03.organisms/responsive-header/tests/__snapshots__/ResponsiveHeader.spec.ts.snap +72 -0
  43. package/app/components/03.organisms/site-header/CONSUMER-STYLING.md +60 -0
  44. package/app/components/03.organisms/site-header/SiteHeader.vue +96 -0
  45. package/app/components/03.organisms/site-header/stories/SiteHeader.stories.ts +135 -0
  46. package/app/components/03.organisms/site-header/tests/SiteHeader.spec.ts +103 -0
  47. package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +15 -0
  48. package/app/components/05.forms/input-select/CONSUMER-STYLING.md +42 -0
  49. package/app/components/05.forms/input-select/InputSelectCore.vue +21 -9
  50. package/app/components/05.forms/input-text/CONSUMER-STYLING.md +60 -0
  51. package/app/components/05.forms/input-text/InputTextCore.vue +17 -8
  52. package/app/layouts/default.vue +64 -67
  53. package/package.json +4 -1
@@ -0,0 +1,121 @@
1
+ {
2
+ "SRCDEV ExpandingPanelClassic Basic": {
3
+ "description": "Basic uncontrolled ExpandingPanelClassic with summary and content slots",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<ExpandingPanelClassic name=\"$1delivery\">",
7
+ " <template #summary>",
8
+ " <span>$2Delivery &amp; Returns</span>",
9
+ " </template>",
10
+ " <template #content>",
11
+ " <p>$3Free standard delivery on orders over £50.</p>",
12
+ " </template>",
13
+ "</ExpandingPanelClassic>"
14
+ ]
15
+ },
16
+ "SRCDEV ExpandingPanelClassic Controlled": {
17
+ "description": "ExpandingPanelClassic controlled via v-model with external toggle",
18
+ "scope": "vue,html",
19
+ "body": [
20
+ "<ExpandingPanelClassic name=\"$1faq-1\" v-model=\"$2isOpen\">",
21
+ " <template #summary>",
22
+ " <span>$3What is your returns policy?</span>",
23
+ " </template>",
24
+ " <template #content>",
25
+ " <p>$4You can return any item within 30 days.</p>",
26
+ " </template>",
27
+ "</ExpandingPanelClassic>"
28
+ ]
29
+ },
30
+ "SRCDEV ExpandingPanelClassic Accordion Group": {
31
+ "description": "Multiple ExpandingPanelClassic panels grouped into a native accordion (shared name, only one open at a time)",
32
+ "scope": "vue,html",
33
+ "body": [
34
+ "<ExpandingPanelClassic name=\"$1faq\">",
35
+ " <template #summary>",
36
+ " <span>$2Question one?</span>",
37
+ " </template>",
38
+ " <template #content>",
39
+ " <p>$3Answer one.</p>",
40
+ " </template>",
41
+ "</ExpandingPanelClassic>",
42
+ "<ExpandingPanelClassic name=\"$1faq\">",
43
+ " <template #summary>",
44
+ " <span>$4Question two?</span>",
45
+ " </template>",
46
+ " <template #content>",
47
+ " <p>$5Answer two.</p>",
48
+ " </template>",
49
+ "</ExpandingPanelClassic>"
50
+ ]
51
+ },
52
+ "SRCDEV ExpandingPanelClassic Force Opened": {
53
+ "description": "ExpandingPanelClassic locked open, no toggle icon, cannot be collapsed",
54
+ "scope": "vue,html",
55
+ "body": [
56
+ "<ExpandingPanelClassic name=\"$1notice\" :force-opened=\"true\">",
57
+ " <template #summary>",
58
+ " <strong>$2Important notice</strong>",
59
+ " </template>",
60
+ " <template #content>",
61
+ " <p>$3This panel cannot be collapsed.</p>",
62
+ " </template>",
63
+ "</ExpandingPanelClassic>"
64
+ ]
65
+ },
66
+ "SRCDEV ExpandingPanelClassic Content Is On Top": {
67
+ "description": "ExpandingPanelClassic with content overlaying page content instead of pushing layout down",
68
+ "scope": "vue,html",
69
+ "body": [
70
+ "<ExpandingPanelClassic",
71
+ " name=\"$1overlay\"",
72
+ " :content-is-on-top=\"true\"",
73
+ " :style-class-passthrough=\"['$2my-overlay-panel']\"",
74
+ ">",
75
+ " <template #summary>",
76
+ " <span>$3Open me — content overlays what's below</span>",
77
+ " </template>",
78
+ " <template #content>",
79
+ " <div class=\"$2my-overlay-panel-body\">",
80
+ " <p>$4Positioned absolutely below the summary, doesn't push page content down.</p>",
81
+ " </div>",
82
+ " </template>",
83
+ "</ExpandingPanelClassic>"
84
+ ]
85
+ },
86
+ "SRCDEV ExpandingPanelClassic Custom Icon": {
87
+ "description": "ExpandingPanelClassic with a custom toggle icon via #icon slot",
88
+ "scope": "vue,html",
89
+ "body": [
90
+ "<ExpandingPanelClassic name=\"$1custom-icon\">",
91
+ " <template #summary>",
92
+ " <span>$2Section title</span>",
93
+ " </template>",
94
+ " <template #icon>",
95
+ " <span style=\"font-size:1.2rem\">$3+</span>",
96
+ " </template>",
97
+ " <template #content>",
98
+ " <p>$4Content here.</p>",
99
+ " </template>",
100
+ "</ExpandingPanelClassic>"
101
+ ]
102
+ },
103
+ "SRCDEV ExpandingPanelClassic CSS Override": {
104
+ "description": "CSS override scaffold for ExpandingPanelClassic tokens",
105
+ "scope": "css",
106
+ "body": [
107
+ ".$1my-panel {",
108
+ " .expanding-panel-classic {",
109
+ " --expanding-panel-classic-summary-gap: $2;",
110
+ " --expanding-panel-classic-summary-padding-block: $3;",
111
+ " --expanding-panel-classic-icon-size: $4;",
112
+ "",
113
+ " &.content-is-on-top {",
114
+ " --expanding-panel-classic-content-gap: $5;",
115
+ " --expanding-panel-classic-content-z-index: $6;",
116
+ " }",
117
+ " }",
118
+ "}"
119
+ ]
120
+ }
121
+ }
@@ -64,7 +64,7 @@
64
64
  ]
65
65
  },
66
66
  "SRCDEV ExpandingPanel Content Is On Top": {
67
- "description": "ExpandingPanel with content overlaying page content instead of pushing layout down — styling must go on a wrapper INSIDE #content, never on .inner",
67
+ "description": "ExpandingPanel with content overlaying page content instead of pushing layout down",
68
68
  "scope": "vue,html",
69
69
  "body": [
70
70
  "<ExpandingPanel",
@@ -76,8 +76,6 @@
76
76
  " <span>$3Open me — content overlays what's below</span>",
77
77
  " </template>",
78
78
  " <template #content>",
79
- " <!-- Wrapper INSIDE the slot carries background/padding/shadow — never .inner itself,",
80
- " which must stay visually unstyled or it renders while the panel is closed. -->",
81
79
  " <div class=\"$2my-overlay-panel-body\">",
82
80
  " <p>$4Positioned absolutely below the summary, doesn't push page content down.</p>",
83
81
  " </div>",
@@ -0,0 +1,46 @@
1
+ {
2
+ "SRCDEV ResponsiveHeader Basic": {
3
+ "description": "ResponsiveHeader with dropdown groups and a secondaryNavigation slot",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<ResponsiveHeader",
7
+ " :responsive-nav-links=\"$1responsiveNavLinks\"",
8
+ " :style-class-passthrough=\"['site-header-nav']\"",
9
+ ">",
10
+ " <template #secondaryNavigation>",
11
+ " <NuxtLink to=\"$2/settings\" aria-label=\"$3Settings\">",
12
+ " <Icon name=\"$4material-symbols:settings-outline-rounded\" />",
13
+ " </NuxtLink>",
14
+ " </template>",
15
+ "</ResponsiveHeader>"
16
+ ]
17
+ },
18
+ "SRCDEV ResponsiveHeader Nav Data (with dropdown)": {
19
+ "description": "responsiveNavLinks data with a dropdown group",
20
+ "scope": "typescript,vue",
21
+ "body": [
22
+ "const $1responsiveNavLinks = {",
23
+ " firstNav: [",
24
+ " { name: \"$2Home\", path: \"$3/\" },",
25
+ " {",
26
+ " name: \"$4Components\",",
27
+ " childLinksTitle: \"$5UI Components\",",
28
+ " childLinks: [{ name: \"$6Item\", path: \"$7/path\" }],",
29
+ " },",
30
+ " ],",
31
+ "};"
32
+ ]
33
+ },
34
+ "SRCDEV ResponsiveHeader CSS Override": {
35
+ "description": "CSS override scaffold for ResponsiveHeader token styling — sets a fixed (non-fluid) link font-size",
36
+ "scope": "css",
37
+ "body": [
38
+ ".$1site-header-nav {",
39
+ " --responsive-header-link-color: $2inherit;",
40
+ " --responsive-header-bg: $3transparent;",
41
+ " /* Fixed, NOT var(--step-*) — see responsive-header.md's measurement-pipeline note. */",
42
+ " --responsive-header-link-font-size: ${4:1.4rem};",
43
+ "}"
44
+ ]
45
+ }
46
+ }
@@ -0,0 +1,74 @@
1
+ {
2
+ "SRCDEV SiteHeader Basic": {
3
+ "description": "SiteHeader wrapping PageRow + SkipLinks + ResponsiveHeader, with a branding slot",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<SiteHeader",
7
+ " :responsive-nav-links=\"$1responsiveNavLinks\"",
8
+ " :style-class-passthrough=\"['header']\"",
9
+ " :nav-style-class-passthrough=\"['site-header-nav']\"",
10
+ ">",
11
+ " <template #branding>",
12
+ " <NuxtLink to=\"/\" class=\"home-link\">$2Brand</NuxtLink>",
13
+ " </template>",
14
+ "</SiteHeader>"
15
+ ]
16
+ },
17
+ "SRCDEV SiteHeader With Secondary Navigation": {
18
+ "description": "SiteHeader with the secondaryNavigation slot (e.g. a settings icon link)",
19
+ "scope": "vue,html",
20
+ "body": [
21
+ "<SiteHeader :responsive-nav-links=\"$1responsiveNavLinks\">",
22
+ " <template #branding>",
23
+ " <NuxtLink to=\"/\" class=\"home-link\">$2Brand</NuxtLink>",
24
+ " </template>",
25
+ " <template #secondaryNavigation>",
26
+ " <NuxtLink to=\"$3/settings\" aria-label=\"$4Settings\">",
27
+ " <Icon name=\"$5material-symbols:settings-outline-rounded\" />",
28
+ " </NuxtLink>",
29
+ " </template>",
30
+ "</SiteHeader>"
31
+ ]
32
+ },
33
+ "SRCDEV SiteHeader Nav Data (flat)": {
34
+ "description": "Simple flat responsiveNavLinks data with no dropdowns, for a few-page site",
35
+ "scope": "typescript,vue",
36
+ "body": [
37
+ "const $1responsiveNavLinks = {",
38
+ " main: [",
39
+ " { name: \"$2Home\", path: \"$3/\" },",
40
+ " { name: \"$4About\", path: \"$5/about\" },",
41
+ " { name: \"$6Contact\", path: \"$7/contact\" },",
42
+ " ],",
43
+ "};"
44
+ ]
45
+ },
46
+ "SRCDEV SiteHeader Nav Data (with dropdown)": {
47
+ "description": "responsiveNavLinks data with a dropdown group, for the full overflow-nav experience",
48
+ "scope": "typescript,vue",
49
+ "body": [
50
+ "const $1responsiveNavLinks = {",
51
+ " firstNav: [",
52
+ " {",
53
+ " name: \"$2Components\",",
54
+ " childLinksTitle: \"$3UI Components\",",
55
+ " childLinks: [{ name: \"$4Item\", path: \"$5/path\" }],",
56
+ " },",
57
+ " { name: \"$6Typography\", path: \"$7/typography\" },",
58
+ " ],",
59
+ "};"
60
+ ]
61
+ },
62
+ "SRCDEV SiteHeader CSS Override": {
63
+ "description": "CSS override scaffold for SiteHeader token styling",
64
+ "scope": "css",
65
+ "body": [
66
+ ".$1header {",
67
+ " --site-header-gap: $2 2.4rem;",
68
+ " --site-header-padding-inline: $3 2.4rem;",
69
+ " --site-header-bg: $4transparent;",
70
+ " /* --site-header-position: sticky; */",
71
+ "}"
72
+ ]
73
+ }
74
+ }
@@ -1,6 +1,7 @@
1
1
  <template>
2
2
  <div class="display-accordian" :class="[elementClasses]">
3
- <ExpandingPanel
3
+ <component
4
+ :is="panelComponent"
4
5
  v-for="(item, key) in itemCount"
5
6
  :key="key"
6
7
  :name
@@ -17,15 +18,19 @@
17
18
  <template #content>
18
19
  <slot :name="`accordian-${key}-content`"></slot>
19
20
  </template>
20
- </ExpandingPanel>
21
+ </component>
21
22
  </div>
22
23
  </template>
23
24
 
24
25
  <script setup lang="ts">
26
+ import ExpandingPanel from "../expanding-panel/ExpandingPanel.vue";
27
+ import ExpandingPanelClassic from "../expanding-panel-classic/ExpandingPanelClassic.vue";
28
+
25
29
  interface Props {
26
30
  name?: string;
27
31
  itemCount?: number;
28
32
  animationDuration?: number;
33
+ variant?: "modern" | "classic";
29
34
  styleClassPassthrough?: string | string[];
30
35
  }
31
36
 
@@ -33,26 +38,29 @@ const props = withDefaults(defineProps<Props>(), {
33
38
  name: undefined,
34
39
  itemCount: 0,
35
40
  animationDuration: 300,
41
+ variant: "modern",
36
42
  styleClassPassthrough: () => [],
37
43
  });
38
44
 
39
45
  const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
40
46
  const animationDurationStr = computed(() => `${props.animationDuration}ms`);
47
+ const panelComponent = computed(() => (props.variant === "classic" ? ExpandingPanelClassic : ExpandingPanel));
41
48
  </script>
42
49
 
43
50
  <style lang="css">
44
51
  @layer components {
45
- .display-accordian {
46
- max-width: 600px;
47
- margin: 0 auto;
52
+ .display-accordian {
53
+ max-width: 600px;
54
+ margin: 0 auto;
48
55
 
49
- .accordian-item {
50
- &.expanding-panel {
51
- transition:
52
- margin-block-end v-bind(animationDurationStr) ease-in-out,
53
- border-radius v-bind(animationDurationStr) ease-in-out;
56
+ .accordian-item {
57
+ &.expanding-panel,
58
+ &.expanding-panel-classic {
59
+ transition:
60
+ margin-block-end v-bind(animationDurationStr) ease-in-out,
61
+ border-radius v-bind(animationDurationStr) ease-in-out;
62
+ }
54
63
  }
55
64
  }
56
65
  }
57
- }
58
66
  </style>
@@ -0,0 +1,93 @@
1
+ # AccordianCore — Consumer Styling Guide
2
+
3
+ ## No tokens of its own
4
+
5
+ `AccordianCore` doesn't define any `--accordian-*` custom properties. It's a thin wrapper that
6
+ renders `itemCount` copies of a panel component (`ExpandingPanel` by default, or
7
+ `ExpandingPanelClassic` when `variant="classic"`) and forwards `name`/`animationDuration` to each
8
+ one — all real styling surface lives on whichever panel component is active. See that
9
+ component's own `CONSUMER-STYLING.md`:
10
+
11
+ - [../expanding-panel/CONSUMER-STYLING.md](../expanding-panel/CONSUMER-STYLING.md) (default, `variant="modern"`)
12
+ - [../expanding-panel-classic/CONSUMER-STYLING.md](../expanding-panel-classic/CONSUMER-STYLING.md) (`variant="classic"`)
13
+
14
+ `--expanding-panel-*` tokens set at any scope (global, page) apply to `AccordianCore`'s panels
15
+ too, since they're the same underlying component — no `AccordianCore`-specific override is
16
+ needed for summary gap, icon size, etc.
17
+
18
+ ---
19
+
20
+ ## The `.accordian-item` hook
21
+
22
+ Every panel gets `style-class-passthrough="['accordian-item']"` automatically — this is the one
23
+ `AccordianCore`-specific override surface, used to target *all* panels in the group without
24
+ reaching into `ExpandingPanel`/`ExpandingPanelClassic` internals.
25
+
26
+ ```css
27
+ /* In the consuming page's unscoped <style> block */
28
+ .display-accordian {
29
+ .accordian-item.expanding-panel {
30
+ /* modern variant (default) */
31
+ border-block-end: 1px solid var(--theme-border, #e0e0e0);
32
+ }
33
+
34
+ /* If this instance uses variant="classic", target the classic root class instead: */
35
+ .accordian-item.expanding-panel-classic {
36
+ border-block-end: 1px solid var(--theme-border, #e0e0e0);
37
+ }
38
+ }
39
+ ```
40
+
41
+ Note the root class differs by variant (`.expanding-panel` vs `.expanding-panel-classic`) — a
42
+ selector written for one won't match the other. If you switch `variant` on an existing instance,
43
+ check any `.accordian-item.expanding-panel*` overrides still target the right class.
44
+
45
+ ---
46
+
47
+ ## Page-scoped overrides
48
+
49
+ No `:deep()` is required (component styles are unscoped).
50
+
51
+ ```css
52
+ .faq-section {
53
+ .display-accordian {
54
+ max-width: 480px;
55
+
56
+ .accordian-item.expanding-panel {
57
+ --expanding-panel-summary-padding-block: 1.6rem;
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ ---
64
+
65
+ ## Per-instance overrides via styleClassPassthrough
66
+
67
+ Use sparingly — prefer global or page-scoped CSS. `styleClassPassthrough` on `AccordianCore`
68
+ targets the root `.display-accordian` element, not the individual panels (those already get
69
+ `.accordian-item` automatically):
70
+
71
+ ```vue
72
+ <AccordianCore name="faq" :item-count="3" :style-class-passthrough="['compact-faq']">
73
+ ...
74
+ </AccordianCore>
75
+ ```
76
+
77
+ ```css
78
+ .display-accordian.compact-faq {
79
+ max-width: 400px;
80
+ }
81
+ ```
82
+
83
+ ---
84
+
85
+ ## Notes
86
+
87
+ - `variant` is group-wide: every panel in one `AccordianCore` renders the same component, so
88
+ there's no per-panel styling divergence caused by variant — just remember which root class
89
+ (`.expanding-panel` vs `.expanding-panel-classic`) your `.accordian-item` selectors need.
90
+ - `contentIsOnTop` isn't exposed on `AccordianCore` — it isn't meaningful for a grouped accordion
91
+ (see `ExpandingPanel`'s own docs for why: it's for a single panel overlaying trailing page
92
+ content, not stacked/grouped panels). Use `ExpandingPanel`/`ExpandingPanelClassic` directly if
93
+ you need an overlay panel.
@@ -18,6 +18,14 @@ const meta: Meta<typeof AccordianCore> = {
18
18
  control: { type: "number", min: 0, step: 50 },
19
19
  description: "Expand/collapse animation duration in milliseconds",
20
20
  },
21
+ variant: {
22
+ control: { type: "radio" },
23
+ options: ["modern", "classic"],
24
+ description:
25
+ "Which panel implementation to render: 'modern' (ExpandingPanel, ::details-content based) or " +
26
+ "'classic' (ExpandingPanelClassic, grid-template-rows based) — use classic when the open/close " +
27
+ "animation must run identically in every browser today",
28
+ },
21
29
  styleClassPassthrough: {
22
30
  control: "object",
23
31
  description: "Additional CSS classes applied to the root element",
@@ -27,6 +35,7 @@ const meta: Meta<typeof AccordianCore> = {
27
35
  name: undefined,
28
36
  itemCount: 3,
29
37
  animationDuration: 300,
38
+ variant: "modern",
30
39
  styleClassPassthrough: [],
31
40
  },
32
41
  };
@@ -71,6 +80,28 @@ export const Default: Story = {
71
80
  }),
72
81
  };
73
82
 
83
+ export const ClassicVariant: Story = {
84
+ name: "Classic Variant",
85
+ args: {
86
+ itemCount: 3,
87
+ animationDuration: 300,
88
+ variant: "classic",
89
+ },
90
+ render: (args) => ({
91
+ components: { AccordianCore },
92
+ setup() {
93
+ return { args, slots: buildGenericSlots(args.itemCount ?? 3) };
94
+ },
95
+ template: `
96
+ <AccordianCore v-bind="args">
97
+ <template v-for="(content, name) in slots" #[name] :key="name">
98
+ <span v-html="content" />
99
+ </template>
100
+ </AccordianCore>
101
+ `,
102
+ }),
103
+ };
104
+
74
105
  export const SinglePanel: Story = {
75
106
  args: {
76
107
  itemCount: 1,
@@ -8,6 +8,7 @@ interface AccordianCoreInstance extends ComponentPublicInstance {
8
8
  name: string | undefined;
9
9
  itemCount: number;
10
10
  animationDuration: number;
11
+ variant: "modern" | "classic";
11
12
  styleClassPassthrough: string | string[];
12
13
  }
13
14
 
@@ -92,6 +93,35 @@ describe("AccordianCore", () => {
92
93
  expect(wrapper.findAllComponents({ name: "ExpandingPanel" })).toHaveLength(0);
93
94
  });
94
95
 
96
+ // ─── variant ──────────────────────────────────────────────────────────────
97
+
98
+ it("renders ExpandingPanel by default (variant: modern)", async () => {
99
+ const wrapper = await mountSuspended(AccordianCore, {
100
+ props: { itemCount: 2 },
101
+ });
102
+ expect(wrapper.findAllComponents({ name: "ExpandingPanel" })).toHaveLength(2);
103
+ expect(wrapper.findAllComponents({ name: "ExpandingPanelClassic" })).toHaveLength(0);
104
+ });
105
+
106
+ it("renders ExpandingPanelClassic when variant is classic", async () => {
107
+ const wrapper = await mountSuspended(AccordianCore, {
108
+ props: { itemCount: 2, variant: "classic" },
109
+ });
110
+ expect(wrapper.findAllComponents({ name: "ExpandingPanelClassic" })).toHaveLength(2);
111
+ expect(wrapper.findAllComponents({ name: "ExpandingPanel" })).toHaveLength(0);
112
+ });
113
+
114
+ it("forwards props identically to ExpandingPanelClassic when variant is classic", async () => {
115
+ const wrapper = await mountSuspended(AccordianCore, {
116
+ props: { itemCount: 2, name: "classic-accordian", animationDuration: 500, variant: "classic" },
117
+ });
118
+ wrapper.findAllComponents({ name: "ExpandingPanelClassic" }).forEach((panel) => {
119
+ expect(panel.props("name")).toBe("classic-accordian");
120
+ expect(panel.props("animationDuration")).toBe(500);
121
+ expect(panel.props("styleClassPassthrough")).toEqual(["accordian-item"]);
122
+ });
123
+ });
124
+
95
125
  // ─── Props forwarded to ExpandingPanel ───────────────────────────────────
96
126
 
97
127
  it("forwards animationDuration to every ExpandingPanel", async () => {
@@ -137,6 +167,7 @@ describe("AccordianCore", () => {
137
167
  expect(vm.name).toBeUndefined();
138
168
  expect(vm.itemCount).toBe(0);
139
169
  expect(vm.animationDuration).toBe(300);
170
+ expect(vm.variant).toBe("modern");
140
171
  expect(vm.styleClassPassthrough).toEqual([]);
141
172
  });
142
173
 
@@ -8,10 +8,8 @@ exports[`AccordianCore > renders correct HTML structure with all props set 1`] =
8
8
  <div class="label-wrapper"></div>
9
9
  <div class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon mi-12" aria-hidden="true"></span></div>
10
10
  </summary>
11
+ <div id="id-snapshot-accordian-content" class="expanding-panel-content" aria-labelledby="id-snapshot-accordian-trigger" role="region"></div>
11
12
  </details>
12
- <div id="id-snapshot-accordian-content" class="expanding-panel-content" aria-labelledby="id-snapshot-accordian-trigger" role="region">
13
- <div class="inner"></div>
14
- </div>
15
13
  </div>
16
14
  <div class="expanding-panel accordian-item" icon-size="medium">
17
15
  <details class="expanding-panel-details" name="snapshot-accordian">
@@ -19,10 +17,8 @@ exports[`AccordianCore > renders correct HTML structure with all props set 1`] =
19
17
  <div class="label-wrapper"></div>
20
18
  <div class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon mi-12" aria-hidden="true"></span></div>
21
19
  </summary>
20
+ <div id="id-snapshot-accordian-content" class="expanding-panel-content" aria-labelledby="id-snapshot-accordian-trigger" role="region"></div>
22
21
  </details>
23
- <div id="id-snapshot-accordian-content" class="expanding-panel-content" aria-labelledby="id-snapshot-accordian-trigger" role="region">
24
- <div class="inner"></div>
25
- </div>
26
22
  </div>
27
23
  <div class="expanding-panel accordian-item" icon-size="medium">
28
24
  <details class="expanding-panel-details" name="snapshot-accordian">
@@ -30,10 +26,8 @@ exports[`AccordianCore > renders correct HTML structure with all props set 1`] =
30
26
  <div class="label-wrapper"></div>
31
27
  <div class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon mi-12" aria-hidden="true"></span></div>
32
28
  </summary>
29
+ <div id="id-snapshot-accordian-content" class="expanding-panel-content" aria-labelledby="id-snapshot-accordian-trigger" role="region"></div>
33
30
  </details>
34
- <div id="id-snapshot-accordian-content" class="expanding-panel-content" aria-labelledby="id-snapshot-accordian-trigger" role="region">
35
- <div class="inner"></div>
36
- </div>
37
31
  </div>
38
32
  </div>"
39
33
  `;
@@ -46,10 +40,8 @@ exports[`AccordianCore > renders correct HTML structure with default props 1`] =
46
40
  <div class="label-wrapper"></div>
47
41
  <div class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon mi-12" aria-hidden="true"></span></div>
48
42
  </summary>
43
+ <div id="id-v-0-0-0-content" class="expanding-panel-content" aria-labelledby="id-v-0-0-0-trigger" role="region"></div>
49
44
  </details>
50
- <div id="id-v-0-0-0-content" class="expanding-panel-content" aria-labelledby="id-v-0-0-0-trigger" role="region">
51
- <div class="inner"></div>
52
- </div>
53
45
  </div>
54
46
  <div class="expanding-panel accordian-item" icon-size="medium">
55
47
  <details class="expanding-panel-details" name="v-0-0-1">
@@ -57,10 +49,8 @@ exports[`AccordianCore > renders correct HTML structure with default props 1`] =
57
49
  <div class="label-wrapper"></div>
58
50
  <div class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon mi-12" aria-hidden="true"></span></div>
59
51
  </summary>
52
+ <div id="id-v-0-0-1-content" class="expanding-panel-content" aria-labelledby="id-v-0-0-1-trigger" role="region"></div>
60
53
  </details>
61
- <div id="id-v-0-0-1-content" class="expanding-panel-content" aria-labelledby="id-v-0-0-1-trigger" role="region">
62
- <div class="inner"></div>
63
- </div>
64
54
  </div>
65
55
  </div>"
66
56
  `;
@@ -73,10 +63,8 @@ exports[`AccordianCore > renders correct HTML structure with populated slots 1`]
73
63
  <div class="label-wrapper"><span>Summary 0</span></div>
74
64
  <div class="icon-wrapper"><span>Icon 0</span></div>
75
65
  </summary>
66
+ <div id="id-v-0-0-0-content" class="expanding-panel-content" aria-labelledby="id-v-0-0-0-trigger" role="region"><span>Content 0</span></div>
76
67
  </details>
77
- <div id="id-v-0-0-0-content" class="expanding-panel-content" aria-labelledby="id-v-0-0-0-trigger" role="region">
78
- <div class="inner"><span>Content 0</span></div>
79
- </div>
80
68
  </div>
81
69
  <div class="expanding-panel accordian-item" icon-size="medium">
82
70
  <details class="expanding-panel-details" name="v-0-0-1">
@@ -84,10 +72,8 @@ exports[`AccordianCore > renders correct HTML structure with populated slots 1`]
84
72
  <div class="label-wrapper"><span>Summary 1</span></div>
85
73
  <div class="icon-wrapper"><span>Icon 1</span></div>
86
74
  </summary>
75
+ <div id="id-v-0-0-1-content" class="expanding-panel-content" aria-labelledby="id-v-0-0-1-trigger" role="region"><span>Content 1</span></div>
87
76
  </details>
88
- <div id="id-v-0-0-1-content" class="expanding-panel-content" aria-labelledby="id-v-0-0-1-trigger" role="region">
89
- <div class="inner"><span>Content 1</span></div>
90
- </div>
91
77
  </div>
92
78
  </div>"
93
79
  `;
@@ -13,9 +13,10 @@ All `--expanding-panel-*` tokens are the stable override surface. Set them at an
13
13
  | `--expanding-panel-content-z-index` | `10` | Stacking order of the content region when `contentIsOnTop` is `true` |
14
14
  | `--expanding-panel-content-gap` | `0px` | Space between the summary and the content region when `contentIsOnTop` is `true` |
15
15
 
16
- Note: `background-color`, `padding`, and shadow are **not** tokenised, and must never be applied
17
- to `.inner` itself — see [expanding-panel.md](../../../../.claude/skills/components/expanding-panel.md#styling-the-content-when-contentisontop)
18
- for why, and style a wrapper *inside* the `#content` slot instead.
16
+ Note: `background-color`, `padding`, and shadow are **not** tokenised. They can be applied directly
17
+ to `.expanding-panel-content`, or to a wrapper inside the `#content` slot — see
18
+ [expanding-panel.md](../../../../.claude/skills/components/expanding-panel.md#styling-the-content-when-contentisontop)
19
+ for why both are safe.
19
20
 
20
21
  ---
21
22
 
@@ -64,7 +65,6 @@ visual style, pass a modifier class:
64
65
  >
65
66
  <template #summary>...</template>
66
67
  <template #content>
67
- <!-- Wrapper INSIDE the slot carries the visual styling — never .inner itself -->
68
68
  <div class="promo-panel-body">...</div>
69
69
  </template>
70
70
  </ExpandingPanel>
@@ -89,12 +89,12 @@ visual style, pass a modifier class:
89
89
 
90
90
  - `--expanding-panel-content-gap` and `--expanding-panel-content-z-index` only take effect when
91
91
  `contentIsOnTop` is `true` — they're no-ops for the default (in-flow) layout.
92
- - Never set `background-color`, `padding`, `border`, or a shadow directly on `.inner`. `.inner`
93
- has no explicit height when collapsed (`grid-template-rows: 0fr`) and relies on
94
- `overflow: hidden` to clip its *children* to 0px but padding/border/background on `.inner`'s
95
- own box are not "overflow content", so they'd still render as a visible gap under the summary
96
- while closed. Always style a wrapper element placed *inside* the `#content` slot instead; that
97
- wrapper is a child of `.inner` and gets clipped correctly.
92
+ - `background-color`, `padding`, `border`, and shadow are all safe to set directly on
93
+ `.expanding-panel-content`. The open/close animation clips via `overflow: clip` on
94
+ `::details-content` (the native anonymous box wrapping everything after `<summary>`), and
95
+ `.expanding-panel-content` is a child of that box — so its own box is correctly clipped to the
96
+ animated height while closed/closing. A wrapper inside the `#content` slot works equally well
97
+ if you prefer to keep the styling scoped to your own markup.
98
98
  - Don't stack multiple `contentIsOnTop` panels as direct siblings (linked via a shared `name` or
99
99
  not). The overlay is absolutely positioned so it doesn't push the next element down — which
100
100
  means a sibling `ExpandingPanel` placed right after it sits exactly where the overlay renders,