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,8 +1,8 @@
1
1
  <template>
2
- <PageRow tag="div" :variant="pageRowVariant" :style-class-passthrough="styleClassPassthrough">
2
+ <PageRow tag="div" class="site-header-row" :variant="pageRowVariant" :style-class-passthrough="styleClassPassthrough">
3
3
  <template #default>
4
4
  <header class="site-header">
5
- <nav class="home-navigation" :aria-label="homeNavAriaLabel">
5
+ <nav class="site-header-home-nav" :aria-label="homeNavAriaLabel">
6
6
  <SkipLinks>
7
7
  <template #homeLink>
8
8
  <slot name="branding"></slot>
@@ -33,7 +33,7 @@
33
33
  </template>
34
34
 
35
35
  <script setup lang="ts">
36
- import type { ResponsiveHeaderProp } from "../../../types/components";
36
+ import type { ResponsiveHeaderProp } from "~/types/components";
37
37
  import PageRow from "../../01.atoms/page-row/PageRow.vue";
38
38
  import SkipLinks from "../../01.atoms/navigation/skip-links/SkipLinks.vue";
39
39
  import ResponsiveHeader from "../responsive-header/ResponsiveHeader.vue";
@@ -75,7 +75,7 @@ withDefaults(defineProps<Props>(), {
75
75
  pageRowVariant: "content",
76
76
  styleClassPassthrough: () => [],
77
77
  navStyleClassPassthrough: () => [],
78
- homeNavAriaLabel: "Home Navigation",
78
+ homeNavAriaLabel: "Home navigation",
79
79
  mainNavAriaLabel: "Main navigation",
80
80
  secondaryNavAriaLabel: "Secondary navigation",
81
81
  overflowMenuAriaLabel: "Overflow navigation menu",
@@ -88,32 +88,27 @@ const slots = useSlots();
88
88
 
89
89
  <style lang="css">
90
90
  @layer components {
91
- /* ─── Public CSS tokens ─────────────────────────────────────────────────
92
- Override on the consumer's scope class (via styleClassPassthrough).
93
- Nested --responsive-header-* / --overflow-nav-* tokens still apply —
94
- forward a scope class via navStyleClassPassthrough to reach them.
95
-
96
- --site-header-gap (default: 2.4rem)
97
- --site-header-padding-inline (default: 2.4rem)
98
- --site-header-padding-block (default: 0px)
99
- --site-header-bg (default: transparent)
100
- --site-header-position (default: static)
101
- --site-header-sticky-offset (default: 0px)
102
- --site-header-z-index (default: 9)
103
- ──────────────────────────────────────────────────────────────────────── */
91
+ .site-header-row {
92
+ position: var(--site-header-position, static);
93
+ top: var(--site-header-sticky-offset, 0px);
94
+ z-index: var(--site-header-z-index, 9);
95
+ }
104
96
 
105
97
  .site-header {
106
98
  display: grid;
107
- grid-template-columns: auto 1fr;
99
+ grid-template-columns: fit-content(var(--site-header-branding-max-width, 50%)) minmax(0, 1fr);
108
100
  align-items: center;
109
101
  gap: var(--site-header-gap, 2.4rem);
110
102
  padding-block: var(--site-header-padding-block, 0px);
111
103
  padding-inline: var(--site-header-padding-inline, 2.4rem);
112
104
  background-color: var(--site-header-bg, transparent);
113
- position: var(--site-header-position, static);
114
- top: var(--site-header-sticky-offset, 0px);
115
- z-index: var(--site-header-z-index, 9);
116
105
  width: 100%;
106
+ min-inline-size: 0;
107
+
108
+ .site-header-home-nav {
109
+ min-inline-size: 0;
110
+ overflow-wrap: anywhere;
111
+ }
117
112
  }
118
113
  }
119
114
  </style>
@@ -1,5 +1,8 @@
1
+ import { ref } from "vue";
1
2
  import SiteHeader from "../SiteHeader.vue";
3
+ import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
2
4
  import type { Meta, StoryObj } from "@nuxtjs/storybook";
5
+ import type { MediaCanvas } from "~/types/components";
3
6
 
4
7
  const meta: Meta<typeof SiteHeader> = {
5
8
  title: "Organisms/Site Header",
@@ -34,6 +37,16 @@ const meta: Meta<typeof SiteHeader> = {
34
37
  control: "object",
35
38
  description: "Extra CSS classes applied to the nested ResponsiveHeader (targets its own token API)",
36
39
  },
40
+ homeNavAriaLabel: {
41
+ control: "text",
42
+ description: "Accessible name of the branding nav landmark",
43
+ table: { category: "Accessibility", defaultValue: { summary: "Home navigation" } },
44
+ },
45
+ overflowButtonLabel: {
46
+ control: "text",
47
+ description: "Accessible name of the burger button (forwarded to ResponsiveHeader)",
48
+ table: { category: "Accessibility", defaultValue: { summary: "More navigation" } },
49
+ },
37
50
  },
38
51
  args: {
39
52
  pageRowVariant: "content",
@@ -43,6 +56,16 @@ const meta: Meta<typeof SiteHeader> = {
43
56
  styleClassPassthrough: [],
44
57
  navStyleClassPassthrough: [],
45
58
  },
59
+ parameters: {
60
+ docs: {
61
+ description: {
62
+ component:
63
+ "Site header row: a `#branding` slot inside a skip-links nav, then `ResponsiveHeader` for the main navigation, all inside a `PageRow`. " +
64
+ "Props: `responsiveNavLinks` (nav groups), `pageRowVariant`, collapse options forwarded to `ResponsiveHeader`, and label props for every landmark and button. " +
65
+ "Slots: `branding`, `secondaryNavigation`. Make it sticky with `--site-header-position: sticky` on the passthrough class.",
66
+ },
67
+ },
68
+ },
46
69
  };
47
70
 
48
71
  export default meta;
@@ -133,3 +156,118 @@ export const WithSecondaryNavigationSlot: Story = {
133
156
  `,
134
157
  }),
135
158
  };
159
+
160
+ const brandingTemplate = `<a href="/" style="color:inherit;text-decoration:none;font-weight:600">{{ brand }}</a>`;
161
+
162
+ export const Sticky: Story = {
163
+ name: "Sticky (--site-header-position)",
164
+ args: {
165
+ styleClassPassthrough: ["site-header-sticky-demo"],
166
+ responsiveNavLinks: {
167
+ main: [
168
+ { name: "Home", path: "/" },
169
+ { name: "About", path: "/about" },
170
+ { name: "Contact", path: "/contact" },
171
+ ],
172
+ },
173
+ },
174
+ render: (args) => ({
175
+ components: { SiteHeader },
176
+ setup() {
177
+ return { args, brand: "Brand" };
178
+ },
179
+ template: `
180
+ <div>
181
+ <SiteHeader
182
+ v-bind="args"
183
+ style="--site-header-position: sticky; --site-header-bg: var(--theme-surface-subtle); --site-header-padding-block: 1.2rem;"
184
+ >
185
+ <template #branding>${brandingTemplate}</template>
186
+ </SiteHeader>
187
+ <div style="padding: 2.4rem; display: grid; gap: 1.6rem;">
188
+ <p v-for="n in 40" :key="n" style="margin: 0;">Scroll the page: the header stays pinned to the top. Paragraph {{ n }}.</p>
189
+ </div>
190
+ </div>
191
+ `,
192
+ }),
193
+ parameters: {
194
+ layout: "fullscreen",
195
+ docs: {
196
+ story: { inline: false, height: "480px" },
197
+ description: {
198
+ story:
199
+ "`--site-header-position: sticky` on the root. Until 2026-10-21 the sticky rule sat on the inner `<header>`, whose parent is exactly its height, so it never stuck; it now sits on the outer `.site-header-row`.",
200
+ },
201
+ },
202
+ },
203
+ };
204
+
205
+ export const StressTest: Story = {
206
+ name: "Stress Test (Worst-Case Data)",
207
+ args: {
208
+ responsiveNavLinks: {
209
+ firstNav: [
210
+ { name: "Dienstleistungsübersicht", path: "/a" },
211
+ {
212
+ name: "Behandlungen und Pflegeangebote",
213
+ childLinksTitle: "Alle Behandlungen in Northtown",
214
+ childLinks: [
215
+ { name: "Supercalifragilisticexpialidociousandthensome", path: "/b1" },
216
+ { name: "🙂 Emoji link", path: "/b2" },
217
+ { name: "<b>Not bold</b>", path: "/b3" },
218
+ ],
219
+ },
220
+ { name: "مرحبا", path: "/c" },
221
+ { name: "x", path: "/d" },
222
+ ...Array.from({ length: 8 }, (_, i) => ({ name: `Item ${i + 1}`, path: `/item-${i + 1}` })),
223
+ ],
224
+ secondNav: [
225
+ { name: "Contact", path: "/contact" },
226
+ { name: "Contact", path: "/contact" },
227
+ ],
228
+ },
229
+ collapseAtMainNavIntersection: true,
230
+ },
231
+ render: (args) => ({
232
+ components: { SiteHeader },
233
+ setup() {
234
+ return { args, brand: "Alex Morgan Hair, Beauty and Wellbeing Studio of Northtown" };
235
+ },
236
+ template: `
237
+ <SiteHeader v-bind="args">
238
+ <template #branding>${brandingTemplate}</template>
239
+ </SiteHeader>
240
+ `,
241
+ }),
242
+ decorators: [
243
+ (story, context) => ({
244
+ components: { story, CanvasSwitcher },
245
+ setup() {
246
+ const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
247
+ return { canvasName };
248
+ },
249
+ template: `
250
+ <div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
251
+ <CanvasSwitcher v-model:canvas-name="canvasName" />
252
+ </div>
253
+ <div :class="canvasName" style="margin-inline: auto; outline: 1px dashed currentColor;">
254
+ <story />
255
+ </div>
256
+ `,
257
+ }),
258
+ ],
259
+ parameters: {
260
+ layout: "fullscreen",
261
+ initialCanvas: "tabletCanvas",
262
+ docs: {
263
+ story: { inline: false, height: "520px" },
264
+ description: {
265
+ story:
266
+ "A long brand name and 14 hostile nav items at every `CanvasSwitcher` width, with `collapseAtMainNavIntersection` on. Check that: the brand column takes at most half the row " +
267
+ "(`--site-header-branding-max-width`) and wraps instead of squeezing the nav to nothing; the nav collapses into the burger rather than overflowing; " +
268
+ "the long German, emoji, `<b>` and right-to-left labels render as text and wrap in the dropdown; the duplicate `Contact` links both render. " +
269
+ "Set `responsiveNavLinks` to `{}` in Controls: the header shows just the brand.",
270
+ },
271
+ },
272
+ },
273
+ };
@@ -1,7 +1,7 @@
1
- import { describe, it, expect } from "vitest";
1
+ import { describe, it, expect, afterEach, vi } from "vitest";
2
2
  import { mountSuspended } from "@nuxt/test-utils/runtime";
3
3
  import SiteHeader from "../SiteHeader.vue";
4
- import type { ResponsiveHeaderProp } from "../../../../types/components";
4
+ import type { ResponsiveHeaderProp } from "~/types/components";
5
5
 
6
6
  const navLinks: ResponsiveHeaderProp = {
7
7
  firstNav: [{ name: "Home", path: "/" }],
@@ -21,39 +21,49 @@ const responsiveHeaderStub = {
21
21
  template: `<div class="responsive-header-stub"><slot name="secondaryNavigation"></slot></div>`,
22
22
  };
23
23
 
24
+ const mounted: Awaited<ReturnType<typeof mountSuspended>>[] = [];
25
+
26
+ const mountHeader = async (options: { props?: Record<string, unknown>; slots?: Record<string, string> } = {}) => {
27
+ const wrapper = await mountSuspended(SiteHeader, {
28
+ ...options,
29
+ global: { stubs: { ResponsiveHeader: responsiveHeaderStub } },
30
+ });
31
+ mounted.push(wrapper);
32
+ return wrapper;
33
+ };
34
+
35
+ afterEach(() => {
36
+ mounted.splice(0).forEach((wrapper) => wrapper.unmount());
37
+ vi.restoreAllMocks();
38
+ });
39
+
24
40
  describe("SiteHeader", () => {
25
41
  it("mounts without error", async () => {
26
- const wrapper = await mountSuspended(SiteHeader, {
27
- global: { stubs: { ResponsiveHeader: responsiveHeaderStub } },
28
- });
42
+ const wrapper = await mountHeader();
29
43
  expect(wrapper.vm).toBeTruthy();
30
44
  });
31
45
 
32
46
  it("renders the branding slot inside SkipLinks' homeLink slot", async () => {
33
- const wrapper = await mountSuspended(SiteHeader, {
47
+ const wrapper = await mountHeader({
34
48
  slots: { branding: "<a href='/' class='brand-link'>Brand</a>" },
35
- global: { stubs: { ResponsiveHeader: responsiveHeaderStub } },
36
49
  });
37
50
  expect(wrapper.find(".skip-links__home .brand-link").exists()).toBe(true);
38
51
  });
39
52
 
40
53
  it("does not render the secondaryNavigation slot when not provided", async () => {
41
- const wrapper = await mountSuspended(SiteHeader, {
42
- global: { stubs: { ResponsiveHeader: responsiveHeaderStub } },
43
- });
54
+ const wrapper = await mountHeader();
44
55
  expect(wrapper.text()).not.toContain("Extra nav");
45
56
  });
46
57
 
47
58
  it("forwards the secondaryNavigation slot through to ResponsiveHeader", async () => {
48
- const wrapper = await mountSuspended(SiteHeader, {
59
+ const wrapper = await mountHeader({
49
60
  slots: { secondaryNavigation: "<span>Extra nav</span>" },
50
- global: { stubs: { ResponsiveHeader: responsiveHeaderStub } },
51
61
  });
52
62
  expect(wrapper.text()).toContain("Extra nav");
53
63
  });
54
64
 
55
65
  it("forwards responsiveNavLinks and other nav props to ResponsiveHeader", async () => {
56
- const wrapper = await mountSuspended(SiteHeader, {
66
+ const wrapper = await mountHeader({
57
67
  props: {
58
68
  responsiveNavLinks: navLinks,
59
69
  gapBetweenFirstAndSecondNav: 24,
@@ -61,7 +71,6 @@ describe("SiteHeader", () => {
61
71
  allowExpandOnGesture: false,
62
72
  navStyleClassPassthrough: ["site-header-nav"],
63
73
  },
64
- global: { stubs: { ResponsiveHeader: responsiveHeaderStub } },
65
74
  });
66
75
  const responsiveHeader = wrapper.findComponent(responsiveHeaderStub);
67
76
  expect(responsiveHeader.props("responsiveNavLinks")).toEqual(navLinks);
@@ -72,32 +81,50 @@ describe("SiteHeader", () => {
72
81
  });
73
82
 
74
83
  it("passes pageRowVariant through to the root PageRow", async () => {
75
- const wrapper = await mountSuspended(SiteHeader, {
84
+ const wrapper = await mountHeader({
76
85
  props: { pageRowVariant: "full" },
77
- global: { stubs: { ResponsiveHeader: responsiveHeaderStub } },
78
86
  });
79
- expect(wrapper.find(".page-row.full").exists()).toBe(true);
87
+ expect(wrapper.find(".page-row[data-variant=\"full\"]").exists()).toBe(true);
80
88
  });
81
89
 
82
90
  it("defaults pageRowVariant to content", async () => {
83
- const wrapper = await mountSuspended(SiteHeader, {
84
- global: { stubs: { ResponsiveHeader: responsiveHeaderStub } },
85
- });
86
- expect(wrapper.find(".page-row.content").exists()).toBe(true);
91
+ const wrapper = await mountHeader();
92
+ expect(wrapper.find(".page-row[data-variant=\"content\"]").exists()).toBe(true);
87
93
  });
88
94
 
89
95
  it("applies styleClassPassthrough to the root PageRow", async () => {
90
- const wrapper = await mountSuspended(SiteHeader, {
96
+ const wrapper = await mountHeader({
91
97
  props: { styleClassPassthrough: ["header"] },
92
- global: { stubs: { ResponsiveHeader: responsiveHeaderStub } },
93
98
  });
94
99
  expect(wrapper.find(".page-row.header").exists()).toBe(true);
95
100
  });
96
101
 
97
- it("renders correct HTML structure with default props", async () => {
98
- const wrapper = await mountSuspended(SiteHeader, {
99
- global: { stubs: { ResponsiveHeader: responsiveHeaderStub } },
102
+ it("puts the sticky hook on the outer row, not the inner header", async () => {
103
+ const wrapper = await mountHeader({ props: { styleClassPassthrough: ["header"] } });
104
+ expect(wrapper.classes()).toContain("site-header-row");
105
+ expect(wrapper.classes()).toContain("header");
106
+ expect(wrapper.find("header.site-header").classes()).not.toContain("site-header-row");
107
+ });
108
+
109
+ it("labels the branding nav landmark, defaulting to Home navigation", async () => {
110
+ const fallback = await mountHeader();
111
+ expect(fallback.find("nav.site-header-home-nav").attributes("aria-label")).toBe("Home navigation");
112
+
113
+ const custom = await mountHeader({ props: { homeNavAriaLabel: "Startseite" } });
114
+ expect(custom.find("nav.site-header-home-nav").attributes("aria-label")).toBe("Startseite");
115
+ });
116
+
117
+ it("renders the brand alone when there are no nav links", async () => {
118
+ const wrapper = await mountHeader({
119
+ props: { responsiveNavLinks: {} },
120
+ slots: { branding: "<a href='/' class='brand-link'>Brand</a>" },
100
121
  });
122
+ expect(wrapper.find(".brand-link").exists()).toBe(true);
123
+ expect(wrapper.findComponent(responsiveHeaderStub).props("responsiveNavLinks")).toEqual({});
124
+ });
125
+
126
+ it("renders correct HTML structure with default props", async () => {
127
+ const wrapper = await mountHeader();
101
128
  expect(wrapper.html()).toMatchSnapshot();
102
129
  });
103
130
  });
@@ -1,9 +1,9 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
3
  exports[`SiteHeader > renders correct HTML structure with default props 1`] = `
4
- "<div class="page-row content">
4
+ "<div class="page-row site-header-row" data-variant="content">
5
5
  <header class="site-header">
6
- <nav class="home-navigation" aria-label="Home Navigation">
6
+ <nav class="site-header-home-nav" aria-label="Home navigation">
7
7
  <div class="skip-links">
8
8
  <div class="skip-links__home"></div>
9
9
  <nav class="skip-links__nav" aria-label="Skip navigation"><a href="#main-content" class="skip-links__link">Skip to main content</a><a href="#footer-content" class="skip-links__link">Skip to footer</a></nav>
@@ -5,3 +5,15 @@ export interface QrCodeVariant {
5
5
  marker: SVGVariant
6
6
  pixel: SVGVariant
7
7
  }
8
+
9
+ export type CaptureQrCodeErrorName =
10
+ | "NotAllowedError"
11
+ | "NotFoundError"
12
+ | "NotSupportedError"
13
+ | "NotReadableError"
14
+ | "OverconstrainedError"
15
+ | "StreamApiNotSupportedError"
16
+ | "InsecureContextError"
17
+ | "StreamLoadTimeoutError"
18
+
19
+ export type CaptureQrCodeErrorMessages = Partial<Record<CaptureQrCodeErrorName | "default", string>>
@@ -0,0 +1,8 @@
1
+ const segmenter =
2
+ typeof Intl !== "undefined" && "Segmenter" in Intl ? new Intl.Segmenter(undefined, { granularity: "grapheme" }) : null;
3
+
4
+ // User-perceived characters, so a typewriter never shows half an emoji or a stray combining mark.
5
+ export function splitGraphemes(text: string): string[] {
6
+ if (!segmenter) return Array.from(text);
7
+ return Array.from(segmenter.segment(text), (segment) => segment.segment);
8
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "srcdev-nuxt-components",
3
3
  "type": "module",
4
- "version": "9.6.18",
4
+ "version": "9.6.20",
5
5
  "main": "nuxt.config.ts",
6
6
  "types": "types.d.ts",
7
7
  "license": "MIT",
@@ -1,180 +0,0 @@
1
- # AccordianCore Component
2
-
3
- ## Overview
4
-
5
- `AccordianCore` renders a group of `ExpandingPanel` components (or `ExpandingPanelClassic`, via the `variant` prop). When a shared `name` prop is supplied, the native `<details>` behaviour ensures only one panel can be open at a time. Content is filled via **indexed dynamic slots** — one set per panel, driven by `itemCount`.
6
-
7
- ---
8
-
9
- ## Dynamic slot pattern
10
-
11
- For `itemCount="3"` the following slots exist:
12
-
13
- | Slot | Purpose |
14
- |------|---------|
15
- | `#accordian-0-summary` | Clickable label for panel 0 |
16
- | `#accordian-0-icon` | Custom toggle icon for panel 0 (optional) |
17
- | `#accordian-0-content` | Expandable content for panel 0 |
18
- | `#accordian-1-summary` | Clickable label for panel 1 |
19
- | `#accordian-1-icon` | Custom toggle icon for panel 1 (optional) |
20
- | `#accordian-1-content` | Expandable content for panel 1 |
21
- | … | … |
22
-
23
- **Rules:**
24
- - Slots are **zero-indexed**: first panel = `accordian-0-*`, last = `accordian-{itemCount - 1}-*`.
25
- - `accordian-{n}-summary` and `accordian-{n}-content` are the primary slots — always provide these.
26
- - `accordian-{n}-icon` is optional. When omitted, the default `ExpandingPanel` caret icon is used.
27
- - Always keep `itemCount` in sync with the number of slot sets you provide.
28
-
29
- ---
30
-
31
- ## Props reference
32
-
33
- | Prop | Type | Default | Notes |
34
- |------|------|---------|-------|
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
- | `itemCount` | `number` | `0` | Number of `ExpandingPanel` components to render. |
37
- | `animationDuration` | `number` | `300` | Expand/collapse animation duration in ms, forwarded to every panel. |
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
- | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root `.display-accordian` element. |
40
-
41
- ---
42
-
43
- ## Usage examples
44
-
45
- ### Basic accordion (exclusive open)
46
-
47
- ```vue
48
- <AccordianCore name="faq" :item-count="3" :animation-duration="300">
49
- <template #accordian-0-summary><span>What is your returns policy?</span></template>
50
- <template #accordian-0-content>
51
- <p>You can return any item within 30 days of purchase.</p>
52
- </template>
53
-
54
- <template #accordian-1-summary><span>How long does delivery take?</span></template>
55
- <template #accordian-1-content>
56
- <p>Standard delivery takes 3–5 working days.</p>
57
- </template>
58
-
59
- <template #accordian-2-summary><span>Do you ship internationally?</span></template>
60
- <template #accordian-2-content>
61
- <p>Yes — we ship to over 40 countries.</p>
62
- </template>
63
- </AccordianCore>
64
- ```
65
-
66
- Passing `name="faq"` groups all panels so only one can be open at a time.
67
-
68
- ### Independent panels (no name — each opens freely)
69
-
70
- ```vue
71
- <AccordianCore :item-count="2">
72
- <template #accordian-0-summary><span>Panel A</span></template>
73
- <template #accordian-0-content><p>Content A</p></template>
74
-
75
- <template #accordian-1-summary><span>Panel B</span></template>
76
- <template #accordian-1-content><p>Content B</p></template>
77
- </AccordianCore>
78
- ```
79
-
80
- ### Custom icons per panel
81
-
82
- ```vue
83
- <AccordianCore :item-count="2">
84
- <template #accordian-0-summary><span>Section one</span></template>
85
- <template #accordian-0-icon><span>+</span></template>
86
- <template #accordian-0-content><p>Content one</p></template>
87
-
88
- <template #accordian-1-summary><span>Section two</span></template>
89
- <template #accordian-1-icon><span>+</span></template>
90
- <template #accordian-1-content><p>Content two</p></template>
91
- </AccordianCore>
92
- ```
93
-
94
- ### Modern variant (opt-in, not WebKit-safe)
95
-
96
- ```vue
97
- <AccordianCore name="faq" :item-count="2" variant="modern">
98
- <template #accordian-0-summary><span>Question one?</span></template>
99
- <template #accordian-0-content><p>Answer one.</p></template>
100
-
101
- <template #accordian-1-summary><span>Question two?</span></template>
102
- <template #accordian-1-content><p>Answer two.</p></template>
103
- </AccordianCore>
104
- ```
105
-
106
- ### Programmatic slot rendering (many items)
107
-
108
- ```vue
109
- <AccordianCore :item-count="items.length">
110
- <template v-for="(item, i) in items" #[`accordian-${i}-summary`] :key="i">
111
- <span>{{ item.title }}</span>
112
- </template>
113
- <template v-for="(item, i) in items" #[`accordian-${i}-content`] :key="i">
114
- <p>{{ item.body }}</p>
115
- </template>
116
- </AccordianCore>
117
- ```
118
-
119
- ---
120
-
121
- ## CSS custom properties
122
-
123
- `AccordianCore` has no `--accordian-*` tokens of its own — its panels are `ExpandingPanel`
124
- (or `ExpandingPanelClassic`, per `variant`) underneath, so `--expanding-panel-*` tokens apply
125
- directly. See `CONSUMER-STYLING.md` in the component directory for the full override reference,
126
- including the `.accordian-item` hook and the variant-dependent root class.
127
-
128
- Override in a consuming component or theme block:
129
-
130
- | Property | Effect |
131
- |----------|--------|
132
- | Applied via `accordian-item` class on each panel | Panels animate `margin-block-end` and `border-radius` alongside the expand transition — duration follows `animationDuration`. |
133
-
134
- ---
135
-
136
- ## Local style override scaffold
137
-
138
- When consuming this component, scaffold a style block using `styleClassPassthrough`. Delete the block if unused.
139
-
140
- See [component-local-style-override.md](../component-local-style-override.md) for the full pattern.
141
-
142
- ```vue
143
- <AccordianCore :style-class-passthrough="['my-accordian']" :item-count="3">
144
- ...
145
- </AccordianCore>
146
-
147
- <style>
148
- /* ─── AccordianCore local overrides ────────────────────────────────
149
- Colours, borders, geometry only — do not override behaviour.
150
- Delete this block if no overrides are needed.
151
- ─────────────────────────────────────────────────────────────────── */
152
- .display-accordian {
153
- &.my-accordian {
154
- /* Geometry */
155
- /* max-width: none; */ /* default is 600px — remove the width cap */
156
-
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
- /* Border */
162
- /* border-block-end: 1px solid currentColor; */
163
-
164
- /* Geometry */
165
- /* border-radius: 0.8rem; */
166
- }
167
- }
168
- }
169
- </style>
170
- ```
171
-
172
- ---
173
-
174
- ## Notes
175
-
176
- - `AccordianCore` always passes `style-class-passthrough="['accordian-item']"` to every inner panel — use `.accordian-item` as the hook for per-panel styling overrides.
177
- - `variant` applies to the whole group — every panel in a given `AccordianCore` renders as the same component. Switching `variant` at runtime swaps every panel's underlying component (via `<component :is>`), which resets each panel's open/closed state.
178
- - For a single standalone expand/collapse panel, use `ExpandingPanel` (or `ExpandingPanelClassic`) directly instead.
179
- - Auto-imported in Nuxt — no manual import needed.
180
- - File: `app/components/02.molecules/expandable/accordian/AccordianCore.vue`