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,18 +1,34 @@
1
1
  <template>
2
- <component :is="tag" class="deep-expanding-menu-classic" :class="[elementClasses]">
3
- <div class="inner">
2
+ <component
3
+ :is="tag"
4
+ ref="rootRef"
5
+ class="deep-expanding-menu-classic"
6
+ :class="[elementClasses]"
7
+ :aria-label="ariaLabel"
8
+ @click="closeOtherGroups"
9
+ @keydown.esc="closeOnEscape"
10
+ >
11
+ <div class="deep-expanding-menu-classic-inner">
4
12
  <template v-for="(link, key) in navLinks" :key="key">
5
- <NuxtLink v-if="link.path" :to="link.path" class="navigation-link">{{ link.name }}</NuxtLink>
6
- <details v-else ref="navigationGroupRef" class="navigation-group" name="navigation-group">
7
- <summary class="navigation-group-toggle">
8
- <span>{{ link.name }}</span>
9
- <Icon name="bi:caret-down-fill" class="icon" />
13
+ <NuxtLink v-if="link.path" :to="link.path" class="deep-expanding-menu-classic-link">{{ link.name }}</NuxtLink>
14
+ <details v-else class="deep-expanding-menu-classic-group" :name="groupName">
15
+ <summary class="deep-expanding-menu-classic-group-toggle">
16
+ <span class="deep-expanding-menu-classic-group-label">{{ link.name }}</span>
17
+ <Icon name="bi:caret-down-fill" class="deep-expanding-menu-classic-group-icon" aria-hidden="true" />
10
18
  </summary>
11
- <div class="navigation-group-panel">
12
- <h4 class="page-heading-4 mb-6">{{ link.childLinksTitle }}</h4>
13
- <ul class="navigation-group-list">
14
- <li v-for="childLink in link.childLinks" :key="childLink.name" class="navigation-group-item">
15
- <NuxtLink :to="childLink.path" class="navigation-group-link">{{ childLink.name }}</NuxtLink>
19
+ <div class="deep-expanding-menu-classic-group-panel">
20
+ <h4 v-if="link.childLinksTitle" class="deep-expanding-menu-classic-group-heading page-heading-4 mb-6">
21
+ {{ link.childLinksTitle }}
22
+ </h4>
23
+ <ul v-if="link.childLinks?.length" class="deep-expanding-menu-classic-group-list" role="list">
24
+ <li
25
+ v-for="(childLink, childIndex) in link.childLinks"
26
+ :key="`${childIndex}-${childLink.name}`"
27
+ class="deep-expanding-menu-classic-group-item"
28
+ >
29
+ <NuxtLink :to="childLink.path" class="deep-expanding-menu-classic-group-link">
30
+ {{ childLink.name }}
31
+ </NuxtLink>
16
32
  </li>
17
33
  </ul>
18
34
  </div>
@@ -27,20 +43,48 @@ import { onClickOutside } from "@vueuse/core";
27
43
  import type { ResponsiveHeaderNavItem } from "~/types/components";
28
44
 
29
45
  interface Props {
30
- tag?: "div" | "section" | "nav" | "ul" | "ol";
46
+ tag?: "div" | "section" | "nav";
31
47
  navLinks?: ResponsiveHeaderNavItem[];
48
+ ariaLabel?: string;
32
49
  styleClassPassthrough?: string | string[];
33
50
  }
34
51
 
35
52
  const props = withDefaults(defineProps<Props>(), {
36
53
  tag: "nav",
37
54
  navLinks: () => [],
55
+ ariaLabel: undefined,
38
56
  styleClassPassthrough: () => [],
39
57
  });
40
58
 
41
59
  const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
42
60
 
43
- const navigationGroupRef = useTemplateRef<HTMLElement[]>("navigationGroupRef");
61
+ // Per-instance so groups are exclusive within one menu but not across menus.
62
+ const groupName = `deep-expanding-menu-classic-${useId()}`;
63
+
64
+ const rootRef = ref<HTMLElement | null>(null);
65
+
66
+ const openGroups = () =>
67
+ Array.from(rootRef.value?.querySelectorAll<HTMLDetailsElement>("details[open]") ?? []);
68
+
69
+ const closeOtherGroups = (event: Event) => {
70
+ const target = event.target as Element | null;
71
+ const followedLink = !!target?.closest?.("a");
72
+ openGroups().forEach((group) => {
73
+ if (followedLink || !target || !group.contains(target)) group.open = false;
74
+ });
75
+ };
76
+
77
+ const closeOnEscape = (event: KeyboardEvent) => {
78
+ const target = event.target as Node | null;
79
+ const group = openGroups().find((details) => target && details.contains(target));
80
+ if (!group) return;
81
+ group.open = false;
82
+ group.querySelector<HTMLElement>("summary")?.focus();
83
+ };
84
+
85
+ onClickOutside(rootRef, () => {
86
+ openGroups().forEach((group) => (group.open = false));
87
+ });
44
88
 
45
89
  watch(
46
90
  () => props.styleClassPassthrough,
@@ -48,36 +92,31 @@ watch(
48
92
  resetElementClasses(props.styleClassPassthrough);
49
93
  }
50
94
  );
51
-
52
- onMounted(() => {
53
- navigationGroupRef.value?.forEach((element, index) => {
54
- onClickOutside(element, () => {
55
- navigationGroupRef.value?.[index]?.removeAttribute("open");
56
- });
57
- });
58
- });
59
95
  </script>
60
96
 
61
97
  <style lang="css">
62
98
  @layer components {
63
99
  @layer popover-setup {
64
100
  .deep-expanding-menu-classic {
65
- container-type: inline-size;
101
+ container: deep-expanding-menu-classic / inline-size;
66
102
  display: grid;
67
103
  grid-template-areas: "element-stack";
68
104
  align-items: center;
69
- gap: var(--deep-expanding-menu-classic-gap, 1.2rem);
70
105
 
71
- .inner {
106
+ .deep-expanding-menu-classic-inner {
72
107
  grid-area: element-stack;
73
108
  display: flex;
109
+ flex-wrap: wrap;
74
110
  gap: var(--deep-expanding-menu-classic-item-gap, 2.4rem);
75
111
  align-items: center;
112
+ min-inline-size: 0;
76
113
  z-index: 1;
77
114
 
78
- .navigation-link,
79
- .navigation-group-toggle {
115
+ .deep-expanding-menu-classic-link,
116
+ .deep-expanding-menu-classic-group-toggle {
80
117
  all: unset;
118
+ min-inline-size: 0;
119
+ overflow-wrap: anywhere;
81
120
  border-bottom: var(--deep-expanding-menu-classic-link-border-width, 0.2rem) solid transparent;
82
121
  padding-block: var(--deep-expanding-menu-classic-link-padding-block, 0.8rem);
83
122
 
@@ -87,18 +126,22 @@ onMounted(() => {
87
126
  &:focus,
88
127
  &:focus-visible {
89
128
  cursor: pointer;
90
- border-color: var(
91
- --deep-expanding-menu-classic-link-border-colour-hover,
92
- var(--blue-10)
93
- );
129
+ border-color: var(--deep-expanding-menu-classic-link-border-colour-hover, var(--theme-border));
130
+ }
131
+
132
+ &:focus-visible {
133
+ outline: var(--deep-expanding-menu-classic-focus-outline-width, 0.2rem) solid
134
+ var(--deep-expanding-menu-classic-focus-outline-colour, var(--theme-ring, currentcolor));
135
+ outline-offset: 0.2rem;
94
136
  }
95
137
  }
96
138
 
97
- .navigation-group {
139
+ .deep-expanding-menu-classic-group {
98
140
  --_icon-transform: scaleY(1);
99
141
 
100
142
  display: grid;
101
143
  grid-template-areas: "details-stack";
144
+ min-inline-size: 0;
102
145
  z-index: 1;
103
146
  position: relative;
104
147
 
@@ -111,16 +154,21 @@ onMounted(() => {
111
154
  --_icon-transform: scaleY(-1);
112
155
  }
113
156
 
114
- .navigation-group-toggle {
157
+ .deep-expanding-menu-classic-group-toggle {
115
158
  grid-area: details-stack;
116
159
 
117
- display: flex !important;
160
+ display: flex;
118
161
  align-items: center;
119
162
  gap: 1.2rem;
120
163
  list-style: none;
121
164
 
122
- .icon {
165
+ .deep-expanding-menu-classic-group-label {
166
+ min-inline-size: 0;
167
+ }
168
+
169
+ .deep-expanding-menu-classic-group-icon {
123
170
  display: block;
171
+ flex-shrink: 0;
124
172
  font-size: var(--deep-expanding-menu-classic-icon-size, 1.2rem);
125
173
 
126
174
  transform: var(--_icon-transform);
@@ -128,51 +176,62 @@ onMounted(() => {
128
176
  }
129
177
  }
130
178
 
131
- .navigation-group-panel {
179
+ .deep-expanding-menu-classic-group-panel {
132
180
  display: grid;
133
181
  grid-area: details-stack;
134
182
  z-index: 2;
135
183
  position: absolute;
136
184
  inset: auto;
137
- top: var(--deep-expanding-menu-classic-panel-offset-top, 4rem);
138
- left: 0rem;
185
+ top: var(--deep-expanding-menu-classic-panel-offset-top, calc(100% + 1rem));
186
+ left: 0;
139
187
  gap: var(--deep-expanding-menu-classic-panel-list-gap, 1.2rem);
140
188
 
141
- width: var(--deep-expanding-menu-classic-panel-width, 20rem);
189
+ width: min(var(--deep-expanding-menu-classic-panel-width, 20rem), 100cqi);
142
190
 
143
- @media screen and (min-width: 768px) {
144
- width: var(--deep-expanding-menu-classic-panel-width-tablet, 40rem);
191
+ @container deep-expanding-menu-classic (min-width: 768px) {
192
+ width: min(var(--deep-expanding-menu-classic-panel-width-tablet, 40rem), 100cqi);
145
193
  }
146
194
 
147
- @media screen and (min-width: 1024px) {
148
- width: var(--deep-expanding-menu-classic-panel-width-desktop, 60rem);
195
+ @container deep-expanding-menu-classic (min-width: 1024px) {
196
+ width: min(var(--deep-expanding-menu-classic-panel-width-desktop, 60rem), 100cqi);
149
197
  }
150
198
 
151
- background-color: var(--deep-expanding-menu-classic-panel-background-colour, white);
199
+ color: var(--deep-expanding-menu-classic-panel-text-colour, var(--theme-text));
200
+ background-color: var(--deep-expanding-menu-classic-panel-background-colour, var(--theme-surface-subtle));
152
201
  border: var(--deep-expanding-menu-classic-panel-border-width, 0.1rem) solid
153
- var(--deep-expanding-menu-classic-panel-border-colour, black);
202
+ var(--deep-expanding-menu-classic-panel-border-colour, var(--theme-border));
154
203
  border-radius: var(--deep-expanding-menu-classic-panel-border-radius, 1.2rem);
155
204
  box-shadow: var(--deep-expanding-menu-classic-panel-shadow, 0 0 1rem rgba(0, 0, 0, 0.1));
156
205
  padding: var(--deep-expanding-menu-classic-panel-padding, 1.2rem);
157
206
  overflow: clip;
158
207
 
159
- h4 {
160
- color: var(--deep-expanding-menu-classic-panel-heading-colour, var(--slate-10));
208
+ .deep-expanding-menu-classic-group-heading {
209
+ color: var(--deep-expanding-menu-classic-panel-heading-colour, var(--theme-text));
210
+ overflow-wrap: anywhere;
211
+ display: -webkit-box;
212
+ -webkit-box-orient: vertical;
213
+ overflow: hidden;
214
+ -webkit-line-clamp: var(--deep-expanding-menu-classic-panel-heading-line-clamp, none);
215
+ line-clamp: var(--deep-expanding-menu-classic-panel-heading-line-clamp, none);
161
216
  }
162
217
 
163
- .navigation-group-list {
218
+ .deep-expanding-menu-classic-group-list {
164
219
  display: grid;
165
- grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
220
+ grid-template-columns: repeat(auto-fill, minmax(min(100%, 12rem), 1fr));
166
221
  gap: var(--deep-expanding-menu-classic-panel-list-gap, 1.2rem);
222
+ list-style: none;
167
223
  padding-inline-start: 0;
168
- margin-block-end: 0.8rem;
224
+ margin-block: 0 0.8rem;
169
225
 
170
- .navigation-group-item {
226
+ .deep-expanding-menu-classic-group-item {
171
227
  display: block;
228
+ min-inline-size: 0;
172
229
 
173
- a.navigation-group-link {
230
+ a.deep-expanding-menu-classic-group-link {
174
231
  display: inline-block;
175
- color: var(--deep-expanding-menu-classic-group-link-colour, var(--slate-10));
232
+ max-inline-size: 100%;
233
+ overflow-wrap: anywhere;
234
+ color: var(--deep-expanding-menu-classic-group-link-colour, var(--theme-text));
176
235
  text-decoration: none;
177
236
  padding-block: 0.8rem;
178
237
 
@@ -183,10 +242,7 @@ onMounted(() => {
183
242
  &:hover,
184
243
  &:focus-visible {
185
244
  cursor: pointer;
186
- border-color: var(
187
- --deep-expanding-menu-classic-group-link-border-colour-hover,
188
- var(--slate-10)
189
- );
245
+ border-color: var(--deep-expanding-menu-classic-group-link-border-colour-hover, var(--theme-text));
190
246
  }
191
247
  }
192
248
  }
@@ -1,6 +1,8 @@
1
+ import { computed, ref } from "vue";
1
2
  import DeepExpandingMenuClassic from "../DeepExpandingMenuClassic.vue";
3
+ import CanvasSwitcher from "../../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
2
4
  import type { Meta, StoryObj } from "@nuxtjs/storybook";
3
- import type { ResponsiveHeaderNavItem } from "~/types/components";
5
+ import type { MediaCanvas, ResponsiveHeaderNavItem } from "~/types/components";
4
6
 
5
7
  const navLinks: ResponsiveHeaderNavItem[] = [
6
8
  { name: "Home", path: "/" },
@@ -16,63 +18,181 @@ const navLinks: ResponsiveHeaderNavItem[] = [
16
18
  { name: "Contact", path: "/contact" },
17
19
  ];
18
20
 
19
- const meta: Meta<typeof DeepExpandingMenuClassic> = {
21
+ const stressNavLinks: ResponsiveHeaderNavItem[] = [
22
+ { name: "H", path: "/" },
23
+ {
24
+ name: "Dienstleistungsangebotsübersichtsseite",
25
+ childLinksTitle:
26
+ "Unsere vollständige Übersicht aller Dienstleistungen, Behandlungen und Pflegeangebote für Haut und Haar in Northtown",
27
+ childLinks: [
28
+ { name: "Haarschnitt", path: "/services/haircuts" },
29
+ { name: "https://example.com/a/very/long/unbroken/path/that/never/wraps/on/its/own", path: "/long-url" },
30
+ { name: "🎨 Colouring", path: "/services/colouring" },
31
+ { name: "Styling", path: "/services/styling-1" },
32
+ { name: "Styling", path: "/services/styling-2" },
33
+ { name: "<b>Not bold</b>", path: "/services/html" },
34
+ { name: "قص الشعر", path: "/services/rtl" },
35
+ ...Array.from({ length: 24 }, (_, i) => ({ name: `Treatment ${i + 1}`, path: `/services/treatment-${i + 1}` })),
36
+ ],
37
+ },
38
+ { name: "Gruppe ohne Titel", childLinks: [{ name: "Only link", path: "/only" }] },
39
+ { name: "Empty group", childLinksTitle: "Nothing here yet", childLinks: [] },
40
+ { name: "Supercalifragilisticexpialidociousandthensomemoretext", path: "/unbroken" },
41
+ { name: "🙂", path: "/emoji" },
42
+ { name: "Contact", path: "/contact" },
43
+ { name: "Contact", path: "/contact" },
44
+ {
45
+ name: "About us and our story",
46
+ childLinksTitle: "About Alex Morgan",
47
+ childLinks: [
48
+ { name: "Team", path: "/about/team" },
49
+ { name: "History", path: "/about/history" },
50
+ ],
51
+ },
52
+ ];
53
+
54
+ type LineClamp = "none" | "1" | "2" | "3";
55
+
56
+ // Story-only arg: drives a CSS token, not a DeepExpandingMenuClassic prop. See the argTypes entry.
57
+ type StoryArgs = InstanceType<typeof DeepExpandingMenuClassic>["$props"] & {
58
+ headingLineClamp: LineClamp;
59
+ };
60
+
61
+ const meta: Meta<StoryArgs> = {
20
62
  title: "Molecules/DeepExpandingMenuClassic",
21
63
  component: DeepExpandingMenuClassic,
22
64
  argTypes: {
65
+ headingLineClamp: {
66
+ control: "select",
67
+ options: ["none", "1", "2", "3"] satisfies LineClamp[],
68
+ description:
69
+ "**Story control, not a prop.** Sets the `--deep-expanding-menu-classic-panel-heading-line-clamp` CSS token on a wrapper so you can try it here. " +
70
+ "To use it in an app, set the token in your own CSS, e.g. `.site-nav { --deep-expanding-menu-classic-panel-heading-line-clamp: 1; }`. " +
71
+ "`1` is single-line ellipsis, `none` (the default) shows everything.",
72
+ table: { category: "CSS tokens (story only, set in your CSS)", defaultValue: { summary: "none" } },
73
+ },
23
74
  tag: {
24
75
  control: { type: "select" },
25
- options: ["div", "section", "nav", "ul", "ol"],
76
+ options: ["div", "section", "nav"],
26
77
  description: "Root element tag",
78
+ table: { category: "Basic", defaultValue: { summary: "nav" } },
79
+ },
80
+ ariaLabel: {
81
+ control: "text",
82
+ description: "Accessible name for the root. Set it when the page has more than one `<nav>`",
27
83
  table: { category: "Basic" },
28
84
  },
29
- navLinks: { table: { disable: true } },
85
+ navLinks: {
86
+ control: "object",
87
+ description: "Top-level items: `path` renders a link, `childLinks` renders a toggle and panel",
88
+ table: { category: "Content" },
89
+ },
30
90
  styleClassPassthrough: { table: { disable: true } },
31
91
  },
32
92
  args: {
33
93
  tag: "nav",
34
94
  navLinks,
95
+ headingLineClamp: "none",
35
96
  },
97
+ decorators: [
98
+ (story, context) => ({
99
+ components: { story, CanvasSwitcher },
100
+ setup() {
101
+ const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
102
+ return { canvasName };
103
+ },
104
+ template: `
105
+ <div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
106
+ <CanvasSwitcher v-model:canvas-name="canvasName" />
107
+ </div>
108
+ <div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
109
+ <story />
110
+ </div>
111
+ `,
112
+ }),
113
+ ],
36
114
  parameters: {
115
+ layout: "fullscreen",
37
116
  docs: {
38
117
  description: {
39
118
  component:
40
- "`<details>`/`<summary>`-based fallback implementation of `DeepExpandingMenu`, using `onClickOutside` to close an open group instead of CSS anchor-positioning/Popover API — use this when the modern implementation's browser support (see its own docs) isn't acceptable.",
119
+ "`<details>`/`<summary>`-based version of `DeepExpandingMenu`: each item is a direct link (`path`) or a native `<details>` group showing its child links (`childLinks`, with an optional `childLinksTitle` heading). " +
120
+ "Groups in one menu are exclusive; outside click, Escape and following a child link close the open group. No Popover API or anchor positioning involved. Props: `navLinks`, `tag`, `ariaLabel`. " +
121
+ "Prefer `DeepExpandingMenu` unless you need this simpler fallback.",
41
122
  },
42
123
  },
43
124
  },
44
125
  };
45
126
 
46
127
  export default meta;
47
- type Story = StoryObj<typeof DeepExpandingMenuClassic>;
128
+ type Story = StoryObj<StoryArgs>;
129
+
130
+ function useStorySetup(args: StoryArgs) {
131
+ const componentArgs = computed(() => {
132
+ const { headingLineClamp: _headingLineClamp, ...rest } = args;
133
+ return rest;
134
+ });
135
+ const tokenStyles = computed(() => ({
136
+ "--deep-expanding-menu-classic-panel-heading-line-clamp": args.headingLineClamp,
137
+ }));
138
+ return { componentArgs, tokenStyles };
139
+ }
140
+
141
+ const render = (args: StoryArgs) => ({
142
+ components: { DeepExpandingMenuClassic },
143
+ setup() {
144
+ return useStorySetup(args);
145
+ },
146
+ template: `<div :style="tokenStyles"><DeepExpandingMenuClassic v-bind="componentArgs" /></div>`,
147
+ });
48
148
 
49
149
  export const Default: Story = {
50
- render: (args) => ({
51
- components: { DeepExpandingMenuClassic },
52
- setup() {
53
- return { args };
54
- },
55
- template: `<DeepExpandingMenuClassic v-bind="args" style="padding: 4rem;" />`,
56
- }),
150
+ render,
57
151
  };
58
152
 
59
153
  export const CustomColours: Story = {
154
+ name: "Custom Colours",
60
155
  render: (args) => ({
61
156
  components: { DeepExpandingMenuClassic },
62
157
  setup() {
63
- return { args };
158
+ return useStorySetup(args);
64
159
  },
65
160
  template: `
66
- <DeepExpandingMenuClassic
67
- v-bind="args"
68
- style="padding: 4rem; --deep-expanding-menu-classic-panel-background-colour: #1a1a1a; --deep-expanding-menu-classic-panel-border-colour: transparent; --deep-expanding-menu-classic-group-link-colour: white; --deep-expanding-menu-classic-panel-heading-colour: #aaaaaa;"
69
- />
161
+ <div
162
+ :style="tokenStyles"
163
+ style="--deep-expanding-menu-classic-panel-background-colour: #1a1a1a; --deep-expanding-menu-classic-panel-border-colour: transparent; --deep-expanding-menu-classic-group-link-colour: white; --deep-expanding-menu-classic-group-link-border-colour-hover: white; --deep-expanding-menu-classic-panel-heading-colour: #aaaaaa;"
164
+ >
165
+ <DeepExpandingMenuClassic v-bind="componentArgs" />
166
+ </div>
70
167
  `,
71
168
  }),
72
169
  parameters: {
73
170
  docs: {
74
171
  description: {
75
- story: "Overriding the public tokens via inline `--deep-expanding-menu-classic-*` custom properties.",
172
+ story: "Overriding the public `--deep-expanding-menu-classic-*` tokens on a wrapper element.",
173
+ },
174
+ },
175
+ },
176
+ };
177
+
178
+ export const StressTest: Story = {
179
+ name: "Stress Test (Worst-Case Data)",
180
+ args: {
181
+ navLinks: stressNavLinks,
182
+ },
183
+ render,
184
+ parameters: {
185
+ initialCanvas: "mobileCanvas",
186
+ docs: {
187
+ story: { inline: false, height: "720px" },
188
+ description: {
189
+ story:
190
+ "Hostile nav data at every `CanvasSwitcher` width. Check that: the top-level row wraps instead of overflowing; " +
191
+ "the long German label and the unbroken word break inside the canvas; the caret stays beside a wrapped toggle label; " +
192
+ "the first panel's long heading wraps (try the story-only `headingLineClamp` CSS token control), its unbroken URL link stays inside the panel, " +
193
+ "`<b>Not bold</b>` renders as text, the right-to-left link reads correctly, and its 31 links stay inside the panel; " +
194
+ "duplicate `Styling` and `Contact` items both render; a group without a title shows its list only; " +
195
+ "an empty group opens with its heading and no list; panels are never wider than the menu itself (they open from their toggle, so one near the right edge can still overrun it: `DeepExpandingMenu` flips instead).",
76
196
  },
77
197
  },
78
198
  },