srcdev-nuxt-components 9.3.6 → 9.3.7

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 (20) hide show
  1. package/.claude/skills/components/accordian-core.md +7 -7
  2. package/.claude/skills/components/content-docs.md +1 -0
  3. package/.claude/skills/components/expanding-panel.md +5 -5
  4. package/.claude/skills/components/navigation-items.md +1 -0
  5. package/.claude/skills/components/responsive-header.md +1 -0
  6. package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +22 -16
  7. package/app/components/02.molecules/expandable/accordian/AccordianCore.vue +5 -1
  8. package/app/components/02.molecules/expandable/accordian/tests/AccordianCore.spec.ts +22 -14
  9. package/app/components/02.molecules/expandable/accordian/tests/__snapshots__/AccordianCore.spec.ts.snap +42 -28
  10. package/app/components/02.molecules/expandable/expanding-panel/CONSUMER-STYLING.md +8 -10
  11. package/app/components/02.molecules/expandable/expanding-panel/ExpandingPanel.vue +15 -47
  12. package/app/components/02.molecules/expandable/expanding-panel/tests/__snapshots__/ExpandingPanel.spec.ts.snap +4 -12
  13. package/app/components/03.organisms/responsive-header/NavigationItems.vue +91 -4
  14. package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +5 -1
  15. package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +2 -2
  16. package/app/components/03.organisms/responsive-header/tests/__snapshots__/NavigationItems.spec.ts.snap +13 -13
  17. package/app/components/03.organisms/responsive-header/tests/__snapshots__/ResponsiveHeader.spec.ts.snap +13 -13
  18. package/app/components/03.organisms/services/services-section/ServicesSection.vue +19 -0
  19. package/app/components/03.organisms/services/services-section/tests/__snapshots__/ServicesSection.spec.ts.snap +8 -8
  20. package/package.json +1 -1
@@ -35,7 +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
+ | `variant` | `"modern" \| "classic"` | `"classic"` | `"classic"` renders each panel as `ExpandingPanelClassic` (`grid-template-rows`-based, animates identically in every browser). `"modern"` renders `ExpandingPanel` instead (`::details-content`-based) **known not to work correctly on WebKit (Safari/iOS)**, confirmed via real-device testing (2026-08); the animation silently fails and, worse, `contentIsOnTop` positioning never applies there. `"modern"` is opt-in for consumers who've confirmed it works for their audience, not the safe default. Applies to every panel in the group — mixing variants within one `AccordianCore` isn't supported. See [expanding-panel-classic.md](expanding-panel-classic.md) and CLAUDE.md pitfall #19. |
39
39
  | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root `.display-accordian` element. |
40
40
 
41
41
  ---
@@ -91,10 +91,10 @@ Passing `name="faq"` groups all panels so only one can be open at a time.
91
91
  </AccordianCore>
92
92
  ```
93
93
 
94
- ### Classic variant (animation must run in every browser)
94
+ ### Modern variant (opt-in, not WebKit-safe)
95
95
 
96
96
  ```vue
97
- <AccordianCore name="faq" :item-count="2" variant="classic">
97
+ <AccordianCore name="faq" :item-count="2" variant="modern">
98
98
  <template #accordian-0-summary><span>Question one?</span></template>
99
99
  <template #accordian-0-content><p>Answer one.</p></template>
100
100
 
@@ -154,10 +154,10 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
154
154
  /* Geometry */
155
155
  /* max-width: none; */ /* default is 600px — remove the width cap */
156
156
 
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" */
160
- .accordian-item.expanding-panel {
157
+ /* Panel-level overrides via the .accordian-item hook — .expanding-panel-classic is
158
+ the root class rendered by the default "classic" variant; swap in .expanding-panel
159
+ if this instance uses variant="modern" */
160
+ .accordian-item.expanding-panel-classic {
161
161
  /* Border */
162
162
  /* border-block-end: 1px solid currentColor; */
163
163
 
@@ -17,6 +17,7 @@
17
17
  | `docsPageNavItems` | `DocsNavItem[]` | `[]` | Items rendered in the `docsPageNav` panel. Panel omitted entirely when empty. |
18
18
  | `docsNavLabel` | `string` | `"Navigation"` | Heading text for the `docsNav` panel's `#summary`. |
19
19
  | `docsPageNavLabel` | `string` | `"On this page"` | Heading text for the `docsPageNav` panel's `#summary`. |
20
+ | `panelVariant` | `"modern" \| "classic"` | `"classic"` | Which panel component renders `docsNav`/`docsPageNav`: `ExpandingPanel` (`"modern"`) or `ExpandingPanelClassic` (`"classic"`, default). `"modern"` is known not to work correctly on WebKit — the mobile nav (which relies on `contentIsOnTop`) becomes unreachable there. See CLAUDE.md pitfall #19. |
20
21
  | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root `.content-docs` element. |
21
22
 
22
23
  `DocsNavItem` (from `~/types/components`):
@@ -4,9 +4,9 @@
4
4
 
5
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>`) where supported, 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
6
 
7
- **WebKit (Safari/iOS) does not support `::details-content`** as of 2026-08, confirmed via real-device testing — Chrome's device-emulation mode cannot catch this, since it's still the Blink engine underneath regardless of which browser UI it's simulating. WebKit silently drops the whole rule block: no console warning, no error, just no animation and (more seriously) `content-is-on-top`'s `position: absolute` never applying, so an overlay panel renders in normal document flow and can break the surrounding layout. Fixed 2026-08-22: `ExpandingPanel` now carries an `@supports not selector(::details-content)` fallback block that reproduces `ExpandingPanelClassic`'s `grid-template-rows` technique automatically no consumer action needed, this works transparently on every browser. See pitfall #19 in the repo's `CLAUDE.md` for the full mechanism.
7
+ **WebKit (Safari/iOS) does not support `::details-content`** as of 2026-08, confirmed via real-device testing — Chrome's device-emulation mode cannot catch this, since it's still the Blink engine underneath regardless of which browser UI it's simulating. WebKit silently drops the whole rule block: no animation and (more seriously) `content-is-on-top`'s `position: absolute` never applying, so an overlay panel renders in normal document flow and can break the surrounding layout. An `@supports not selector(::details-content)` CSS-only fallback was tried (2026-08-22) but didn't reliably kick in on real devices either and was reverted the same day **do not reintroduce that approach**.
8
8
 
9
- `ExpandingPanelClassic` still exists as a standalone optionsame props/model/slots API, always uses the `grid-template-rows` implementation with no pseudo-element dependency at all but there's no longer a compatibility reason to prefer it over `ExpandingPanel`.
9
+ **The actual fix is component-level, not CSS-level**: every consumer that gave `ExpandingPanel` a choice of implementation now defaults to `ExpandingPanelClassic` instead see `AccordianCore`'s `variant` prop (default `"classic"`), `ContentDocs`'s `panelVariant` prop (default `"classic"`), and `ResponsiveHeader`/`NavigationItems`'s `panelVariant` prop (default `"classic"`). `ExpandingPanel` itself is unchanged (still `::details-content`-only, no fallback) and logs a `console.warn` in dev mode on every mount warning about this. Reach for `ExpandingPanel` directly, or pass `variant`/`panelVariant: "modern"` to one of the above, only once you've confirmed it's acceptable for your actual audience — it is **not** the safe default anywhere in this library anymore. See pitfall #19 in the repo's `CLAUDE.md` for the full history.
10
10
 
11
11
  ---
12
12
 
@@ -145,9 +145,9 @@ Always supply a meaningful `name` prop when using multiple panels on the same pa
145
145
 
146
146
  ## Styling the content when contentIsOnTop
147
147
 
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) in browsers that support `::details-content`: the clipping boundary there is `::details-content` itself (`overflow: clip`, animated `height`), and `.expanding-panel-content` is a *child* of that box, so its own box is correctly clipped regardless of where the styling lives.
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 `::details-content` itself (`overflow: clip`, animated `height`), and `.expanding-panel-content` is a *child* of that box, so its own box is correctly clipped regardless of where the styling lives.
149
149
 
150
- In the WebKit fallback path (no `::details-content` support), `.expanding-panel-content` itself is the animated grid track (`grid-template-rows: 0fr → 1fr`) and remains the element being sized — so styling placed directly on it is still clipped correctly there too. The one structural difference: the fallback wraps the `#content` slot in an internal `.inner` div (needed so a plain child's `min-height: auto` doesn't stop the row collapsing to true `0`) — this is purely an implementation detail for the collapse mechanism and isn't a styling hook; don't target `.inner` directly, style `.expanding-panel-content` or the slot's own markup instead, same as the `::details-content` path.
150
+ If this needs to work correctly on WebKit too, use `ExpandingPanelClassic` instead (see the Overview section above) — its equivalent guidance lives in its own `CONSUMER-STYLING.md`, and its `.expanding-panel-classic-content > .inner` structure differs slightly.
151
151
 
152
152
  ---
153
153
 
@@ -213,7 +213,7 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
213
213
 
214
214
  ## Notes
215
215
 
216
- - The open/close animation targets `::details-content` (`height: 0 → auto`, `overflow: clip`) where supported — no JS height measurement needed. Requires `interpolate-size: allow-keywords` for the height transition itself (Chromium only as of 2026; other browsers with `::details-content` support still toggle correctly, just instantly). WebKit has neither, and falls back to `ExpandingPanelClassic`'s `grid-template-rows` technique automatically (see the Overview section above) that path animates in every browser regardless of `interpolate-size`.
216
+ - The open/close animation targets `::details-content` (`height: 0 → auto`, `overflow: clip`) — no JS height measurement needed. Requires `interpolate-size: allow-keywords` for the height transition itself (Chromium only as of 2026; other browsers with `::details-content` support still toggle correctly, just instantly). WebKit has neither, and there is no automatic fallback — use `ExpandingPanelClassic` (see the Overview section above) for anything that needs to work correctly there.
217
217
  - `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 { ... }`.
218
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
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.
@@ -16,6 +16,7 @@ active-route highlighting, hover indicator).
16
16
  | Prop | Type | Default | Notes |
17
17
  |------|------|---------|-------|
18
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
+ | `panelVariant` | `"modern" \| "classic"` | `"classic"` | Which panel component renders a dropdown's submenu: `ExpandingPanel` (`"modern"`) or `ExpandingPanelClassic` (`"classic"`, default). `"modern"` is known not to work correctly on WebKit. Normally set via `ResponsiveHeader`'s own `panelVariant` prop rather than directly. See CLAUDE.md pitfall #19. |
19
20
  | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root `.overflow-navigation-wrapper` element. |
20
21
 
21
22
  ---
@@ -106,6 +106,7 @@ buggy toggle-based version too.
106
106
  | `collapseBreakpoint` | `number \| null` | `null` | A fixed pixel width below which the whole main nav collapses into the overflow burger, instead of the default per-item responsive collapse. |
107
107
  | `collapseAtMainNavIntersection` | `boolean` | `false` | Like `collapseBreakpoint`, but the breakpoint is derived automatically from the main nav's own measured width rather than a fixed number. |
108
108
  | `allowExpandOnGesture` | `boolean` | `true` | When `true`, hovering/focusing a dropdown summary opens it (in addition to click). When `false`, only click toggles it. |
109
+ | `panelVariant` | `"modern" \| "classic"` | `"classic"` | Forwarded to `NavigationItems` for its dropdown submenu panels: `ExpandingPanel` (`"modern"`) or `ExpandingPanelClassic` (`"classic"`, default). `"modern"` is known not to work correctly on WebKit. See CLAUDE.md pitfall #19. |
109
110
  | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root `.navigation` element. |
110
111
 
111
112
  ## Slots
@@ -5,7 +5,8 @@
5
5
  <slot name="docsContent"></slot>
6
6
  </div>
7
7
  <div v-if="hasDocsNav" class="docs-nav">
8
- <ExpandingPanel
8
+ <component
9
+ :is="panelComponent"
9
10
  v-model="docsNavPanelOpen"
10
11
  :name="docsNavPanelName"
11
12
  :animation-duration="200"
@@ -37,10 +38,11 @@
37
38
  </ul>
38
39
  </nav>
39
40
  </template>
40
- </ExpandingPanel>
41
+ </component>
41
42
  </div>
42
43
  <div v-if="hasDocsPageNav" class="docs-page-nav">
43
- <ExpandingPanel
44
+ <component
45
+ :is="panelComponent"
44
46
  v-model="docsPageNavPanelOpen"
45
47
  :name="docsPageNavPanelName"
46
48
  :animation-duration="200"
@@ -72,7 +74,7 @@
72
74
  </ul>
73
75
  </nav>
74
76
  </template>
75
- </ExpandingPanel>
77
+ </component>
76
78
  </div>
77
79
  </div>
78
80
  </component>
@@ -80,6 +82,8 @@
80
82
 
81
83
  <script setup lang="ts">
82
84
  import type { DocsNavItem } from "~/types/components";
85
+ import ExpandingPanel from "~/components/02.molecules/expandable/expanding-panel/ExpandingPanel.vue";
86
+ import ExpandingPanelClassic from "~/components/02.molecules/expandable/expanding-panel-classic/ExpandingPanelClassic.vue";
83
87
 
84
88
  interface Props {
85
89
  tag?: "div" | "section" | "article" | "main";
@@ -87,6 +91,7 @@ interface Props {
87
91
  docsPageNavItems?: DocsNavItem[];
88
92
  docsNavLabel?: string;
89
93
  docsPageNavLabel?: string;
94
+ panelVariant?: "modern" | "classic";
90
95
  styleClassPassthrough?: string | string[];
91
96
  }
92
97
  const props = withDefaults(defineProps<Props>(), {
@@ -95,9 +100,16 @@ const props = withDefaults(defineProps<Props>(), {
95
100
  docsPageNavItems: () => [],
96
101
  docsNavLabel: "Navigation",
97
102
  docsPageNavLabel: "On this page",
103
+ // Defaults to "classic" — the docs/admin nav toggle relies on contentIsOnTop on mobile, and
104
+ // ExpandingPanel's ::details-content-based positioning silently fails on WebKit (Safari/iOS),
105
+ // making the nav unreachable there. See AccordianCore's own variant prop and CLAUDE.md
106
+ // pitfall #19 for the full mechanism. "modern" is opt-in.
107
+ panelVariant: "classic",
98
108
  styleClassPassthrough: () => [],
99
109
  });
100
110
 
111
+ const panelComponent = computed(() => (props.panelVariant === "modern" ? ExpandingPanel : ExpandingPanelClassic));
112
+
101
113
  const activeNavItem = defineModel<string | undefined>("activeNavItem");
102
114
  const activePageNavItem = defineModel<string | undefined>("activePageNavItem");
103
115
 
@@ -238,12 +250,9 @@ const docsPageNavPanelName = computed(() => (isMobile.value ? panelGroupId : `${
238
250
  align-items: start;
239
251
 
240
252
  @container contentDocs (width >= 1024px) {
241
- .expanding-panel {
242
- & .expanding-panel-details {
243
- .expanding-panel-summary {
244
- cursor: default;
245
- }
246
- }
253
+ .expanding-panel-summary,
254
+ .expanding-panel-classic-summary {
255
+ cursor: default;
247
256
  }
248
257
  }
249
258
  }
@@ -256,12 +265,9 @@ const docsPageNavPanelName = computed(() => (isMobile.value ? panelGroupId : `${
256
265
  align-items: start;
257
266
 
258
267
  @container contentDocs (width >= 768px) {
259
- .expanding-panel {
260
- & .expanding-panel-details {
261
- .expanding-panel-summary {
262
- cursor: default;
263
- }
264
- }
268
+ .expanding-panel-summary,
269
+ .expanding-panel-classic-summary {
270
+ cursor: default;
265
271
  }
266
272
  }
267
273
  }
@@ -34,11 +34,15 @@ interface Props {
34
34
  styleClassPassthrough?: string | string[];
35
35
  }
36
36
 
37
+ // Defaults to "classic" (not "modern") — ExpandingPanel's animation/positioning relies on
38
+ // ::details-content, which WebKit (Safari/iOS) doesn't support (see ExpandingPanel's own dev-mode
39
+ // warning and CLAUDE.md pitfall #19). "modern" is opt-in for consumers who've confirmed it works
40
+ // for their audience, not the safe default.
37
41
  const props = withDefaults(defineProps<Props>(), {
38
42
  name: undefined,
39
43
  itemCount: 0,
40
44
  animationDuration: 300,
41
- variant: "modern",
45
+ variant: "classic",
42
46
  styleClassPassthrough: () => [],
43
47
  });
44
48
 
@@ -74,11 +74,11 @@ describe("AccordianCore", () => {
74
74
 
75
75
  // ─── Item count ───────────────────────────────────────────────────────────
76
76
 
77
- it("renders the correct number of ExpandingPanel components", async () => {
77
+ it("renders the correct number of panel components", async () => {
78
78
  const wrapper = await mountSuspended(AccordianCore, {
79
79
  props: { itemCount: 4 },
80
80
  });
81
- expect(wrapper.findAll(".expanding-panel")).toHaveLength(4);
81
+ expect(wrapper.findAll(".accordian-item")).toHaveLength(4);
82
82
  });
83
83
 
84
84
  it("renders no ExpandingPanel components when itemCount is 0", async () => {
@@ -95,15 +95,15 @@ describe("AccordianCore", () => {
95
95
 
96
96
  // ─── variant ──────────────────────────────────────────────────────────────
97
97
 
98
- it("renders ExpandingPanel by default (variant: modern)", async () => {
98
+ it("renders ExpandingPanelClassic by default (variant: classic)", async () => {
99
99
  const wrapper = await mountSuspended(AccordianCore, {
100
100
  props: { itemCount: 2 },
101
101
  });
102
- expect(wrapper.findAllComponents({ name: "ExpandingPanel" })).toHaveLength(2);
103
- expect(wrapper.findAllComponents({ name: "ExpandingPanelClassic" })).toHaveLength(0);
102
+ expect(wrapper.findAllComponents({ name: "ExpandingPanelClassic" })).toHaveLength(2);
103
+ expect(wrapper.findAllComponents({ name: "ExpandingPanel" })).toHaveLength(0);
104
104
  });
105
105
 
106
- it("renders ExpandingPanelClassic when variant is classic", async () => {
106
+ it("renders ExpandingPanelClassic when variant is classic (explicit)", async () => {
107
107
  const wrapper = await mountSuspended(AccordianCore, {
108
108
  props: { itemCount: 2, variant: "classic" },
109
109
  });
@@ -111,6 +111,14 @@ describe("AccordianCore", () => {
111
111
  expect(wrapper.findAllComponents({ name: "ExpandingPanel" })).toHaveLength(0);
112
112
  });
113
113
 
114
+ it("renders ExpandingPanel when variant is modern", async () => {
115
+ const wrapper = await mountSuspended(AccordianCore, {
116
+ props: { itemCount: 2, variant: "modern" },
117
+ });
118
+ expect(wrapper.findAllComponents({ name: "ExpandingPanel" })).toHaveLength(2);
119
+ expect(wrapper.findAllComponents({ name: "ExpandingPanelClassic" })).toHaveLength(0);
120
+ });
121
+
114
122
  it("forwards props identically to ExpandingPanelClassic when variant is classic", async () => {
115
123
  const wrapper = await mountSuspended(AccordianCore, {
116
124
  props: { itemCount: 2, name: "classic-accordian", animationDuration: 500, variant: "classic" },
@@ -122,11 +130,11 @@ describe("AccordianCore", () => {
122
130
  });
123
131
  });
124
132
 
125
- // ─── Props forwarded to ExpandingPanel ───────────────────────────────────
133
+ // ─── Props forwarded to ExpandingPanel (variant: modern) ─────────────────
126
134
 
127
135
  it("forwards animationDuration to every ExpandingPanel", async () => {
128
136
  const wrapper = await mountSuspended(AccordianCore, {
129
- props: { itemCount: 3, animationDuration: 500 },
137
+ props: { itemCount: 3, animationDuration: 500, variant: "modern" },
130
138
  });
131
139
  wrapper.findAllComponents({ name: "ExpandingPanel" }).forEach((panel) => {
132
140
  expect(panel.props("animationDuration")).toBe(500);
@@ -135,7 +143,7 @@ describe("AccordianCore", () => {
135
143
 
136
144
  it("forwards name to every ExpandingPanel", async () => {
137
145
  const wrapper = await mountSuspended(AccordianCore, {
138
- props: { itemCount: 2, name: "test-accordian" },
146
+ props: { itemCount: 2, name: "test-accordian", variant: "modern" },
139
147
  });
140
148
  wrapper.findAllComponents({ name: "ExpandingPanel" }).forEach((panel) => {
141
149
  expect(panel.props("name")).toBe("test-accordian");
@@ -144,7 +152,7 @@ describe("AccordianCore", () => {
144
152
 
145
153
  it("passes accordian-item styleClassPassthrough to every ExpandingPanel", async () => {
146
154
  const wrapper = await mountSuspended(AccordianCore, {
147
- props: { itemCount: 2 },
155
+ props: { itemCount: 2, variant: "modern" },
148
156
  });
149
157
  wrapper.findAllComponents({ name: "ExpandingPanel" }).forEach((panel) => {
150
158
  expect(panel.props("styleClassPassthrough")).toEqual(["accordian-item"]);
@@ -153,11 +161,11 @@ describe("AccordianCore", () => {
153
161
 
154
162
  // ─── Default prop values ──────────────────────────────────────────────────
155
163
 
156
- it("defaults animationDuration to 300 on ExpandingPanel when not provided", async () => {
164
+ it("defaults animationDuration to 300 on ExpandingPanelClassic when not provided", async () => {
157
165
  const wrapper = await mountSuspended(AccordianCore, {
158
166
  props: { itemCount: 1 },
159
167
  });
160
- const panel = wrapper.findComponent({ name: "ExpandingPanel" });
168
+ const panel = wrapper.findComponent({ name: "ExpandingPanelClassic" });
161
169
  expect(panel.props("animationDuration")).toBe(300);
162
170
  });
163
171
 
@@ -167,7 +175,7 @@ describe("AccordianCore", () => {
167
175
  expect(vm.name).toBeUndefined();
168
176
  expect(vm.itemCount).toBe(0);
169
177
  expect(vm.animationDuration).toBe(300);
170
- expect(vm.variant).toBe("modern");
178
+ expect(vm.variant).toBe("classic");
171
179
  expect(vm.styleClassPassthrough).toEqual([]);
172
180
  });
173
181
 
@@ -241,7 +249,7 @@ describe("AccordianCore", () => {
241
249
  "accordian-2-summary": "<span>Gamma</span>",
242
250
  },
243
251
  });
244
- const panels = wrapper.findAllComponents({ name: "ExpandingPanel" });
252
+ const panels = wrapper.findAllComponents({ name: "ExpandingPanelClassic" });
245
253
  expect(panels[0]?.text()).toContain("Alpha");
246
254
  expect(panels[1]?.text()).toContain("Beta");
247
255
  expect(panels[2]?.text()).toContain("Gamma");
@@ -2,78 +2,92 @@
2
2
 
3
3
  exports[`AccordianCore > renders correct HTML structure with all props set 1`] = `
4
4
  "<div class="display-accordian extra-class">
5
- <div class="expanding-panel accordian-item" icon-size="medium">
6
- <details class="expanding-panel-details" name="snapshot-accordian">
7
- <summary id="id-snapshot-accordian-trigger" class="expanding-panel-summary" aria-controls="id-snapshot-accordian-content" aria-expanded="false">
5
+ <div class="expanding-panel-classic accordian-item" icon-size="medium">
6
+ <details class="expanding-panel-classic-details" name="snapshot-accordian">
7
+ <summary id="id-snapshot-accordian-trigger" class="expanding-panel-classic-summary" aria-controls="id-snapshot-accordian-content" aria-expanded="false">
8
8
  <div class="label-wrapper"></div>
9
9
  <div class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon" 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 class="inner"></div></div>
12
11
  </details>
12
+ <div id="id-snapshot-accordian-content" class="expanding-panel-classic-content" aria-labelledby="id-snapshot-accordian-trigger" role="region">
13
+ <div class="inner"></div>
14
+ </div>
13
15
  </div>
14
- <div class="expanding-panel accordian-item" icon-size="medium">
15
- <details class="expanding-panel-details" name="snapshot-accordian">
16
- <summary id="id-snapshot-accordian-trigger" class="expanding-panel-summary" aria-controls="id-snapshot-accordian-content" aria-expanded="false">
16
+ <div class="expanding-panel-classic accordian-item" icon-size="medium">
17
+ <details class="expanding-panel-classic-details" name="snapshot-accordian">
18
+ <summary id="id-snapshot-accordian-trigger" class="expanding-panel-classic-summary" aria-controls="id-snapshot-accordian-content" aria-expanded="false">
17
19
  <div class="label-wrapper"></div>
18
20
  <div class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon" aria-hidden="true"></span></div>
19
21
  </summary>
20
- <div id="id-snapshot-accordian-content" class="expanding-panel-content" aria-labelledby="id-snapshot-accordian-trigger" role="region"><div class="inner"></div></div>
21
22
  </details>
23
+ <div id="id-snapshot-accordian-content" class="expanding-panel-classic-content" aria-labelledby="id-snapshot-accordian-trigger" role="region">
24
+ <div class="inner"></div>
25
+ </div>
22
26
  </div>
23
- <div class="expanding-panel accordian-item" icon-size="medium">
24
- <details class="expanding-panel-details" name="snapshot-accordian">
25
- <summary id="id-snapshot-accordian-trigger" class="expanding-panel-summary" aria-controls="id-snapshot-accordian-content" aria-expanded="false">
27
+ <div class="expanding-panel-classic accordian-item" icon-size="medium">
28
+ <details class="expanding-panel-classic-details" name="snapshot-accordian">
29
+ <summary id="id-snapshot-accordian-trigger" class="expanding-panel-classic-summary" aria-controls="id-snapshot-accordian-content" aria-expanded="false">
26
30
  <div class="label-wrapper"></div>
27
31
  <div class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon" aria-hidden="true"></span></div>
28
32
  </summary>
29
- <div id="id-snapshot-accordian-content" class="expanding-panel-content" aria-labelledby="id-snapshot-accordian-trigger" role="region"><div class="inner"></div></div>
30
33
  </details>
34
+ <div id="id-snapshot-accordian-content" class="expanding-panel-classic-content" aria-labelledby="id-snapshot-accordian-trigger" role="region">
35
+ <div class="inner"></div>
36
+ </div>
31
37
  </div>
32
38
  </div>"
33
39
  `;
34
40
 
35
41
  exports[`AccordianCore > renders correct HTML structure with default props 1`] = `
36
42
  "<div class="display-accordian">
37
- <div class="expanding-panel accordian-item" icon-size="medium">
38
- <details class="expanding-panel-details" name="v-0-0-0">
39
- <summary id="id-v-0-0-0-trigger" class="expanding-panel-summary" aria-controls="id-v-0-0-0-content" aria-expanded="false">
43
+ <div class="expanding-panel-classic accordian-item" icon-size="medium">
44
+ <details class="expanding-panel-classic-details" name="v-0-0-0">
45
+ <summary id="id-v-0-0-0-trigger" class="expanding-panel-classic-summary" aria-controls="id-v-0-0-0-content" aria-expanded="false">
40
46
  <div class="label-wrapper"></div>
41
47
  <div class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon" aria-hidden="true"></span></div>
42
48
  </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 class="inner"></div></div>
44
49
  </details>
50
+ <div id="id-v-0-0-0-content" class="expanding-panel-classic-content" aria-labelledby="id-v-0-0-0-trigger" role="region">
51
+ <div class="inner"></div>
52
+ </div>
45
53
  </div>
46
- <div class="expanding-panel accordian-item" icon-size="medium">
47
- <details class="expanding-panel-details" name="v-0-0-1">
48
- <summary id="id-v-0-0-1-trigger" class="expanding-panel-summary" aria-controls="id-v-0-0-1-content" aria-expanded="false">
54
+ <div class="expanding-panel-classic accordian-item" icon-size="medium">
55
+ <details class="expanding-panel-classic-details" name="v-0-0-1">
56
+ <summary id="id-v-0-0-1-trigger" class="expanding-panel-classic-summary" aria-controls="id-v-0-0-1-content" aria-expanded="false">
49
57
  <div class="label-wrapper"></div>
50
58
  <div class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon" aria-hidden="true"></span></div>
51
59
  </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 class="inner"></div></div>
53
60
  </details>
61
+ <div id="id-v-0-0-1-content" class="expanding-panel-classic-content" aria-labelledby="id-v-0-0-1-trigger" role="region">
62
+ <div class="inner"></div>
63
+ </div>
54
64
  </div>
55
65
  </div>"
56
66
  `;
57
67
 
58
68
  exports[`AccordianCore > renders correct HTML structure with populated slots 1`] = `
59
69
  "<div class="display-accordian">
60
- <div class="expanding-panel accordian-item" icon-size="medium">
61
- <details class="expanding-panel-details" name="v-0-0-0">
62
- <summary id="id-v-0-0-0-trigger" class="expanding-panel-summary" aria-controls="id-v-0-0-0-content" aria-expanded="false">
70
+ <div class="expanding-panel-classic accordian-item" icon-size="medium">
71
+ <details class="expanding-panel-classic-details" name="v-0-0-0">
72
+ <summary id="id-v-0-0-0-trigger" class="expanding-panel-classic-summary" aria-controls="id-v-0-0-0-content" aria-expanded="false">
63
73
  <div class="label-wrapper"><span>Summary 0</span></div>
64
74
  <div class="icon-wrapper"><span>Icon 0</span></div>
65
75
  </summary>
66
- <div id="id-v-0-0-0-content" class="expanding-panel-content" aria-labelledby="id-v-0-0-0-trigger" role="region"><div class="inner"><span>Content 0</span></div></div>
67
76
  </details>
77
+ <div id="id-v-0-0-0-content" class="expanding-panel-classic-content" aria-labelledby="id-v-0-0-0-trigger" role="region">
78
+ <div class="inner"><span>Content 0</span></div>
79
+ </div>
68
80
  </div>
69
- <div class="expanding-panel accordian-item" icon-size="medium">
70
- <details class="expanding-panel-details" name="v-0-0-1">
71
- <summary id="id-v-0-0-1-trigger" class="expanding-panel-summary" aria-controls="id-v-0-0-1-content" aria-expanded="false">
81
+ <div class="expanding-panel-classic accordian-item" icon-size="medium">
82
+ <details class="expanding-panel-classic-details" name="v-0-0-1">
83
+ <summary id="id-v-0-0-1-trigger" class="expanding-panel-classic-summary" aria-controls="id-v-0-0-1-content" aria-expanded="false">
72
84
  <div class="label-wrapper"><span>Summary 1</span></div>
73
85
  <div class="icon-wrapper"><span>Icon 1</span></div>
74
86
  </summary>
75
- <div id="id-v-0-0-1-content" class="expanding-panel-content" aria-labelledby="id-v-0-0-1-trigger" role="region"><div class="inner"><span>Content 1</span></div></div>
76
87
  </details>
88
+ <div id="id-v-0-0-1-content" class="expanding-panel-classic-content" aria-labelledby="id-v-0-0-1-trigger" role="region">
89
+ <div class="inner"><span>Content 1</span></div>
90
+ </div>
77
91
  </div>
78
92
  </div>"
79
93
  `;
@@ -90,16 +90,14 @@ visual style, pass a modifier class:
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
92
  - `background-color`, `padding`, `border`, and shadow are all safe to set directly on
93
- `.expanding-panel-content`. Where `::details-content` is supported (not WebKit see below), the
94
- open/close animation clips via `overflow: clip` on that pseudo-element, and
95
- `.expanding-panel-content` is a child of it, so its own box is correctly clipped to the animated
96
- height while closed/closing. A wrapper inside the `#content` slot works equally well if you
97
- prefer to keep the styling scoped to your own markup.
98
- - WebKit (Safari/iOS) doesn't support `::details-content``ExpandingPanel` falls back to
99
- `ExpandingPanelClassic`'s `grid-template-rows` technique there automatically, no consumer action
100
- needed. That path wraps the `#content` slot in an internal `.inner` div (an implementation
101
- detail of the collapse mechanism, not a styling hook) but styling on `.expanding-panel-content`
102
- itself still works correctly, same as the `::details-content` path.
93
+ `.expanding-panel-content`. The open/close animation clips via `overflow: clip` on
94
+ `::details-content`, and `.expanding-panel-content` is a child of it, so its own box is
95
+ correctly clipped to the animated height while closed/closing. A wrapper inside the `#content`
96
+ slot works equally well if you prefer to keep the styling scoped to your own markup.
97
+ - **WebKit (Safari/iOS) doesn't support `::details-content` at all**, and `ExpandingPanel` has no
98
+ fallback for it (an `@supports`-based CSS fallback was tried and reverted 2026-08-22it didn't
99
+ reliably work). If this panel needs to work correctly on real Apple devices, use
100
+ `ExpandingPanelClassic` instead see its own `CONSUMER-STYLING.md`.
103
101
  - Don't stack multiple `contentIsOnTop` panels as direct siblings (linked via a shared `name` or
104
102
  not). The overlay is absolutely positioned so it doesn't push the next element down — which
105
103
  means a sibling `ExpandingPanel` placed right after it sits exactly where the overlay renders,
@@ -23,9 +23,7 @@
23
23
  :aria-labelledby="`id-${name}-trigger`"
24
24
  role="region"
25
25
  >
26
- <div class="inner">
27
- <slot name="content"></slot>
28
- </div>
26
+ <slot name="content"></slot>
29
27
  </div>
30
28
  </details>
31
29
  </div>
@@ -66,6 +64,20 @@ onClickOutside(rootEl, () => {
66
64
  });
67
65
 
68
66
  if (import.meta.dev) {
67
+ // ::details-content (the animation/positioning mechanism this component relies on) is not
68
+ // supported in WebKit (Safari/iOS) as of 2026-08 — confirmed via real-device testing, not
69
+ // reproducible in Chrome's device-emulation mode since that's still the Blink engine. An
70
+ // earlier @supports-based fallback attempt (2026-08-22) turned out not to reliably kick in
71
+ // either. Prefer ExpandingPanelClassic (or a consumer's own `classic` prop, where offered —
72
+ // see AccordianCore) for anything that needs to work correctly on real Apple devices today.
73
+ console.warn(
74
+ `ExpandingPanel${props.name ? ` "${props.name}"` : ""}: this variant depends on ` +
75
+ "::details-content, which WebKit (Safari/iOS) does not support — the animation and, more " +
76
+ "seriously, contentIsOnTop's overlay positioning silently fail there with no console error. " +
77
+ "Use ExpandingPanelClassic instead if this panel needs to work correctly on real Apple " +
78
+ "devices."
79
+ );
80
+
69
81
  watch(
70
82
  () => props.contentIsOnTop,
71
83
  (value) => {
@@ -189,50 +201,6 @@ const onDetailsToggle = (event: Event) => {
189
201
  }
190
202
  }
191
203
  }
192
-
193
- /* ::details-content isn't supported in WebKit (Safari/iOS) as of 2026-08 — the two rule
194
- blocks above are silently dropped there entirely (no error, no animation, and crucially
195
- content-is-on-top's position: absolute never applies, so the overlay renders in normal
196
- document flow instead). This reproduces both using ExpandingPanelClassic's
197
- grid-template-rows technique (broadly supported, including Safari), targeting the real
198
- .expanding-panel-content element (and its .inner child, matching Classic's structure —
199
- .inner's own overflow:hidden is what lets the grid row actually collapse to 0, since a
200
- plain block child's default min-height:auto would otherwise floor it at content height)
201
- instead of the pseudo-element. */
202
- @supports not selector(::details-content) {
203
- .expanding-panel-details {
204
- .expanding-panel-content {
205
- display: grid;
206
- grid-template-rows: 0fr;
207
- transition: grid-template-rows v-bind(animationDurationStr) ease-in-out;
208
- will-change: grid-template-rows;
209
-
210
- .inner {
211
- overflow: hidden;
212
- margin-top: 0;
213
- }
214
- }
215
-
216
- &[open] .expanding-panel-content {
217
- grid-template-rows: 1fr;
218
- }
219
- }
220
- }
221
-
222
- &.content-is-on-top {
223
- @supports not selector(::details-content) {
224
- .expanding-panel-details {
225
- position: relative;
226
-
227
- .expanding-panel-content {
228
- position: absolute;
229
- top: calc(100% + var(--expanding-panel-content-gap, 0px));
230
- inset-inline: 0;
231
- z-index: var(--expanding-panel-content-z-index, 10);
232
- }
233
- }
234
- }
235
- }
236
204
  }
237
205
  }
238
206
  </style>
@@ -7,9 +7,7 @@ exports[`ExpandingPanel > renders correct HTML structure with all props set 1`]
7
7
  <div class="label-wrapper"></div>
8
8
  <div class="icon-wrapper icon-wrapper--hidden"><span class="iconify i-bi:caret-down-fill icon" aria-hidden="true"></span></div>
9
9
  </summary>
10
- <div id="id-snap-all-content" class="expanding-panel-content" aria-labelledby="id-snap-all-trigger" role="region">
11
- <div class="inner"></div>
12
- </div>
10
+ <div id="id-snap-all-content" class="expanding-panel-content" aria-labelledby="id-snap-all-trigger" role="region"></div>
13
11
  </details>
14
12
  </div>"
15
13
  `;
@@ -21,9 +19,7 @@ exports[`ExpandingPanel > renders correct HTML structure with contentIsOnTop tru
21
19
  <div class="label-wrapper"></div>
22
20
  <div class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon" aria-hidden="true"></span></div>
23
21
  </summary>
24
- <div id="id-snap-content-on-top-content" class="expanding-panel-content" aria-labelledby="id-snap-content-on-top-trigger" role="region">
25
- <div class="inner"></div>
26
- </div>
22
+ <div id="id-snap-content-on-top-content" class="expanding-panel-content" aria-labelledby="id-snap-content-on-top-trigger" role="region"></div>
27
23
  </details>
28
24
  </div>"
29
25
  `;
@@ -35,9 +31,7 @@ exports[`ExpandingPanel > renders correct HTML structure with default props 1`]
35
31
  <div class="label-wrapper"></div>
36
32
  <div class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon" aria-hidden="true"></span></div>
37
33
  </summary>
38
- <div id="id-snap-default-content" class="expanding-panel-content" aria-labelledby="id-snap-default-trigger" role="region">
39
- <div class="inner"></div>
40
- </div>
34
+ <div id="id-snap-default-content" class="expanding-panel-content" aria-labelledby="id-snap-default-trigger" role="region"></div>
41
35
  </details>
42
36
  </div>"
43
37
  `;
@@ -50,9 +44,7 @@ exports[`ExpandingPanel > renders correct HTML structure with populated slots 1`
50
44
  <div class="icon-wrapper"><span>▸</span></div>
51
45
  </summary>
52
46
  <div id="id-snap-slots-content" class="expanding-panel-content" aria-labelledby="id-snap-slots-trigger" role="region">
53
- <div class="inner">
54
- <p>Content text</p>
55
- </div>
47
+ <p>Content text</p>
56
48
  </div>
57
49
  </details>
58
50
  </div>"
@@ -56,7 +56,8 @@
56
56
  role="none"
57
57
  @mouseenter="hoveredItemKey = `${String(groupKey)}-${localIndex}`"
58
58
  >
59
- <ExpandingPanel
59
+ <component
60
+ :is="panelComponent"
60
61
  v-model="panelOpenStates[`${String(groupKey)}-${localIndex}`]"
61
62
  name="overflow-navigation-group"
62
63
  :animation-duration="DETAILS_ANIMATION_DURATION"
@@ -96,7 +97,7 @@
96
97
  <div aria-hidden="true" class="overflow-sub-nav-indicator-hovered"></div>
97
98
  </div>
98
99
  </template>
99
- </ExpandingPanel>
100
+ </component>
100
101
  </li>
101
102
  </template>
102
103
  </ul>
@@ -107,17 +108,26 @@
107
108
 
108
109
  <script setup lang="ts">
109
110
  import type { ResponsiveHeaderState, ResponsiveHeaderNavItem } from "../../../types/components";
111
+ import ExpandingPanel from "../../02.molecules/expandable/expanding-panel/ExpandingPanel.vue";
112
+ import ExpandingPanelClassic from "../../02.molecules/expandable/expanding-panel-classic/ExpandingPanelClassic.vue";
110
113
 
111
114
  interface Props {
112
115
  mainNavigationState?: ResponsiveHeaderState;
116
+ panelVariant?: "modern" | "classic";
113
117
  styleClassPassthrough?: string | string[];
114
118
  }
115
119
 
116
120
  const props = withDefaults(defineProps<Props>(), {
117
121
  mainNavigationState: () => ({ clonedNavLinks: {}, navListVisibility: {}, hasSecondNav: false }),
122
+ // Defaults to "classic" — see ExpandingPanel's own dev-mode warning and CLAUDE.md pitfall #19:
123
+ // its ::details-content-based implementation doesn't work correctly on WebKit (Safari/iOS).
124
+ // "modern" is opt-in.
125
+ panelVariant: "classic",
118
126
  styleClassPassthrough: () => [],
119
127
  });
120
128
 
129
+ const panelComponent = computed(() => (props.panelVariant === "modern" ? ExpandingPanel : ExpandingPanelClassic));
130
+
121
131
  const hoveredItemKey = ref<string | null>(null);
122
132
  const hoveredChildKey = ref<string | null>(null);
123
133
 
@@ -231,6 +241,12 @@ watch(
231
241
  }
232
242
 
233
243
  .overflow-navigation-details {
244
+ /* Two variants share this styling, one full block each rather than a merged
245
+ selector list — their DOM shapes differ (modern nests .expanding-panel-content
246
+ *inside* .expanding-panel-details; classic renders .expanding-panel-classic-content
247
+ as a *sibling* of .expanding-panel-classic-details), so the [open] + ... sibling
248
+ combinator below is only structurally correct for the classic block. See
249
+ ExpandingPanel vs ExpandingPanelClassic's own templates. */
234
250
  &.expanding-panel {
235
251
  margin-block-end: 0;
236
252
 
@@ -293,6 +309,69 @@ watch(
293
309
  }
294
310
  }
295
311
  }
312
+
313
+ &.expanding-panel-classic {
314
+ margin-block-end: 0;
315
+
316
+ .expanding-panel-classic-details {
317
+ .expanding-panel-classic-summary {
318
+ padding-block: var(--overflow-nav-items-padding-block, 0.8rem);
319
+ padding-inline: var(--overflow-nav-padding-inline, 0.8rem);
320
+ gap: 1rem;
321
+ color: var(--overflow-nav-link-color, inherit);
322
+ border-bottom: 0.1rem solid var(--overflow-nav-link-border-color, #efefef75);
323
+
324
+ .label-wrapper {
325
+ .overflow-navigation-text {
326
+ text-wrap: nowrap;
327
+ }
328
+ }
329
+ .icon-wrapper {
330
+ padding: 0;
331
+ }
332
+ }
333
+
334
+ &[open] {
335
+ .expanding-panel-classic-summary {
336
+ border-bottom: 0.1rem solid transparent;
337
+ }
338
+ + .expanding-panel-classic-content {
339
+ border-bottom: 0.1rem solid var(--overflow-nav-link-border-color, #efefef75);
340
+ .overflow-navigation-sub-nav-inner {
341
+ margin-top: var(--overflow-nav-items-gap, 0px);
342
+ }
343
+ }
344
+ }
345
+ }
346
+
347
+ .expanding-panel-classic-content {
348
+ border-bottom: 0.1rem solid transparent;
349
+
350
+ .overflow-navigation-sub-nav-inner {
351
+ margin-top: 0;
352
+ position: relative;
353
+
354
+ .overflow-navigation-sub-nav-list {
355
+ display: flex;
356
+ flex-direction: column;
357
+ gap: 2px;
358
+
359
+ .overflow-navigation-sub-nav-item {
360
+ padding-block: var(--overflow-nav-items-padding-block, 0.8rem);
361
+ padding-inline: var(--overflow-nav-padding-inline, 0.8rem);
362
+ font-size: var(--overflow-nav-sub-item-font-size, inherit);
363
+ color: var(--overflow-nav-sub-item-color, inherit);
364
+
365
+ .overflow-navigation-sub-nav-link {
366
+ display: block;
367
+ text-decoration: none;
368
+ color: inherit;
369
+ }
370
+ }
371
+ }
372
+ }
373
+ }
374
+ }
296
375
  }
297
376
  }
298
377
  }
@@ -325,7 +404,11 @@ watch(
325
404
  .overflow-navigation-wrapper
326
405
  .overflow-navigation-item.is-hovered
327
406
  .overflow-navigation-details
328
- .expanding-panel-summary {
407
+ .expanding-panel-summary,
408
+ .overflow-navigation-wrapper
409
+ .overflow-navigation-item.is-hovered
410
+ .overflow-navigation-details
411
+ .expanding-panel-classic-summary {
329
412
  anchor-name: --overflow-nav-indicator;
330
413
  }
331
414
 
@@ -340,7 +423,11 @@ watch(
340
423
  .overflow-navigation-wrapper:not(:has(.overflow-navigation-item.is-hovered))
341
424
  .overflow-navigation-item.is-active
342
425
  .overflow-navigation-details
343
- .expanding-panel-summary {
426
+ .expanding-panel-summary,
427
+ .overflow-navigation-wrapper:not(:has(.overflow-navigation-item.is-hovered))
428
+ .overflow-navigation-item.is-active
429
+ .overflow-navigation-details
430
+ .expanding-panel-classic-summary {
344
431
  anchor-name: --overflow-nav-indicator;
345
432
  }
346
433
 
@@ -100,7 +100,7 @@
100
100
  />
101
101
  </summary>
102
102
  <div class="overflow-details-nav" role="menu">
103
- <NavigationItems :main-navigation-state="mainNavigationState" />
103
+ <NavigationItems :main-navigation-state="mainNavigationState" :panel-variant="panelVariant" />
104
104
  </div>
105
105
  </details>
106
106
  <slot v-if="slots.secondaryNavigation" name="secondaryNavigation"></slot>
@@ -125,6 +125,7 @@ interface Props {
125
125
  collapseAtMainNavIntersection?: boolean;
126
126
  styleClassPassthrough?: string | string[];
127
127
  allowExpandOnGesture?: boolean;
128
+ panelVariant?: "modern" | "classic";
128
129
  }
129
130
 
130
131
  const props = withDefaults(defineProps<Props>(), {
@@ -138,6 +139,9 @@ const props = withDefaults(defineProps<Props>(), {
138
139
  collapseAtMainNavIntersection: false,
139
140
  styleClassPassthrough: () => [],
140
141
  allowExpandOnGesture: true,
142
+ // Forwarded to NavigationItems' overflow submenu panels — see its own default for why
143
+ // "classic" is the safe default (CLAUDE.md pitfall #19).
144
+ panelVariant: "classic",
141
145
  });
142
146
 
143
147
  const collapseNavigationBelowWidth = computed(
@@ -138,10 +138,10 @@ describe("NavigationItems", () => {
138
138
  const wrapper = await mountSuspended(NavigationItems, {
139
139
  props: { mainNavigationState },
140
140
  });
141
- const details = wrapper.find(".expanding-panel-details");
141
+ const details = wrapper.find(".expanding-panel-classic-details");
142
142
  expect(details.attributes("open")).toBeUndefined();
143
143
 
144
- await wrapper.find(".expanding-panel-summary").trigger("click");
144
+ await wrapper.find(".expanding-panel-classic-summary").trigger("click");
145
145
  await nextTick();
146
146
  expect(details.attributes("open")).toBe("");
147
147
  });
@@ -6,24 +6,24 @@ exports[`NavigationItems > renders correct HTML structure with default props 1`]
6
6
  <li class="overflow-navigation-item is-active" style="--_main-navigation-item-width: 0px;" data-group-key="firstNav" data-local-index="0" role="none"><a href="/" class="overflow-navigation-link" role="menuitem"><span class="overflow-navigation-text">Home</span></a></li>
7
7
  <li class="overflow-navigation-item visible" style="--_main-navigation-item-width: 0px;" data-group-key="firstNav" data-local-index="1" role="none"><a href="/about" class="overflow-navigation-link" role="menuitem"><span class="overflow-navigation-text">About</span></a></li>
8
8
  <li class="overflow-navigation-item visible" style="--_main-navigation-item-width: 0px;" data-group-key="firstNav" data-local-index="2" role="none">
9
- <div class="expanding-panel overflow-navigation-details">
10
- <details class="expanding-panel-details" name="overflow-navigation-group">
11
- <summary id="id-overflow-navigation-group-trigger" class="expanding-panel-summary" aria-controls="id-overflow-navigation-group-content" aria-expanded="false">
9
+ <div class="expanding-panel-classic overflow-navigation-details">
10
+ <details class="expanding-panel-classic-details" name="overflow-navigation-group">
11
+ <summary id="id-overflow-navigation-group-trigger" class="expanding-panel-classic-summary" aria-controls="id-overflow-navigation-group-content" aria-expanded="false">
12
12
  <div class="label-wrapper"><span class="overflow-navigation-text" aria-label="UI Components submenu" role="menuitem" aria-haspopup="true">UI Components</span></div>
13
13
  <div class="icon-wrapper"><span class="iconify i-mdi:chevron-down icon" aria-hidden="true"></span></div>
14
14
  </summary>
15
- <div id="id-overflow-navigation-group-content" class="expanding-panel-content" aria-labelledby="id-overflow-navigation-group-trigger" role="region">
16
- <div class="inner">
17
- <div class="overflow-navigation-sub-nav-inner">
18
- <ul class="overflow-navigation-sub-nav-list">
19
- <li class="overflow-navigation-sub-nav-item"><a href="/forms/examples/buttons" class="overflow-navigation-sub-nav-link" role="menuitem"><span class="overflow-navigation-sub-nav-text">Buttons</span></a></li>
20
- <li class="overflow-navigation-sub-nav-item"><a href="/ui/tabs" class="overflow-navigation-sub-nav-link" role="menuitem"><span class="overflow-navigation-sub-nav-text">Tabs</span></a></li>
21
- </ul>
22
- <div aria-hidden="true" class="overflow-sub-nav-indicator-hovered"></div>
23
- </div>
15
+ </details>
16
+ <div id="id-overflow-navigation-group-content" class="expanding-panel-classic-content" aria-labelledby="id-overflow-navigation-group-trigger" role="region">
17
+ <div class="inner">
18
+ <div class="overflow-navigation-sub-nav-inner">
19
+ <ul class="overflow-navigation-sub-nav-list">
20
+ <li class="overflow-navigation-sub-nav-item"><a href="/forms/examples/buttons" class="overflow-navigation-sub-nav-link" role="menuitem"><span class="overflow-navigation-sub-nav-text">Buttons</span></a></li>
21
+ <li class="overflow-navigation-sub-nav-item"><a href="/ui/tabs" class="overflow-navigation-sub-nav-link" role="menuitem"><span class="overflow-navigation-sub-nav-text">Tabs</span></a></li>
22
+ </ul>
23
+ <div aria-hidden="true" class="overflow-sub-nav-indicator-hovered"></div>
24
24
  </div>
25
25
  </div>
26
- </details>
26
+ </div>
27
27
  </div>
28
28
  </li>
29
29
  </ul>
@@ -39,24 +39,24 @@ exports[`ResponsiveHeader > renders correct HTML structure with default props 1`
39
39
  <li class="overflow-navigation-item visible is-active" style="--_main-navigation-item-width: 0px;" data-group-key="firstNav" data-local-index="0" role="none"><a href="/" class="overflow-navigation-link" role="menuitem"><span class="overflow-navigation-text">Home</span></a></li>
40
40
  <li class="overflow-navigation-item visible" style="--_main-navigation-item-width: 0px;" data-group-key="firstNav" data-local-index="1" role="none"><a href="/about" class="overflow-navigation-link" role="menuitem"><span class="overflow-navigation-text">About</span></a></li>
41
41
  <li class="overflow-navigation-item visible" style="--_main-navigation-item-width: 0px;" data-group-key="firstNav" data-local-index="2" role="none">
42
- <div class="expanding-panel overflow-navigation-details">
43
- <details class="expanding-panel-details" name="overflow-navigation-group">
44
- <summary id="id-overflow-navigation-group-trigger" class="expanding-panel-summary" aria-controls="id-overflow-navigation-group-content" aria-expanded="false">
42
+ <div class="expanding-panel-classic overflow-navigation-details">
43
+ <details class="expanding-panel-classic-details" name="overflow-navigation-group">
44
+ <summary id="id-overflow-navigation-group-trigger" class="expanding-panel-classic-summary" aria-controls="id-overflow-navigation-group-content" aria-expanded="false">
45
45
  <div class="label-wrapper"><span class="overflow-navigation-text" aria-label="UI Components submenu" role="menuitem" aria-haspopup="true">UI Components</span></div>
46
46
  <div class="icon-wrapper"><span class="iconify i-mdi:chevron-down icon" aria-hidden="true"></span></div>
47
47
  </summary>
48
- <div id="id-overflow-navigation-group-content" class="expanding-panel-content" aria-labelledby="id-overflow-navigation-group-trigger" role="region">
49
- <div class="inner">
50
- <div class="overflow-navigation-sub-nav-inner">
51
- <ul class="overflow-navigation-sub-nav-list">
52
- <li class="overflow-navigation-sub-nav-item"><a href="/forms/examples/buttons" class="overflow-navigation-sub-nav-link" role="menuitem"><span class="overflow-navigation-sub-nav-text">Buttons</span></a></li>
53
- <li class="overflow-navigation-sub-nav-item"><a href="/ui/tabs" class="overflow-navigation-sub-nav-link" role="menuitem"><span class="overflow-navigation-sub-nav-text">Tabs</span></a></li>
54
- </ul>
55
- <div aria-hidden="true" class="overflow-sub-nav-indicator-hovered"></div>
56
- </div>
48
+ </details>
49
+ <div id="id-overflow-navigation-group-content" class="expanding-panel-classic-content" aria-labelledby="id-overflow-navigation-group-trigger" role="region">
50
+ <div class="inner">
51
+ <div class="overflow-navigation-sub-nav-inner">
52
+ <ul class="overflow-navigation-sub-nav-list">
53
+ <li class="overflow-navigation-sub-nav-item"><a href="/forms/examples/buttons" class="overflow-navigation-sub-nav-link" role="menuitem"><span class="overflow-navigation-sub-nav-text">Buttons</span></a></li>
54
+ <li class="overflow-navigation-sub-nav-item"><a href="/ui/tabs" class="overflow-navigation-sub-nav-link" role="menuitem"><span class="overflow-navigation-sub-nav-text">Tabs</span></a></li>
55
+ </ul>
56
+ <div aria-hidden="true" class="overflow-sub-nav-indicator-hovered"></div>
57
57
  </div>
58
58
  </div>
59
- </details>
59
+ </div>
60
60
  </div>
61
61
  </li>
62
62
  </ul>
@@ -318,6 +318,25 @@ watch(
318
318
  }
319
319
  }
320
320
  }
321
+
322
+ .accordian-item.expanding-panel-classic {
323
+ border-block-end: 1px solid var(--services-section-faq-divider-color, currentColor);
324
+ opacity: var(--services-section-faq-divider-opacity, 0.7);
325
+
326
+ &:first-child {
327
+ border-block-start: 1px solid var(--services-section-faq-divider-color, currentColor);
328
+ }
329
+
330
+ .expanding-panel-classic-details .expanding-panel-classic-summary {
331
+ padding-block: 1.2rem;
332
+ }
333
+
334
+ .expanding-panel-classic-content .inner {
335
+ .services-section__faq-answer {
336
+ line-height: var(--services-section-faq-answer-line-height, 1.6);
337
+ }
338
+ }
339
+ }
321
340
  }
322
341
  }
323
342
  }
@@ -61,18 +61,18 @@ exports[`ServicesSection > renders correct HTML structure 1`] = `
61
61
  <!--v-if--><span class="text-block-0 normal">Frequently Asked Questions</span>
62
62
  </h2>
63
63
  <div class="display-accordian services-section__faq" id="faq">
64
- <div class="expanding-panel accordian-item" icon-size="medium">
65
- <details class="expanding-panel-details" name="faq-v-0-0-0">
66
- <summary id="id-faq-v-0-0-0-trigger" class="expanding-panel-summary" aria-controls="id-faq-v-0-0-0-content" aria-expanded="false">
64
+ <div class="expanding-panel-classic accordian-item" icon-size="medium">
65
+ <details class="expanding-panel-classic-details" name="faq-v-0-0-0">
66
+ <summary id="id-faq-v-0-0-0-trigger" class="expanding-panel-classic-summary" aria-controls="id-faq-v-0-0-0-content" aria-expanded="false">
67
67
  <div class="label-wrapper">FAQ question?</div>
68
68
  <div class="icon-wrapper"><span class="iconify i-mdi:chevron-down" aria-hidden="true"></span></div>
69
69
  </summary>
70
- <div id="id-faq-v-0-0-0-content" class="expanding-panel-content" aria-labelledby="id-faq-v-0-0-0-trigger" role="region">
71
- <div class="inner">
72
- <p class="services-section__faq-answer">FAQ answer.</p>
73
- </div>
74
- </div>
75
70
  </details>
71
+ <div id="id-faq-v-0-0-0-content" class="expanding-panel-classic-content" aria-labelledby="id-faq-v-0-0-0-trigger" role="region">
72
+ <div class="inner">
73
+ <p class="services-section__faq-answer">FAQ answer.</p>
74
+ </div>
75
+ </div>
76
76
  </div>
77
77
  </div>
78
78
  <div class="glass-panel services-section__glass-panel p-24">
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "srcdev-nuxt-components",
3
3
  "type": "module",
4
- "version": "9.3.6",
4
+ "version": "9.3.7",
5
5
  "main": "nuxt.config.ts",
6
6
  "types": "types.d.ts",
7
7
  "license": "MIT",