srcdev-nuxt-components 9.6.18 → 9.6.20

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 (187) hide show
  1. package/.claude/rules/consumer/css-relative-colour-calc.md +15 -0
  2. package/.claude/rules/consumer/ids-from-useid.md +3 -2
  3. package/.claude/rules/consumer/vue-dynamic-arg-no-spaces.md +16 -0
  4. package/.claude/skills/component-compliance-checklist.md +11 -1
  5. package/.claude/skills/component-dynamic-slots.md +2 -2
  6. package/.claude/skills/component-naming.md +1 -1
  7. package/.claude/skills/components/accordion-chat.md +99 -0
  8. package/.claude/skills/components/accordion-group.md +98 -0
  9. package/.claude/skills/components/action-menu.md +11 -5
  10. package/.claude/skills/components/auto-grid.md +3 -1
  11. package/.claude/skills/components/capture-qr-code.md +9 -5
  12. package/.claude/skills/components/carousel-flip.md +41 -30
  13. package/.claude/skills/components/column-flow-grid.md +2 -0
  14. package/.claude/skills/components/dashboard-quad-grid.md +7 -2
  15. package/.claude/skills/components/dashboard-stats-grid.md +4 -0
  16. package/.claude/skills/components/decode-qr-code.md +7 -2
  17. package/.claude/skills/components/deep-expanding-menu-classic.md +31 -30
  18. package/.claude/skills/components/deep-expanding-menu.md +22 -21
  19. package/.claude/skills/components/expanding-panel-classic.md +9 -7
  20. package/.claude/skills/components/expanding-panel.md +9 -8
  21. package/.claude/skills/components/grid-stack.md +2 -0
  22. package/.claude/skills/components/layout-grid-by-cols.md +2 -0
  23. package/.claude/skills/components/masonry-grid.md +9 -1
  24. package/.claude/skills/components/page-row.md +3 -0
  25. package/.claude/skills/components/samaritan-prompt.md +24 -8
  26. package/.claude/skills/components/site-header.md +16 -2
  27. package/.claude/skills/components/sparkle-effect.md +79 -0
  28. package/.claude/skills/index.md +16 -14
  29. package/.claude/skills/storybook-add-story.md +33 -0
  30. package/.claude/skills/testing-add-unit-test.md +26 -0
  31. package/.claude/skills/using-component-skills.md +1 -1
  32. package/.vscode/srcdev-component-accordion-chat.code-snippets +42 -0
  33. package/.vscode/srcdev-component-accordion-group.code-snippets +38 -0
  34. package/.vscode/srcdev-component-action-menu.code-snippets +3 -3
  35. package/.vscode/srcdev-component-capture-decode-qr-code.code-snippets +11 -2
  36. package/.vscode/srcdev-component-carousel-flip.code-snippets +22 -4
  37. package/.vscode/srcdev-component-deep-expanding-menu-classic.code-snippets +32 -2
  38. package/.vscode/srcdev-component-deep-expanding-menu.code-snippets +31 -1
  39. package/.vscode/srcdev-component-samaritan-prompt.code-snippets +5 -4
  40. package/.vscode/srcdev-component-site-header.code-snippets +13 -1
  41. package/.vscode/srcdev-component-sparkle-effect.code-snippets +33 -0
  42. package/app/components/01.atoms/animations/sparkle-effect/CONSUMER-STYLING.md +75 -0
  43. package/app/components/01.atoms/animations/sparkle-effect/SparkleEffect.vue +213 -0
  44. package/app/components/01.atoms/animations/sparkle-effect/stories/SparkleEffect.stories.ts +303 -0
  45. package/app/components/01.atoms/animations/sparkle-effect/tests/SparkleEffect.spec.ts +150 -0
  46. package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +1 -1
  47. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md +12 -2
  48. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/DashboardQuadGrid.vue +19 -17
  49. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/stories/DashboardQuadGrid.stories.ts +42 -0
  50. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/tests/DashboardQuadGrid.spec.ts +31 -44
  51. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/CONSUMER-STYLING.md +13 -1
  52. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/DashboardStatsGrid.vue +40 -31
  53. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/stories/DashboardStatsGrid.stories.ts +47 -0
  54. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/tests/DashboardStatsGrid.spec.ts +51 -53
  55. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/CONSUMER-STYLING.md +3 -0
  56. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/LayoutGridByCols.vue +4 -0
  57. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/stories/LayoutGridByCols.stories.ts +47 -2
  58. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/LayoutGridByCols.spec.ts +36 -23
  59. package/app/components/01.atoms/grids/auto-grid/AutoGrid.vue +7 -3
  60. package/app/components/01.atoms/grids/auto-grid/CONSUMER-STYLING.md +1 -1
  61. package/app/components/01.atoms/grids/auto-grid/stories/AutoGrid.stories.ts +46 -1
  62. package/app/components/01.atoms/grids/auto-grid/tests/AutoGrid.spec.ts +40 -27
  63. package/app/components/01.atoms/grids/auto-grid/tests/__snapshots__/AutoGrid.spec.ts.snap +1 -1
  64. package/app/components/01.atoms/grids/column-flow-grid/CONSUMER-STYLING.md +9 -0
  65. package/app/components/01.atoms/grids/column-flow-grid/ColumnFlowGrid.vue +3 -2
  66. package/app/components/01.atoms/grids/column-flow-grid/stories/ColumnFlowGrid.stories.ts +46 -0
  67. package/app/components/01.atoms/grids/column-flow-grid/tests/ColumnFlowGrid.spec.ts +24 -11
  68. package/app/components/01.atoms/grids/grid-stack/CONSUMER-STYLING.md +3 -0
  69. package/app/components/01.atoms/grids/grid-stack/GridStack.vue +2 -0
  70. package/app/components/01.atoms/grids/grid-stack/stories/GridStack.stories.ts +41 -0
  71. package/app/components/01.atoms/grids/grid-stack/tests/GridStack.spec.ts +26 -13
  72. package/app/components/01.atoms/grids/masonry-grid/CONSUMER-STYLING.md +15 -0
  73. package/app/components/01.atoms/grids/masonry-grid/MasonryGrid.vue +51 -31
  74. package/app/components/01.atoms/grids/masonry-grid/stories/MasonryGrid.stories.ts +48 -0
  75. package/app/components/01.atoms/grids/masonry-grid/tests/MasonryGrid.spec.ts +84 -29
  76. package/app/components/01.atoms/page-row/CONSUMER-STYLING.md +6 -1
  77. package/app/components/01.atoms/page-row/PageRow.vue +16 -15
  78. package/app/components/01.atoms/page-row/stories/PageRow.stories.ts +23 -0
  79. package/app/components/01.atoms/page-row/tests/PageRow.spec.ts +6 -5
  80. package/app/components/02.molecules/action-menu/ActionMenu.vue +6 -2
  81. package/app/components/02.molecules/action-menu/ActionMenuItem.vue +6 -3
  82. package/app/components/02.molecules/action-menu/CONSUMER-STYLING.md +11 -4
  83. package/app/components/02.molecules/action-menu/stories/ActionMenu.stories.ts +130 -18
  84. package/app/components/02.molecules/expandable/accordion-chat/AccordionChat.vue +227 -0
  85. package/app/components/02.molecules/expandable/accordion-chat/CONSUMER-STYLING.md +150 -0
  86. package/app/components/02.molecules/expandable/accordion-chat/stories/AccordionChat.stories.ts +347 -0
  87. package/app/components/02.molecules/expandable/accordion-chat/tests/AccordionChat.spec.ts +205 -0
  88. package/app/components/02.molecules/expandable/accordion-chat/tests/__snapshots__/AccordionChat.spec.ts.snap +40 -0
  89. package/app/components/02.molecules/expandable/{accordian/AccordianCore.vue → accordion-group/AccordionGroup.vue} +32 -20
  90. package/app/components/02.molecules/expandable/accordion-group/CONSUMER-STYLING.md +66 -0
  91. package/app/components/02.molecules/expandable/accordion-group/stories/AccordionGroup.stories.ts +238 -0
  92. package/app/components/02.molecules/expandable/accordion-group/tests/AccordionGroup.spec.ts +172 -0
  93. package/app/components/02.molecules/expandable/accordion-group/tests/__snapshots__/AccordionGroup.spec.ts.snap +45 -0
  94. package/app/components/02.molecules/expandable/expanding-panel/CONSUMER-STYLING.md +1 -0
  95. package/app/components/02.molecules/expandable/expanding-panel/ExpandingPanel.vue +11 -6
  96. package/app/components/02.molecules/expandable/expanding-panel/stories/ExpandingPanel.stories.ts +82 -1
  97. package/app/components/02.molecules/expandable/expanding-panel/tests/ExpandingPanel.spec.ts +24 -30
  98. package/app/components/02.molecules/expandable/expanding-panel/tests/__snapshots__/ExpandingPanel.spec.ts.snap +8 -8
  99. package/app/components/02.molecules/expandable/expanding-panel-classic/CONSUMER-STYLING.md +1 -0
  100. package/app/components/02.molecules/expandable/expanding-panel-classic/ExpandingPanelClassic.vue +10 -5
  101. package/app/components/02.molecules/expandable/expanding-panel-classic/stories/ExpandingPanelClassic.stories.ts +83 -1
  102. package/app/components/02.molecules/expandable/expanding-panel-classic/tests/ExpandingPanelClassic.spec.ts +24 -30
  103. package/app/components/02.molecules/expandable/expanding-panel-classic/tests/__snapshots__/ExpandingPanelClassic.spec.ts.snap +8 -8
  104. package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +63 -44
  105. package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +74 -37
  106. package/app/components/02.molecules/navigation/deep-expanding-menu/stories/DeepExpandingMenu.stories.ts +139 -19
  107. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/DeepExpandingMenu.spec.ts +90 -27
  108. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +8 -8
  109. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md +58 -26
  110. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue +113 -57
  111. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/stories/DeepExpandingMenuClassic.stories.ts +139 -19
  112. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/DeepExpandingMenuClassic.spec.ts +140 -22
  113. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/__snapshots__/DeepExpandingMenuClassic.spec.ts.snap +9 -9
  114. package/app/components/02.molecules/qr-code/CONSUMER-STYLING.md +22 -7
  115. package/app/components/02.molecules/qr-code/CaptureQrCode.vue +63 -71
  116. package/app/components/02.molecules/qr-code/DecodeQrCode.vue +47 -10
  117. package/app/components/02.molecules/qr-code/stories/CaptureQrCode.stories.ts +114 -26
  118. package/app/components/02.molecules/qr-code/stories/DecodeQrCode.stories.ts +115 -24
  119. package/app/components/02.molecules/qr-code/tests/CaptureQrCode.spec.ts +124 -51
  120. package/app/components/02.molecules/qr-code/tests/DecodeQrCode.spec.ts +74 -25
  121. package/app/components/02.molecules/samaritan-prompt/CONSUMER-STYLING.md +22 -10
  122. package/app/components/02.molecules/samaritan-prompt/SamaritanPrompt.vue +30 -12
  123. package/app/components/02.molecules/samaritan-prompt/SamaritanPromptMixed.vue +34 -10
  124. package/app/components/02.molecules/samaritan-prompt/stories/SamaritanPrompt.stories.ts +55 -4
  125. package/app/components/02.molecules/samaritan-prompt/stories/SamaritanPromptMixed.stories.ts +67 -7
  126. package/app/components/02.molecules/samaritan-prompt/tests/SamaritanPrompt.spec.ts +113 -29
  127. package/app/components/02.molecules/samaritan-prompt/tests/SamaritanPromptMixed.spec.ts +92 -45
  128. package/app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md +82 -29
  129. package/app/components/03.organisms/image-galleries/carousel-flip/CarouselFlip.vue +176 -113
  130. package/app/components/03.organisms/image-galleries/carousel-flip/stories/CarouselFlip.stories.ts +153 -23
  131. package/app/components/03.organisms/image-galleries/carousel-flip/tests/CarouselFlip.spec.ts +119 -63
  132. package/app/components/03.organisms/responsive-header/tests/__snapshots__/NavigationItems.spec.ts.snap +2 -2
  133. package/app/components/03.organisms/responsive-header/tests/__snapshots__/ResponsiveHeader.spec.ts.snap +2 -2
  134. package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +8 -8
  135. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +4 -4
  136. package/app/components/03.organisms/site-header/CONSUMER-STYLING.md +41 -23
  137. package/app/components/03.organisms/site-header/SiteHeader.vue +16 -21
  138. package/app/components/03.organisms/site-header/stories/SiteHeader.stories.ts +138 -0
  139. package/app/components/03.organisms/site-header/tests/SiteHeader.spec.ts +53 -26
  140. package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +2 -2
  141. package/app/types/components/qr-code.d.ts +12 -0
  142. package/app/utils/graphemes.ts +8 -0
  143. package/package.json +1 -1
  144. package/.claude/skills/components/accordian-core.md +0 -180
  145. package/.vscode/srcdev-component-accordian-core.code-snippets +0 -74
  146. package/app/components/01.atoms/animations/entry/.nuxt/app.config.mjs +0 -21
  147. package/app/components/01.atoms/animations/entry/.nuxt/components.d.ts +0 -66
  148. package/app/components/01.atoms/animations/entry/.nuxt/imports.d.ts +0 -30
  149. package/app/components/01.atoms/animations/entry/.nuxt/manifest/meta/test.json +0 -1
  150. package/app/components/01.atoms/animations/entry/.nuxt/types/app.config.d.ts +0 -35
  151. package/app/components/01.atoms/animations/entry/.nuxt/types/build.d.ts +0 -23
  152. package/app/components/01.atoms/animations/entry/.nuxt/types/builder-env.d.ts +0 -1
  153. package/app/components/01.atoms/animations/entry/.nuxt/types/components.d.ts +0 -71
  154. package/app/components/01.atoms/animations/entry/.nuxt/types/imports.d.ts +0 -386
  155. package/app/components/01.atoms/animations/entry/.nuxt/types/middleware.d.ts +0 -11
  156. package/app/components/01.atoms/animations/entry/.nuxt/types/modules.d.ts +0 -79
  157. package/app/components/01.atoms/animations/entry/.nuxt/types/nitro-layouts.d.ts +0 -17
  158. package/app/components/01.atoms/animations/entry/.nuxt/types/nitro-nuxt.d.ts +0 -64
  159. package/app/components/01.atoms/animations/entry/.nuxt/types/plugins.d.ts +0 -29
  160. package/app/components/01.atoms/animations/entry/.nuxt/types/runtime-config.d.ts +0 -32
  161. package/app/components/01.atoms/animations/entry/.nuxt/types/shared-imports.d.ts +0 -10
  162. package/app/components/01.atoms/animations/entry/.nuxt/types/vue-shim.d.ts +0 -0
  163. package/app/components/01.atoms/animations/entry/.nuxtrc +0 -1
  164. package/app/components/01.atoms/animations/entry/node_modules/.cache/nuxt/chrome-workspace.json +0 -1
  165. package/app/components/01.atoms/animations/entry/node_modules/.cache/vite/client/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  166. package/app/components/02.molecules/expandable/accordian/CONSUMER-STYLING.md +0 -100
  167. package/app/components/02.molecules/expandable/accordian/stories/AccordianCore.stories.ts +0 -272
  168. package/app/components/02.molecules/expandable/accordian/tests/AccordianCore.spec.ts +0 -257
  169. package/app/components/02.molecules/expandable/accordian/tests/__snapshots__/AccordianCore.spec.ts.snap +0 -95
  170. package/app/components/03.organisms/responsive-header/tests/.nuxt/app.config.mjs +0 -21
  171. package/app/components/03.organisms/responsive-header/tests/.nuxt/components.d.ts +0 -66
  172. package/app/components/03.organisms/responsive-header/tests/.nuxt/imports.d.ts +0 -30
  173. package/app/components/03.organisms/responsive-header/tests/.nuxt/manifest/meta/test.json +0 -1
  174. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/app.config.d.ts +0 -35
  175. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/build.d.ts +0 -23
  176. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/builder-env.d.ts +0 -1
  177. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/components.d.ts +0 -71
  178. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/imports.d.ts +0 -386
  179. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/middleware.d.ts +0 -11
  180. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/modules.d.ts +0 -79
  181. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-layouts.d.ts +0 -17
  182. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-nuxt.d.ts +0 -64
  183. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/plugins.d.ts +0 -29
  184. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/runtime-config.d.ts +0 -32
  185. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/shared-imports.d.ts +0 -10
  186. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/vue-shim.d.ts +0 -0
  187. package/app/components/03.organisms/responsive-header/tests/node_modules/.cache/nuxt/chrome-workspace.json +0 -1
@@ -1,13 +1,26 @@
1
1
  import ExpandingPanelClassic from "../ExpandingPanelClassic.vue";
2
2
  import type { Meta, StoryObj } from "@nuxtjs/storybook";
3
+ import { ref } from "vue";
4
+ import CanvasSwitcher from "../../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
5
+ import type { MediaCanvas } from "~/types/components";
3
6
 
4
7
  const meta: Meta<typeof ExpandingPanelClassic> = {
5
8
  title: "Molecules/Expandable/Expanding Panel Classic",
6
9
  component: ExpandingPanelClassic,
10
+ parameters: {
11
+ docs: {
12
+ description: {
13
+ component:
14
+ "A single expand/collapse panel on native <details>/<summary>. To render a set of these from slots, optionally exclusive-open, use " +
15
+ "[AccordionGroup](?path=/story/molecules-expandable-accordiongroup--exclusive-open), which renders this panel for every item when `variant=\"classic\"`. " +
16
+ "[AccordionChat](?path=/story/molecules-expandable-accordionchat--default) restyles it as a chat: the summary becomes a question bubble and the content an answer bubble.",
17
+ },
18
+ },
19
+ },
7
20
  argTypes: {
8
21
  name: {
9
22
  control: { type: "text" },
10
- description: "Unique name used for ARIA attributes and to group panels in a native <details> accordion",
23
+ description: "The <details> name: panels sharing it form an exclusive-open group. Element ids come from useId(), not from this.",
11
24
  },
12
25
  animationDuration: {
13
26
  control: { type: "number", min: 0, step: 50 },
@@ -291,3 +304,72 @@ export const WithStyleClassPassthrough: Story = {
291
304
  `,
292
305
  }),
293
306
  };
307
+
308
+ export const StressTest: Story = {
309
+ name: "Stress Test (Worst-Case Data)",
310
+ render: (args) => ({
311
+ components: { ExpandingPanelClassic },
312
+ setup() {
313
+ const longUnbroken = "Donaudampfschifffahrtselektrizitätenhauptbetriebswerkbauunterbeamtengesellschaft";
314
+ const longUrl = "https://example.com/" + "a-very-long-path-segment-without-spaces".repeat(4);
315
+ const longCopy = "Unsere Behandlungen werden individuell auf Ihre Bedürfnisse abgestimmt, damit Sie sich rundum wohlfühlen. ".repeat(6);
316
+ return { args, longUnbroken, longUrl, longCopy };
317
+ },
318
+ template: `
319
+ <div style="padding: 1.6rem; display: grid; gap: 1.6rem;">
320
+ <ExpandingPanelClassic v-bind="args" name="stress-a">
321
+ <template #summary><span>{{ longUnbroken }}</span></template>
322
+ <template #content><p style="margin: 0; padding-block: 0.8rem;">{{ longUrl }}</p></template>
323
+ </ExpandingPanelClassic>
324
+ <ExpandingPanelClassic v-bind="args" name="stress-a">
325
+ <template #summary><span>{{ longCopy.slice(0, 220) }}</span></template>
326
+ <template #content><p style="margin: 0; padding-block: 0.8rem;">{{ longCopy }}</p></template>
327
+ </ExpandingPanelClassic>
328
+ <ExpandingPanelClassic v-bind="args">
329
+ <template #summary><span>🎉 Emoji first, then &lt;b&gt;html&lt;/b&gt; as text</span></template>
330
+ <template #content><p style="margin: 0; padding-block: 0.8rem;">&lt;script&gt;alert(1)&lt;/script&gt;</p></template>
331
+ </ExpandingPanelClassic>
332
+ <ExpandingPanelClassic v-bind="args" dir="rtl">
333
+ <template #summary><span>سؤال طويل من اليمين إلى اليسار للتحقق من الاتجاه</span></template>
334
+ <template #content><p style="margin: 0; padding-block: 0.8rem;">إجابة قصيرة.</p></template>
335
+ </ExpandingPanelClassic>
336
+ <ExpandingPanelClassic v-bind="args">
337
+ <template #summary><span>A</span></template>
338
+ </ExpandingPanelClassic>
339
+ <ExpandingPanelClassic v-bind="args">
340
+ <template #content><p style="margin: 0; padding-block: 0.8rem;">No summary slot: only the icon is clickable.</p></template>
341
+ </ExpandingPanelClassic>
342
+ </div>
343
+ `,
344
+ }),
345
+ decorators: [
346
+ (story, context) => ({
347
+ components: { story, CanvasSwitcher },
348
+ setup() {
349
+ const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "mobileCanvas");
350
+ return { canvasName };
351
+ },
352
+ template: `
353
+ <div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
354
+ <CanvasSwitcher v-model:canvas-name="canvasName" />
355
+ </div>
356
+ <div :class="canvasName" style="margin-inline: auto; outline: 1px dashed currentColor;">
357
+ <story />
358
+ </div>
359
+ `,
360
+ }),
361
+ ],
362
+ parameters: {
363
+ initialCanvas: "mobileCanvas",
364
+ docs: {
365
+ story: { inline: false, height: "640px" },
366
+ description: {
367
+ story:
368
+ "Open each panel at every canvas width. The long unbroken summary wraps and the caret stays visible (until 2026-10-20 it pushed the " +
369
+ "caret out of the clipped summary); long URLs and copy wrap in the content; the first two panels share a name, so opening one closes " +
370
+ "the other, and each keeps its own ids; emoji and HTML-like text render as text; the RTL panel puts the caret on the left; the " +
371
+ "single-letter, no-content and no-summary panels keep their shape.",
372
+ },
373
+ },
374
+ },
375
+ };
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect } from "vitest";
2
- import { nextTick } from "vue";
2
+ import { nextTick, defineComponent } from "vue";
3
3
  import { mountSuspended } from "@nuxt/test-utils/runtime";
4
4
  import type { ComponentPublicInstance } from "vue";
5
5
  import ExpandingPanelClassic from "../ExpandingPanelClassic.vue";
@@ -156,18 +156,11 @@ describe("ExpandingPanelClassic", () => {
156
156
 
157
157
  // ─── ARIA & IDs ───────────────────────────────────────────────────────────
158
158
 
159
- it("sets id on summary to id-{name}-trigger", async () => {
159
+ it("points the summary's aria-controls at the content region", async () => {
160
160
  const wrapper = await mountSuspended(ExpandingPanelClassic, {
161
161
  props: { name: "aria-panel" },
162
162
  });
163
- expect(wrapper.find("summary").attributes("id")).toBe("id-aria-panel-trigger");
164
- });
165
-
166
- it("sets aria-controls on summary to id-{name}-content", async () => {
167
- const wrapper = await mountSuspended(ExpandingPanelClassic, {
168
- props: { name: "aria-panel" },
169
- });
170
- expect(wrapper.find("summary").attributes("aria-controls")).toBe("id-aria-panel-content");
163
+ expect(wrapper.find("summary").attributes("aria-controls")).toBe(wrapper.find(".expanding-panel-classic-content").attributes("id"));
171
164
  });
172
165
 
173
166
  it("sets aria-expanded to false when closed", async () => {
@@ -185,20 +178,11 @@ describe("ExpandingPanelClassic", () => {
185
178
  expect(wrapper.find("summary").attributes("aria-expanded")).toBe("true");
186
179
  });
187
180
 
188
- it("sets id on content region to id-{name}-content", async () => {
181
+ it("labels the content region by its summary", async () => {
189
182
  const wrapper = await mountSuspended(ExpandingPanelClassic, {
190
183
  props: { name: "aria-panel" },
191
184
  });
192
- expect(wrapper.find(".expanding-panel-classic-content").attributes("id")).toBe("id-aria-panel-content");
193
- });
194
-
195
- it("sets aria-labelledby on content region to id-{name}-trigger", async () => {
196
- const wrapper = await mountSuspended(ExpandingPanelClassic, {
197
- props: { name: "aria-panel" },
198
- });
199
- expect(wrapper.find(".expanding-panel-classic-content").attributes("aria-labelledby")).toBe(
200
- "id-aria-panel-trigger",
201
- );
185
+ expect(wrapper.find(".expanding-panel-classic-content").attributes("aria-labelledby")).toBe(wrapper.find("summary").attributes("id"));
202
186
  });
203
187
 
204
188
  it("sets role='region' on the content div", async () => {
@@ -482,33 +466,43 @@ describe("ExpandingPanelClassic", () => {
482
466
 
483
467
  // ─── name prop / useId fallback ───────────────────────────────────────────
484
468
 
485
- it("uses the provided name in ARIA attributes", async () => {
469
+ it("builds ids from useId, not from the name", async () => {
486
470
  const wrapper = await mountSuspended(ExpandingPanelClassic, {
487
471
  props: { name: "my-panel" },
488
472
  });
489
- expect(wrapper.find("summary").attributes("id")).toBe("id-my-panel-trigger");
490
- expect(wrapper.find(".expanding-panel-classic-content").attributes("id")).toBe("id-my-panel-content");
473
+ expect(wrapper.find("summary").attributes("id")).not.toContain("my-panel");
474
+ expect(wrapper.find(".expanding-panel-classic-content").attributes("id")).not.toContain("my-panel");
491
475
  });
492
476
 
493
477
  it("generates ARIA ids when no name prop is supplied", async () => {
494
478
  const wrapper = await mountSuspended(ExpandingPanelClassic);
495
479
  const summaryId = wrapper.find("summary").attributes("id") ?? "";
496
480
  const contentId = wrapper.find(".expanding-panel-classic-content").attributes("id") ?? "";
497
- expect(summaryId).toMatch(/^id-.+-trigger$/);
498
- expect(contentId).toMatch(/^id-.+-content$/);
481
+ expect(summaryId).toMatch(/^.+-trigger$/);
482
+ expect(contentId).toMatch(/^.+-content$/);
499
483
  });
500
484
 
501
- it("updates the details name and ARIA ids when the name prop changes after mount", async () => {
485
+ it("updates the details name when the name prop changes after mount, keeping its ids", async () => {
502
486
  const wrapper = await mountSuspended(ExpandingPanelClassic, {
503
487
  props: { name: "panel-a" },
504
488
  });
489
+ const summaryId = wrapper.find("summary").attributes("id");
505
490
  expect(wrapper.find("details").attributes("name")).toBe("panel-a");
506
- expect(wrapper.find("summary").attributes("id")).toBe("id-panel-a-trigger");
507
491
 
508
492
  await wrapper.setProps({ name: "panel-b" });
509
493
 
510
494
  expect(wrapper.find("details").attributes("name")).toBe("panel-b");
511
- expect(wrapper.find("summary").attributes("id")).toBe("id-panel-b-trigger");
512
- expect(wrapper.find(".expanding-panel-classic-content").attributes("id")).toBe("id-panel-b-content");
495
+ expect(wrapper.find("summary").attributes("id")).toBe(summaryId);
496
+ });
497
+
498
+ it("gives panels that share a name distinct ids", async () => {
499
+ const Host = defineComponent({
500
+ components: { ExpandingPanelClassic },
501
+ template: `<div><ExpandingPanelClassic name="shared"></ExpandingPanelClassic><ExpandingPanelClassic name="shared"></ExpandingPanelClassic></div>`,
502
+ });
503
+ const wrapper = await mountSuspended(Host);
504
+ const ids = wrapper.findAll("[id]").map((el) => el.attributes("id"));
505
+ expect(ids).toHaveLength(4);
506
+ expect(new Set(ids).size).toBe(4);
513
507
  });
514
508
  });
@@ -3,12 +3,12 @@
3
3
  exports[`ExpandingPanelClassic > renders correct HTML structure with all props set 1`] = `
4
4
  "<div class="expanding-panel-classic extra-class">
5
5
  <details class="expanding-panel-classic-details" name="snap-all" open="">
6
- <summary id="id-snap-all-trigger" class="expanding-panel-classic-summary" aria-controls="id-snap-all-content" aria-expanded="true">
6
+ <summary id="v-0-1-trigger" class="expanding-panel-classic-summary" aria-controls="v-0-1-content" aria-expanded="true">
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
10
  </details>
11
- <div id="id-snap-all-content" class="expanding-panel-classic-content" aria-labelledby="id-snap-all-trigger" role="region">
11
+ <div id="v-0-1-content" class="expanding-panel-classic-content" aria-labelledby="v-0-1-trigger" role="region">
12
12
  <div class="inner"></div>
13
13
  </div>
14
14
  </div>"
@@ -17,12 +17,12 @@ exports[`ExpandingPanelClassic > renders correct HTML structure with all props s
17
17
  exports[`ExpandingPanelClassic > renders correct HTML structure with contentIsOnTop true 1`] = `
18
18
  "<div class="expanding-panel-classic content-is-on-top">
19
19
  <details class="expanding-panel-classic-details" name="snap-content-on-top">
20
- <summary id="id-snap-content-on-top-trigger" class="expanding-panel-classic-summary" aria-controls="id-snap-content-on-top-content" aria-expanded="false">
20
+ <summary id="v-0-1-trigger" class="expanding-panel-classic-summary" aria-controls="v-0-1-content" aria-expanded="false">
21
21
  <div class="label-wrapper"></div>
22
22
  <div class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon" aria-hidden="true"></span></div>
23
23
  </summary>
24
24
  </details>
25
- <div id="id-snap-content-on-top-content" class="expanding-panel-classic-content" aria-labelledby="id-snap-content-on-top-trigger" role="region">
25
+ <div id="v-0-1-content" class="expanding-panel-classic-content" aria-labelledby="v-0-1-trigger" role="region">
26
26
  <div class="inner"></div>
27
27
  </div>
28
28
  </div>"
@@ -31,12 +31,12 @@ exports[`ExpandingPanelClassic > renders correct HTML structure with contentIsOn
31
31
  exports[`ExpandingPanelClassic > renders correct HTML structure with default props 1`] = `
32
32
  "<div class="expanding-panel-classic">
33
33
  <details class="expanding-panel-classic-details" name="snap-default">
34
- <summary id="id-snap-default-trigger" class="expanding-panel-classic-summary" aria-controls="id-snap-default-content" aria-expanded="false">
34
+ <summary id="v-0-1-trigger" class="expanding-panel-classic-summary" aria-controls="v-0-1-content" aria-expanded="false">
35
35
  <div class="label-wrapper"></div>
36
36
  <div class="icon-wrapper"><span class="iconify i-bi:caret-down-fill icon" aria-hidden="true"></span></div>
37
37
  </summary>
38
38
  </details>
39
- <div id="id-snap-default-content" class="expanding-panel-classic-content" aria-labelledby="id-snap-default-trigger" role="region">
39
+ <div id="v-0-1-content" class="expanding-panel-classic-content" aria-labelledby="v-0-1-trigger" role="region">
40
40
  <div class="inner"></div>
41
41
  </div>
42
42
  </div>"
@@ -45,12 +45,12 @@ exports[`ExpandingPanelClassic > renders correct HTML structure with default pro
45
45
  exports[`ExpandingPanelClassic > renders correct HTML structure with populated slots 1`] = `
46
46
  "<div class="expanding-panel-classic">
47
47
  <details class="expanding-panel-classic-details" name="snap-slots">
48
- <summary id="id-snap-slots-trigger" class="expanding-panel-classic-summary" aria-controls="id-snap-slots-content" aria-expanded="false">
48
+ <summary id="v-0-1-trigger" class="expanding-panel-classic-summary" aria-controls="v-0-1-content" aria-expanded="false">
49
49
  <div class="label-wrapper"><span>Summary text</span></div>
50
50
  <div class="icon-wrapper"><span>▸</span></div>
51
51
  </summary>
52
52
  </details>
53
- <div id="id-snap-slots-content" class="expanding-panel-classic-content" aria-labelledby="id-snap-slots-trigger" role="region">
53
+ <div id="v-0-1-content" class="expanding-panel-classic-content" aria-labelledby="v-0-1-trigger" role="region">
54
54
  <div class="inner">
55
55
  <p>Content text</p>
56
56
  </div>
@@ -2,65 +2,76 @@
2
2
 
3
3
  ## Public token API
4
4
 
5
+ > Changed 2026-10-21: colour defaults now come from `--theme-*` tokens (were `--blue-10`, `white`,
6
+ > `black`, `--slate-10`), and the structural classes are prefixed (see State hooks).
7
+
8
+ ### Top-level row
9
+
5
10
  | Token | Default | Controls |
6
11
  |---|---|---|
7
- | `--deep-expanding-menu-gap` | `2.4rem` | Gap between top-level items |
8
- | `--deep-expanding-menu-link-border-width` | `0.2rem` | Bottom border width on links and group toggles |
12
+ | `--deep-expanding-menu-gap` | `2.4rem` | Gap between top-level items (row and column, the row wraps) |
13
+ | `--deep-expanding-menu-link-border-width` | `0.2rem` | Bottom border width on links, group toggles and child links |
9
14
  | `--deep-expanding-menu-link-padding-block` | `0.8rem` | Vertical padding on links and group toggles |
10
- | `--deep-expanding-menu-link-border-colour-hover` | `var(--blue-10)` | Bottom border colour on hover/focus |
15
+ | `--deep-expanding-menu-link-border-colour-hover` | `var(--theme-border)` | Bottom border colour on hover/focus |
16
+ | `--deep-expanding-menu-focus-outline-width` | `0.2rem` | Keyboard focus ring width on links and toggles |
17
+ | `--deep-expanding-menu-focus-outline-colour` | `var(--theme-ring, currentcolor)` | Keyboard focus ring colour |
11
18
  | `--deep-expanding-menu-icon-size` | `1.2rem` | Caret icon size |
12
- | `--deep-expanding-menu-panel-width` | `min(100%, 50vw)` | Popover panel width |
13
- | `--deep-expanding-menu-panel-background-colour` | `white` | Popover panel background |
14
- | `--deep-expanding-menu-panel-border-width` | `0.1rem` | Popover panel border width |
15
- | `--deep-expanding-menu-panel-border-colour` | `black` | Popover panel border colour |
16
- | `--deep-expanding-menu-panel-border-radius` | `1.2rem` | Popover panel corner radius |
17
- | `--deep-expanding-menu-panel-shadow` | `0 0 1rem rgba(0, 0, 0, 0.1)` | Popover panel box-shadow |
18
- | `--deep-expanding-menu-panel-padding` | `1.2rem` | Popover panel padding |
19
- | `--deep-expanding-menu-panel-heading-colour` | `var(--slate-10)` | `childLinksTitle` heading colour inside the panel |
20
- | `--deep-expanding-menu-panel-list-gap` | `1.2rem` | Gap between child link grid items |
21
- | `--deep-expanding-menu-group-link-colour` | `var(--slate-10)` | Child link text colour |
22
- | `--deep-expanding-menu-group-link-border-colour-hover` | `var(--slate-10)` | Child link bottom border colour on hover/focus |
23
- | `--deep-expanding-menu-panel-z-index` | `999999` | Panel stacking order in browsers without CSS anchor positioning. Ignored where the panel renders in the top layer |
24
19
 
25
- ```css
26
- .my-page {
27
- --deep-expanding-menu-panel-background-colour: #1a1a1a;
28
- --deep-expanding-menu-panel-border-colour: transparent;
29
- --deep-expanding-menu-group-link-colour: white;
30
- }
31
- ```
20
+ ### Panel
32
21
 
33
- Or scope to a single instance via `styleClassPassthrough`:
34
-
35
- ```vue
36
- <DeepExpandingMenu style-class-passthrough="main-nav">...</DeepExpandingMenu>
37
- ```
38
-
39
- ---
40
-
41
- ## Not tokenised
22
+ | Token | Default | Controls |
23
+ |---|---|---|
24
+ | `--deep-expanding-menu-panel-width` | `min(100%, 50vw)` | Panel width |
25
+ | `--deep-expanding-menu-panel-max-height` | `70dvh` | Panel max height; longer lists scroll inside it |
26
+ | `--deep-expanding-menu-panel-text-colour` | `var(--theme-text)` | Panel text colour |
27
+ | `--deep-expanding-menu-panel-background-colour` | `var(--theme-surface-subtle)` | Panel background |
28
+ | `--deep-expanding-menu-panel-border-width` | `0.1rem` | Panel border width |
29
+ | `--deep-expanding-menu-panel-border-colour` | `var(--theme-border)` | Panel border colour |
30
+ | `--deep-expanding-menu-panel-border-radius` | `1.2rem` | Panel corner radius |
31
+ | `--deep-expanding-menu-panel-shadow` | `0 0 1rem rgba(0, 0, 0, 0.1)` | Panel box-shadow |
32
+ | `--deep-expanding-menu-panel-padding` | `1.2rem` | Panel padding |
33
+ | `--deep-expanding-menu-panel-heading-colour` | `var(--theme-text)` | `childLinksTitle` heading colour |
34
+ | `--deep-expanding-menu-panel-heading-line-clamp` | `none` | Lines of `childLinksTitle` before an ellipsis. `1` is single-line ellipsis, `none` shows everything |
35
+ | `--deep-expanding-menu-panel-list-gap` | `1.2rem` | Gap between child link grid items |
36
+ | `--deep-expanding-menu-group-link-colour` | `var(--theme-text)` | Child link text colour |
37
+ | `--deep-expanding-menu-group-link-border-colour-hover` | `var(--theme-text)` | Child link bottom border colour on hover/focus |
38
+ | `--deep-expanding-menu-panel-z-index` | `999999` | Panel stacking order in browsers without CSS anchor positioning. Ignored where the panel renders in the top layer |
42
39
 
43
- - The `1rem` gap between a toggle and its panel.
44
- - Anchor names and popover target ids are generated per-instance via `useId()` and are not
45
- consumer-configurable — they only need to be unique, not meaningful.
40
+ Private tokens (not public API): `--_anchor-name` (per-group anchor, set inline),
41
+ `--_icon-transform` (caret flip), and `--_anchor-bottom`, `--_anchor-left`,
42
+ `--_anchor-top-inverse` (written by `useAnchoredPopover` in the fallback).
46
43
 
47
- ---
44
+ Not tokenised: the `1rem` gap between a toggle and its panel, and the `12rem` minimum child link
45
+ column. Anchor names and popover target ids come from `useId()`.
48
46
 
49
- ## Open state and older browsers
47
+ ## State hooks
50
48
 
51
49
  | Hook | Meaning |
52
50
  |---|---|
53
- | `.navigation-group-toggle[aria-expanded="true"]` | Group open, every browser (flips the caret) |
54
- | `.navigation-group-panel:popover-open` | Panel open, Popover API browsers |
51
+ | `.deep-expanding-menu-group-toggle[aria-expanded="true"]` | Group open, every browser (flips the caret) |
52
+ | `.deep-expanding-menu-group-panel:popover-open` | Panel open, Popover API browsers |
55
53
  | `.deep-expanding-menu-panel-open` | Panel open, browsers without the Popover API (Safari 16 and older). Style it in a **separate rule** from `:popover-open`: a selector list containing `:popover-open` is dropped whole where it's unsupported |
56
- | `.navigation-group-panel[data-placement="top"]` | Flipped above the toggle, browsers without CSS anchor positioning only |
54
+ | `.deep-expanding-menu-group-panel[data-placement="top"]` | Flipped above the toggle, browsers without CSS anchor positioning only |
55
+
56
+ Inner elements: `.deep-expanding-menu-inner` (the flex row), `.deep-expanding-menu-link` (direct
57
+ link), `.deep-expanding-menu-group`, `.deep-expanding-menu-group-toggle`,
58
+ `.deep-expanding-menu-group-label`, `.deep-expanding-menu-group-icon`,
59
+ `.deep-expanding-menu-group-panel`, `.deep-expanding-menu-group-heading`,
60
+ `.deep-expanding-menu-group-list`, `.deep-expanding-menu-group-item`,
61
+ `.deep-expanding-menu-group-link`.
57
62
 
58
- Without CSS anchor positioning (e.g. Safari 17–18) the panel is placed with `position: fixed` from
59
- the toggle's measured position. Without the Popover API (Safari 16) the panel also opens and closes
60
- in JS and isn't in the top layer, so `--deep-expanding-menu-panel-z-index` applies, and an ancestor
61
- with `transform`, `filter` or `contain` becomes its containing block and can misplace it.
63
+ > Changed 2026-10-21: these were `.inner`, `.navigation-link`, `.navigation-group`,
64
+ > `.navigation-group-toggle`, `.icon`, `.navigation-group-panel`, `.navigation-group-list`,
65
+ > `.navigation-group-item` and `.navigation-group-link`. The heading had no class of its own.
62
66
 
63
- ---
67
+ ## Browser support and placement
68
+
69
+ Where CSS anchor positioning is supported, a panel that would overflow the right edge flips to
70
+ align with its toggle's right edge (`position-try-fallbacks: flip-inline`). Without anchor
71
+ positioning (e.g. Safari 17–18) the panel is placed with `position: fixed` from the toggle's
72
+ measured position. Without the Popover API (Safari 16) the panel also opens and closes in JS and
73
+ isn't in the top layer, so `--deep-expanding-menu-panel-z-index` applies, and an ancestor with
74
+ `transform`, `filter` or `contain` becomes its containing block and can misplace it.
64
75
 
65
76
  ## Local overrides
66
77
 
@@ -69,3 +80,11 @@ Set the tokens above on an element you own (a page or section class, or a class
69
80
  `@layer` wrapper) so it beats the library's `@layer components`. If your own file uses
70
81
  `<style scoped>`, tokens set on your element still work, but selectors that reach inside the component need
71
82
  `:deep()`. Patterns and examples: `.claude/skills/component-local-style-override.md`.
83
+
84
+ **Caveat:** in browsers with the Popover API the panel renders in the top layer but stays a DOM
85
+ descendant of the menu, so tokens set on an ancestor still reach it.
86
+
87
+ ## Class passthrough
88
+
89
+ `style-class-passthrough` adds classes to the root element (the `<nav>` by default). It's the
90
+ usual place to scope token overrides to one instance.
@@ -1,26 +1,26 @@
1
1
  <template>
2
- <component :is="tag" ref="rootRef" class="deep-expanding-menu" :class="[elementClasses]">
3
- <div class="inner">
2
+ <component :is="tag" ref="rootRef" class="deep-expanding-menu" :class="[elementClasses]" :aria-label="ariaLabel">
3
+ <div class="deep-expanding-menu-inner">
4
4
  <template v-for="(link, key) in navLinks" :key="key">
5
- <NuxtLink v-if="link.path" :to="link.path" class="navigation-link">{{ link.name }}</NuxtLink>
5
+ <NuxtLink v-if="link.path" :to="link.path" class="deep-expanding-menu-link">{{ link.name }}</NuxtLink>
6
6
 
7
- <div v-else class="navigation-group" :style="`--_anchor-name: --anchor-${uid}-${key};`">
7
+ <div v-else class="deep-expanding-menu-group" :style="`--_anchor-name: --anchor-${uid}-${key};`">
8
8
  <button
9
9
  :ref="(el) => setElement(triggerEls, key, el)"
10
10
  type="button"
11
11
  :popovertarget="`popovertarget-${uid}-${key}`"
12
- class="navigation-group-toggle"
12
+ class="deep-expanding-menu-group-toggle"
13
13
  :aria-expanded="isOpen && activeKey === key"
14
14
  @click="handleGroupClick(key)"
15
15
  >
16
- <span>{{ link.name }}</span>
17
- <Icon name="bi:caret-down-fill" class="icon" />
16
+ <span class="deep-expanding-menu-group-label">{{ link.name }}</span>
17
+ <Icon name="bi:caret-down-fill" class="deep-expanding-menu-group-icon" aria-hidden="true" />
18
18
  </button>
19
19
 
20
20
  <div
21
21
  :id="`popovertarget-${uid}-${key}`"
22
22
  :ref="(el) => setElement(panelEls, key, el)"
23
- class="navigation-group-panel"
23
+ class="deep-expanding-menu-group-panel"
24
24
  :class="{ 'deep-expanding-menu-panel-open': usesFallbackPopover && isOpen && activeKey === key }"
25
25
  :style="activeKey === key ? positionStyle : undefined"
26
26
  :data-placement="activeKey === key ? popoverPlacement : undefined"
@@ -28,10 +28,16 @@
28
28
  @beforetoggle="handlePanelBeforeToggle(key, $event)"
29
29
  @toggle="handlePanelToggle(key, $event)"
30
30
  >
31
- <h4 class="page-heading-4 mb-6">{{ link.childLinksTitle }}</h4>
32
- <ul class="navigation-group-list">
33
- <li v-for="childLink in link.childLinks" :key="childLink.name" class="navigation-group-item">
34
- <NuxtLink :to="childLink.path" class="navigation-group-link">{{ childLink.name }}</NuxtLink>
31
+ <h4 v-if="link.childLinksTitle" class="deep-expanding-menu-group-heading page-heading-4 mb-6">
32
+ {{ link.childLinksTitle }}
33
+ </h4>
34
+ <ul v-if="link.childLinks?.length" class="deep-expanding-menu-group-list" role="list">
35
+ <li
36
+ v-for="(childLink, childIndex) in link.childLinks"
37
+ :key="`${childIndex}-${childLink.name}`"
38
+ class="deep-expanding-menu-group-item"
39
+ >
40
+ <NuxtLink :to="childLink.path" class="deep-expanding-menu-group-link">{{ childLink.name }}</NuxtLink>
35
41
  </li>
36
42
  </ul>
37
43
  </div>
@@ -41,19 +47,22 @@
41
47
  </component>
42
48
  </template>
43
49
 
50
+
44
51
  <script setup lang="ts">
45
52
  import type { ComponentPublicInstance } from "vue";
46
53
  import type { ResponsiveHeaderNavItem } from "~/types/components";
47
54
 
48
55
  interface Props {
49
- tag?: "div" | "section" | "nav" | "ul" | "ol";
56
+ tag?: "div" | "section" | "nav";
50
57
  navLinks?: ResponsiveHeaderNavItem[];
58
+ ariaLabel?: string;
51
59
  styleClassPassthrough?: string | string[];
52
60
  }
53
61
 
54
62
  const props = withDefaults(defineProps<Props>(), {
55
63
  tag: "nav",
56
64
  navLinks: () => [],
65
+ ariaLabel: undefined,
57
66
  styleClassPassthrough: () => [],
58
67
  });
59
68
 
@@ -126,15 +135,19 @@ watch(
126
135
  display: grid;
127
136
  grid-template-areas: "element-stack";
128
137
 
129
- .inner {
138
+ .deep-expanding-menu-inner {
130
139
  grid-area: element-stack;
131
140
  display: flex;
141
+ flex-wrap: wrap;
132
142
  gap: var(--deep-expanding-menu-gap, 2.4rem);
133
143
  align-items: center;
144
+ min-inline-size: 0;
134
145
 
135
- .navigation-link,
136
- .navigation-group-toggle {
146
+ .deep-expanding-menu-link,
147
+ .deep-expanding-menu-group-toggle {
137
148
  all: unset;
149
+ min-inline-size: 0;
150
+ overflow-wrap: anywhere;
138
151
  border-bottom: var(--deep-expanding-menu-link-border-width, 0.2rem) solid transparent;
139
152
  padding-block: var(--deep-expanding-menu-link-padding-block, 0.8rem);
140
153
 
@@ -144,26 +157,35 @@ watch(
144
157
  &:focus,
145
158
  &:focus-visible {
146
159
  cursor: pointer;
147
- border-color: var(
148
- --deep-expanding-menu-link-border-colour-hover,
149
- var(--blue-10)
150
- );
160
+ border-color: var(--deep-expanding-menu-link-border-colour-hover, var(--theme-border));
161
+ }
162
+
163
+ &:focus-visible {
164
+ outline: var(--deep-expanding-menu-focus-outline-width, 0.2rem) solid
165
+ var(--deep-expanding-menu-focus-outline-colour, var(--theme-ring, currentcolor));
166
+ outline-offset: 0.2rem;
151
167
  }
152
168
  }
153
169
 
154
- .navigation-group {
170
+ .deep-expanding-menu-group {
155
171
  --_icon-transform: scaleY(1);
156
172
  position: relative;
173
+ min-inline-size: 0;
157
174
 
158
- .navigation-group-toggle {
175
+ .deep-expanding-menu-group-toggle {
159
176
  anchor-name: var(--_anchor-name);
160
177
 
161
178
  display: flex;
162
179
  align-items: center;
163
180
  gap: 1.2rem;
164
181
 
165
- .icon {
182
+ .deep-expanding-menu-group-label {
183
+ min-inline-size: 0;
184
+ }
185
+
186
+ .deep-expanding-menu-group-icon {
166
187
  display: block;
188
+ flex-shrink: 0;
167
189
  font-size: var(--deep-expanding-menu-icon-size, 1.2rem);
168
190
 
169
191
  transform: var(--_icon-transform);
@@ -171,10 +193,11 @@ watch(
171
193
  }
172
194
  }
173
195
 
174
- .navigation-group-panel {
196
+ .deep-expanding-menu-group-panel {
175
197
  display: none;
176
198
  position: absolute;
177
199
  position-anchor: var(--_anchor-name);
200
+ position-try-fallbacks: flip-inline;
178
201
  margin: 0;
179
202
  top: calc(anchor(bottom) + 1rem);
180
203
  left: calc(anchor(left) + 0rem);
@@ -187,14 +210,17 @@ watch(
187
210
  transition-behavior: allow-discrete;
188
211
 
189
212
  width: var(--deep-expanding-menu-panel-width, min(100%, 50vw));
213
+ max-block-size: var(--deep-expanding-menu-panel-max-height, 70dvh);
190
214
 
191
- background-color: var(--deep-expanding-menu-panel-background-colour, white);
215
+ color: var(--deep-expanding-menu-panel-text-colour, var(--theme-text));
216
+ background-color: var(--deep-expanding-menu-panel-background-colour, var(--theme-surface-subtle));
192
217
  border: var(--deep-expanding-menu-panel-border-width, 0.1rem) solid
193
- var(--deep-expanding-menu-panel-border-colour, black);
218
+ var(--deep-expanding-menu-panel-border-colour, var(--theme-border));
194
219
  border-radius: var(--deep-expanding-menu-panel-border-radius, 1.2rem);
195
220
  box-shadow: var(--deep-expanding-menu-panel-shadow, 0 0 1rem rgba(0, 0, 0, 0.1));
196
221
  padding: var(--deep-expanding-menu-panel-padding, 1.2rem);
197
- overflow: clip;
222
+ overflow-x: clip;
223
+ overflow-y: auto;
198
224
 
199
225
  &:popover-open {
200
226
  display: block;
@@ -224,23 +250,33 @@ watch(
224
250
  }
225
251
  }
226
252
 
227
- h4 {
228
- color: var(--deep-expanding-menu-panel-heading-colour, var(--slate-10));
253
+ .deep-expanding-menu-group-heading {
254
+ color: var(--deep-expanding-menu-panel-heading-colour, var(--theme-text));
255
+ overflow-wrap: anywhere;
256
+ display: -webkit-box;
257
+ -webkit-box-orient: vertical;
258
+ overflow: hidden;
259
+ -webkit-line-clamp: var(--deep-expanding-menu-panel-heading-line-clamp, none);
260
+ line-clamp: var(--deep-expanding-menu-panel-heading-line-clamp, none);
229
261
  }
230
262
 
231
- .navigation-group-list {
263
+ .deep-expanding-menu-group-list {
232
264
  display: grid;
233
- grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
265
+ grid-template-columns: repeat(auto-fill, minmax(min(100%, 12rem), 1fr));
234
266
  gap: var(--deep-expanding-menu-panel-list-gap, 1.2rem);
267
+ list-style: none;
235
268
  padding-inline-start: 0;
236
- margin-block-end: 0.8rem;
269
+ margin-block: 0 0.8rem;
237
270
 
238
- .navigation-group-item {
271
+ .deep-expanding-menu-group-item {
239
272
  display: block;
273
+ min-inline-size: 0;
240
274
 
241
- a.navigation-group-link {
275
+ a.deep-expanding-menu-group-link {
242
276
  display: inline-block;
243
- color: var(--deep-expanding-menu-group-link-colour, var(--slate-10));
277
+ max-inline-size: 100%;
278
+ overflow-wrap: anywhere;
279
+ color: var(--deep-expanding-menu-group-link-colour, var(--theme-text));
244
280
  text-decoration: none;
245
281
  padding-block: 0.8rem;
246
282
 
@@ -251,14 +287,14 @@ watch(
251
287
  &:hover,
252
288
  &:focus-visible {
253
289
  cursor: pointer;
254
- border-color: var(--deep-expanding-menu-group-link-border-colour-hover, var(--slate-10));
290
+ border-color: var(--deep-expanding-menu-group-link-border-colour-hover, var(--theme-text));
255
291
  }
256
292
  }
257
293
  }
258
294
  }
259
295
  }
260
296
 
261
- &:has(> .navigation-group-toggle[aria-expanded="true"]) {
297
+ &:has(> .deep-expanding-menu-group-toggle[aria-expanded="true"]) {
262
298
  --_icon-transform: scaleY(-1);
263
299
  }
264
300
  }
@@ -267,3 +303,4 @@ watch(
267
303
  }
268
304
  }
269
305
  </style>
306
+