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
@@ -66,7 +66,7 @@ watch(
66
66
  box-sizing: border-box;
67
67
  cursor: pointer;
68
68
  display: grid;
69
- grid-template-columns: auto 1fr auto;
69
+ grid-template-columns: auto minmax(0, 1fr) auto;
70
70
  align-items: center;
71
71
 
72
72
  width: 100%;
@@ -105,9 +105,12 @@ watch(
105
105
 
106
106
  .action-menu-item-label {
107
107
  grid-column: 2;
108
- white-space: nowrap;
108
+ display: -webkit-box;
109
+ -webkit-box-orient: vertical;
109
110
  overflow: hidden;
110
- text-overflow: ellipsis;
111
+ overflow-wrap: anywhere;
112
+ -webkit-line-clamp: var(--action-menu-item-label-line-clamp, none);
113
+ line-clamp: var(--action-menu-item-label-line-clamp, none);
111
114
  }
112
115
 
113
116
  .action-menu-item-arrow {
@@ -29,6 +29,7 @@ All `--action-menu-*` tokens are the stable override surface. Action menus repea
29
29
  | `--action-menu-popover-border` | `0.1rem solid var(--slate-03)` | Menu panel border shorthand |
30
30
  | `--action-menu-popover-border-radius` | `0.8rem` | Menu panel corner rounding |
31
31
  | `--action-menu-popover-min-width` | `20rem` | Minimum menu width |
32
+ | `--action-menu-popover-max-width` | `min(32rem, calc(100vw - 3.2rem))` | Maximum menu width; longer labels wrap |
32
33
  | `--action-menu-popover-shadow` | `0 0.4rem 1.6rem rgba(0, 0, 0, 0.1)` | Menu panel drop shadow |
33
34
  | `--action-menu-popover-transition-duration` | `200ms` | Open/close fade duration |
34
35
  | `--action-menu-popover-z-index` | `999999` | Stacking order in browsers without CSS anchor positioning (see Positioning). Ignored where the popover renders in the top layer |
@@ -45,7 +46,13 @@ All `--action-menu-*` tokens are the stable override surface. Action menus repea
45
46
  | `--action-menu-item-gap` | `1.2rem` | Space between icon, label and arrow |
46
47
  | `--action-menu-item-padding-inline` | `1.6rem` | Row horizontal padding |
47
48
  | `--action-menu-item-padding-block` | `1.2rem` | Row vertical padding |
49
+ | `--action-menu-item-label-line-clamp` | `none` | Caps the label at this many lines with an ellipsis (`1` is single-line ellipsis, `none` shows it all) |
48
50
 
51
+ > Changed 2026-10-20: labels used to stay on one line and widen the menu without limit, so a long
52
+ > label pushed it past a phone screen. The menu now stops at `--action-menu-popover-max-width` and
53
+ > labels wrap; set `--action-menu-item-label-line-clamp: 1` for the old single-line look with an
54
+ > ellipsis. `--action-menu-popover-max-width` and `--action-menu-item-label-line-clamp` are new.
55
+ >
49
56
  > Changed 2026-09-27: item `light-dark()` defaults replaced by their light values (older iPad
50
57
  > Safari lacks `light-dark()`), matching the already-light panel. Icon sizes are applied as
51
58
  > `font-size`; the old `width`/`height` on the trigger and arrow icons was silently overridden by
@@ -79,8 +86,8 @@ Inner classes: `.action-menu-trigger`, `.action-menu-trigger-icon`, `.action-men
79
86
  ## Positioning
80
87
 
81
88
  The menu uses the Popover API plus CSS anchor positioning: it opens below the trigger,
82
- right-aligned, and flips above near the bottom of the viewport (`position-try-fallbacks:
83
- flip-block`).
89
+ right-aligned, and flips above near the bottom of the viewport and to open rightwards near the left
90
+ edge (`position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline`).
84
91
 
85
92
  Fallbacks, via the shared `useAnchoredPopover` composable:
86
93
 
@@ -148,5 +155,5 @@ doesn't rely on it for its defaults.
148
155
  ## Notes
149
156
 
150
157
  - Negative `--action-menu-block-distance` values make the menu overlap the trigger.
151
- - `--action-menu-popover-min-width` is a floor; long labels widen the menu. Set
152
- `width: max-content` on `.action-menu-popover` to change that.
158
+ - `--action-menu-popover-min-width` is a floor and `--action-menu-popover-max-width` a ceiling;
159
+ between them the menu sizes to its longest label, and labels wrap at the ceiling.
@@ -2,11 +2,14 @@ import { computed, ref } from "vue";
2
2
  import type { Meta, StoryObj } from "@nuxtjs/storybook";
3
3
  import ActionMenu from "../ActionMenu.vue";
4
4
  import ActionMenuItem from "../ActionMenuItem.vue";
5
+ import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
6
+ import type { MediaCanvas } from "~/types/components";
5
7
 
6
8
  // ─── Meta ─────────────────────────────────────────────────────────────────────
7
9
 
8
10
  interface StoryArgs {
9
11
  itemCount?: number;
12
+ labelLineClamp?: string;
10
13
  label?: string;
11
14
  triggerIcon?: string;
12
15
  styleClassPassthrough?: string | string[];
@@ -17,8 +20,8 @@ const meta: Meta<StoryArgs> = {
17
20
  component: ActionMenu,
18
21
  argTypes: {
19
22
  itemCount: {
20
- control: { type: "number", min: 0, max: 8, step: 1 },
21
- description: "Number of items to show (story control — not a component prop)",
23
+ control: { type: "number", min: 0, max: 5, step: 1 },
24
+ description: "**Story control, not a prop.** Number of items to show (0 to 5, the size of this data set).",
22
25
  table: { category: "Content" },
23
26
  },
24
27
  label: {
@@ -31,6 +34,13 @@ const meta: Meta<StoryArgs> = {
31
34
  description: "Icon name for the trigger button",
32
35
  table: { category: "Appearance" },
33
36
  },
37
+ labelLineClamp: {
38
+ control: { type: "select" },
39
+ options: ["none", "1", "2", "3"],
40
+ description:
41
+ "**Story control, not a prop.** Sets `--action-menu-item-label-line-clamp`. In your CSS: `.my-page { --action-menu-item-label-line-clamp: 1; }`",
42
+ table: { category: "CSS tokens (story only, set in your CSS)" },
43
+ },
34
44
  styleClassPassthrough: {
35
45
  table: { disable: true },
36
46
  },
@@ -38,6 +48,7 @@ const meta: Meta<StoryArgs> = {
38
48
  args: {
39
49
  itemCount: 5,
40
50
  label: "Open actions menu",
51
+ labelLineClamp: "none",
41
52
  triggerIcon: "lucide:ellipsis",
42
53
  },
43
54
  parameters: {
@@ -67,6 +78,17 @@ const actionItems = [
67
78
  { label: "Delete", icon: "lucide:trash-2", href: undefined },
68
79
  ] as const;
69
80
 
81
+ function useStorySetup(args: StoryArgs) {
82
+ const componentArgs = computed(() => {
83
+ const { itemCount: _itemCount, labelLineClamp: _labelLineClamp, ...rest } = args;
84
+ return rest;
85
+ });
86
+ const tokenStyle = computed(() => ({
87
+ "--action-menu-item-label-line-clamp": args.labelLineClamp || undefined,
88
+ }));
89
+ return { args, componentArgs, tokenStyle };
90
+ }
91
+
70
92
  // ─── Stories ──────────────────────────────────────────────────────────────────
71
93
 
72
94
  /**
@@ -81,13 +103,7 @@ export const Default: Story = {
81
103
  render: (args) => ({
82
104
  components: { ActionMenu, ActionMenuItem },
83
105
  setup() {
84
- // `args` is Storybook's own reactive object — read from it directly (`args.x`) inside
85
- // these computeds rather than destructuring it into local variables at setup-time, which
86
- // would snapshot the values once and stop reflecting later Controls-panel changes.
87
- const componentArgs = computed(() => {
88
- const { itemCount, ...rest } = args;
89
- return rest;
90
- });
106
+ const { componentArgs, tokenStyle } = useStorySetup(args);
91
107
  const lastAction = ref<string | null>(null);
92
108
  const items = computed(() =>
93
109
  actionItems.slice(0, args.itemCount ?? 5).map((item, i) => ({
@@ -95,11 +111,11 @@ export const Default: Story = {
95
111
  slotName: `item-${i}`,
96
112
  }))
97
113
  );
98
- return { componentArgs, items, lastAction };
114
+ return { componentArgs, tokenStyle, items, lastAction };
99
115
  },
100
116
  template: `
101
117
  <div style="padding: 4rem 8rem; display: flex; flex-direction: column; align-items: flex-end; gap: 2rem;">
102
- <ActionMenu v-bind="componentArgs">
118
+ <ActionMenu v-bind="componentArgs" :style="tokenStyle">
103
119
  <template v-for="item in items" :key="item.slotName" #[item.slotName]>
104
120
  <ActionMenuItem :label="item.label" @click="lastAction = item.label">
105
121
  <template #icon>
@@ -128,11 +144,11 @@ export const MixedItems: Story = {
128
144
  render: (args) => ({
129
145
  components: { ActionMenu, ActionMenuItem },
130
146
  setup() {
131
- return { args };
147
+ return useStorySetup(args);
132
148
  },
133
149
  template: `
134
150
  <div style="padding: 4rem 8rem; display: flex; justify-content: flex-end;">
135
- <ActionMenu v-bind="args">
151
+ <ActionMenu v-bind="componentArgs" :style="tokenStyle">
136
152
  <template #item-0>
137
153
  <ActionMenuItem label="Edit record" @click="() => {}">
138
154
  <template #icon><Icon name="lucide:pencil" /></template>
@@ -168,11 +184,11 @@ export const NoIcons: Story = {
168
184
  render: (args) => ({
169
185
  components: { ActionMenu, ActionMenuItem },
170
186
  setup() {
171
- return { args };
187
+ return useStorySetup(args);
172
188
  },
173
189
  template: `
174
190
  <div style="padding: 4rem 8rem; display: flex; justify-content: flex-end;">
175
- <ActionMenu v-bind="args">
191
+ <ActionMenu v-bind="componentArgs" :style="tokenStyle">
176
192
  <template #item-0>
177
193
  <ActionMenuItem label="Approve" @click="() => {}" />
178
194
  </template>
@@ -206,7 +222,7 @@ export const InContext: Story = {
206
222
  { name: "Sam Patel", email: "sam@example.com", role: "Editor" },
207
223
  { name: "Jordan Clarke", email: "jordan@example.com", role: "Viewer" },
208
224
  ];
209
- return { args, users };
225
+ return { ...useStorySetup(args), users };
210
226
  },
211
227
  template: `
212
228
  <div style="padding: 4rem; max-width: 56rem; margin: 0 auto;">
@@ -234,7 +250,7 @@ export const InContext: Story = {
234
250
  flex-shrink: 0;
235
251
  "
236
252
  >
237
- {{ user.name[0] }}
253
+ {{ Array.from(user.name)[0] }}
238
254
  </div>
239
255
  <div style="flex: 1; min-width: 0;">
240
256
  <p style="margin: 0; font-size: 1.4rem; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">
@@ -245,7 +261,7 @@ export const InContext: Story = {
245
261
  </p>
246
262
  </div>
247
263
  <span style="font-size: 1.2rem; opacity: 0.5; white-space: nowrap;">{{ user.role }}</span>
248
- <ActionMenu v-bind="args">
264
+ <ActionMenu v-bind="componentArgs" :style="tokenStyle">
249
265
  <template #item-0>
250
266
  <ActionMenuItem label="Edit user" @click="() => {}">
251
267
  <template #icon><Icon name="lucide:pencil" /></template>
@@ -337,3 +353,99 @@ export const ItemCoreStandalone: Story = {
337
353
  `,
338
354
  }),
339
355
  };
356
+
357
+ /**
358
+ * Stress test: hostile labels and item counts, with triggers at both inline edges.
359
+ */
360
+ export const StressTest: Story = {
361
+ name: "Stress Test (Worst-Case Data)",
362
+ args: {
363
+ label: "Aktionen für diesen Eintrag öffnen",
364
+ },
365
+ render: (args) => ({
366
+ components: { ActionMenu, ActionMenuItem },
367
+ setup() {
368
+ const hostileLabels = [
369
+ "Donaudampfschifffahrtselektrizitätenhauptbetriebswerkbauunterbeamtengesellschaft",
370
+ "https://example.com/" + "a-very-long-path-segment-without-spaces".repeat(3),
371
+ "Diesen Eintrag dauerhaft löschen, einschließlich aller zugehörigen Anhänge und Kommentare",
372
+ "🎉 Celebrate",
373
+ "<b>Not bold</b>",
374
+ "حذف هذا السجل نهائيا",
375
+ "A",
376
+ ];
377
+ const manyItems = Array.from({ length: 12 }, (_, i) => `Action ${i + 1}`);
378
+ return { ...useStorySetup(args), hostileLabels, manyItems };
379
+ },
380
+ template: `
381
+ <div style="padding: 2.4rem 1.6rem; display: grid; gap: 4rem;">
382
+ <div style="display: flex; justify-content: space-between; align-items: center; gap: 1.6rem;">
383
+ <ActionMenu v-bind="componentArgs" :style="tokenStyle">
384
+ <template v-for="(text, i) in hostileLabels" :key="i" #[\`item-\${i}\`]>
385
+ <ActionMenuItem :label="text" @click="() => {}">
386
+ <template #icon><Icon name="lucide:pencil" /></template>
387
+ </ActionMenuItem>
388
+ </template>
389
+ </ActionMenu>
390
+ <span>Hostile labels: left-edge trigger and right-edge trigger</span>
391
+ <ActionMenu v-bind="componentArgs" :style="tokenStyle">
392
+ <template v-for="(text, i) in hostileLabels" :key="i" #[\`item-\${i}\`]>
393
+ <ActionMenuItem :label="text" href="https://example.com"></ActionMenuItem>
394
+ </template>
395
+ </ActionMenu>
396
+ </div>
397
+
398
+ <div style="display: flex; justify-content: space-between; align-items: center; gap: 1.6rem;">
399
+ <span>Twelve items</span>
400
+ <ActionMenu v-bind="componentArgs" :style="tokenStyle">
401
+ <template v-for="(text, i) in manyItems" :key="i" #[\`item-\${i}\`]>
402
+ <ActionMenuItem :label="text" @click="() => {}"></ActionMenuItem>
403
+ </template>
404
+ </ActionMenu>
405
+ </div>
406
+
407
+ <div style="display: flex; justify-content: space-between; align-items: center; gap: 1.6rem;">
408
+ <span>One item</span>
409
+ <ActionMenu v-bind="componentArgs" :style="tokenStyle">
410
+ <template #item-0><ActionMenuItem label="Only action" @click="() => {}"></ActionMenuItem></template>
411
+ </ActionMenu>
412
+ </div>
413
+
414
+ <div style="display: flex; justify-content: space-between; align-items: center; gap: 1.6rem;">
415
+ <span>No items (opens an empty panel, nothing focusable)</span>
416
+ <ActionMenu v-bind="componentArgs" :style="tokenStyle"></ActionMenu>
417
+ </div>
418
+ </div>
419
+ `,
420
+ }),
421
+ decorators: [
422
+ (story, context) => ({
423
+ components: { story, CanvasSwitcher },
424
+ setup() {
425
+ const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "mobileCanvas");
426
+ return { canvasName };
427
+ },
428
+ template: `
429
+ <div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
430
+ <CanvasSwitcher v-model:canvas-name="canvasName" />
431
+ </div>
432
+ <div :class="canvasName" style="margin-inline: auto; outline: 1px dashed currentColor;">
433
+ <story />
434
+ </div>
435
+ `,
436
+ }),
437
+ ],
438
+ parameters: {
439
+ initialCanvas: "mobileCanvas",
440
+ docs: {
441
+ story: { inline: false, height: "640px" },
442
+ description: {
443
+ story:
444
+ "Open each menu at every canvas width. Long unbroken words and URLs should wrap inside the menu, which never grows past " +
445
+ "`--action-menu-popover-max-width` (32rem, or the viewport less 3.2rem). The left-edge menu should flip to open rightwards " +
446
+ "rather than off-screen (CSS anchor positioning browsers). Emoji and HTML-like text render as text, the Arabic label wraps like the rest, " +
447
+ "twelve items stay keyboard-navigable, and the empty menu opens without errors. Try the line-clamp control to cap labels at 1 to 3 lines.",
448
+ },
449
+ },
450
+ },
451
+ };
@@ -0,0 +1,227 @@
1
+ <template>
2
+ <div class="accordion-chat" :class="elementClasses">
3
+ <ExpandingPanelClassic
4
+ v-for="index in panelIndexes()"
5
+ :key="index"
6
+ :name
7
+ :animation-duration="animationDuration"
8
+ :style-class-passthrough="['accordion-chat-item']"
9
+ >
10
+ <template #summary>
11
+ <slot :name="`item-${index}-summary`"></slot>
12
+ </template>
13
+ <template #icon>
14
+ <slot :name="`item-${index}-icon`">
15
+ <span class="accordion-chat-icon" aria-hidden="true">
16
+ <Icon name="lucide:plus" class="accordion-chat-icon-glyph" />
17
+ </span>
18
+ </slot>
19
+ </template>
20
+ <template #content>
21
+ <div v-if="slots[`item-${index}-content`]" class="accordion-chat-answer">
22
+ <span v-if="hasAvatar(index)" class="accordion-chat-avatar" aria-hidden="true">
23
+ <slot :name="`item-${index}-avatar`">
24
+ <slot name="avatar">{{ avatarInitials }}</slot>
25
+ </slot>
26
+ </span>
27
+ <div class="accordion-chat-bubble">
28
+ <slot :name="`item-${index}-content`"></slot>
29
+ </div>
30
+ </div>
31
+ </template>
32
+ </ExpandingPanelClassic>
33
+ </div>
34
+ </template>
35
+
36
+ <script setup lang="ts">
37
+ import ExpandingPanelClassic from "../expanding-panel-classic/ExpandingPanelClassic.vue";
38
+
39
+ interface Props {
40
+ name?: string;
41
+ animationDuration?: number;
42
+ avatarLabel?: string;
43
+ showAvatar?: boolean;
44
+ styleClassPassthrough?: string | string[];
45
+ }
46
+
47
+ const props = withDefaults(defineProps<Props>(), {
48
+ name: undefined,
49
+ animationDuration: 300,
50
+ avatarLabel: "",
51
+ showAvatar: true,
52
+ styleClassPassthrough: () => [],
53
+ });
54
+
55
+ const slots = useSlots();
56
+ const SUMMARY_SLOT = /^item-(\d+)-summary$/;
57
+
58
+ const panelIndexes = () =>
59
+ Object.keys(slots)
60
+ .map((slotName) => SUMMARY_SLOT.exec(slotName)?.[1])
61
+ .filter((index): index is string => index !== undefined)
62
+ .map(Number)
63
+ .sort((a, b) => a - b);
64
+
65
+ const toGraphemes = (text: string) =>
66
+ typeof Intl.Segmenter === "function"
67
+ ? Array.from(new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(text), ({ segment }) => segment)
68
+ : Array.from(text);
69
+
70
+ const avatarInitials = computed(() => toGraphemes(props.avatarLabel.trim()).slice(0, 2).join(""));
71
+
72
+ const hasAvatar = (index: number) =>
73
+ props.showAvatar && Boolean(slots[`item-${index}-avatar`] || slots.avatar || avatarInitials.value);
74
+
75
+ const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
76
+
77
+ watch(
78
+ () => props.styleClassPassthrough,
79
+ () => {
80
+ resetElementClasses(props.styleClassPassthrough);
81
+ }
82
+ );
83
+ </script>
84
+
85
+ <style lang="css">
86
+ @layer components {
87
+ .accordion-chat {
88
+ --_accordion-chat-radius: var(--accordion-chat-radius, 2rem);
89
+ --_accordion-chat-tail-radius: var(--accordion-chat-tail-radius, 0.6rem);
90
+ --_accordion-chat-bubble-max-width: var(--accordion-chat-bubble-max-width, 82%);
91
+ --_accordion-chat-duration: var(--accordion-chat-transition-duration, 200ms);
92
+
93
+ display: grid;
94
+ gap: var(--accordion-chat-gap, 1.4rem);
95
+
96
+ .accordion-chat-item.expanding-panel-classic {
97
+ --expanding-panel-classic-icon-size: var(--accordion-chat-icon-size, 2.2rem);
98
+ --expanding-panel-classic-summary-gap: var(--accordion-chat-question-icon-gap, 1.4rem);
99
+
100
+ .expanding-panel-classic-summary {
101
+ inline-size: fit-content;
102
+ max-inline-size: var(--_accordion-chat-bubble-max-width);
103
+ margin-inline-start: auto;
104
+ padding-block: var(--accordion-chat-question-padding-block, 1.3rem);
105
+ padding-inline: var(--accordion-chat-question-padding-inline, 2rem 1.8rem);
106
+ border: var(--accordion-chat-question-border-width, 0.1rem) solid
107
+ var(--accordion-chat-question-border, transparent);
108
+ border-radius: var(--_accordion-chat-radius);
109
+ border-end-end-radius: var(--_accordion-chat-tail-radius);
110
+ background-color: var(--accordion-chat-question-surface, var(--theme-surface-subtle));
111
+ color: var(--accordion-chat-question-colour, var(--theme-text));
112
+ font-family: var(--accordion-chat-question-font-family, inherit);
113
+ font-size: var(--accordion-chat-question-font-size, 1.6rem);
114
+ font-weight: var(--accordion-chat-question-font-weight, 600);
115
+ line-height: var(--accordion-chat-question-line-height, 1.3);
116
+ transition:
117
+ background-color var(--_accordion-chat-duration),
118
+ color var(--_accordion-chat-duration),
119
+ border-color var(--_accordion-chat-duration);
120
+
121
+ &:focus-visible {
122
+ outline: var(--accordion-chat-focus-ring-width, 0.2rem) solid
123
+ var(--accordion-chat-focus-ring-colour, var(--theme-border-focus));
124
+ outline-offset: 0.2rem;
125
+ }
126
+
127
+ .expanding-panel-classic-details[open] & {
128
+ border-color: var(--accordion-chat-question-border-open, var(--accordion-chat-question-border, transparent));
129
+ background-color: var(--accordion-chat-question-surface-open, var(--theme-surface-inverted));
130
+ color: var(--accordion-chat-question-colour-open, var(--theme-text-inverted));
131
+ }
132
+
133
+ .accordion-chat-icon {
134
+ display: grid;
135
+ place-items: center;
136
+ inline-size: 100%;
137
+ aspect-ratio: 1;
138
+ border-radius: 50%;
139
+ background-color: var(--accordion-chat-icon-surface, rgb(0 0 0 / 0.07));
140
+
141
+ .expanding-panel-classic-details[open] & {
142
+ background-color: var(--accordion-chat-icon-surface-open, rgb(255 255 255 / 0.16));
143
+ rotate: 45deg;
144
+ }
145
+
146
+ @media (prefers-reduced-motion: no-preference) {
147
+ transition:
148
+ background-color var(--_accordion-chat-duration),
149
+ rotate calc(var(--_accordion-chat-duration) * 1.25);
150
+ }
151
+
152
+ .accordion-chat-icon-glyph {
153
+ display: block;
154
+ font-size: var(--accordion-chat-icon-glyph-size, 1.4rem);
155
+ }
156
+ }
157
+ }
158
+ }
159
+
160
+ .accordion-chat-answer {
161
+ display: flex;
162
+ align-items: flex-end;
163
+ gap: var(--accordion-chat-avatar-gap, 1rem);
164
+ padding-block-start: var(--accordion-chat-answer-gap, 1rem);
165
+
166
+ .accordion-chat-avatar {
167
+ flex: none;
168
+ display: grid;
169
+ place-items: center;
170
+ inline-size: var(--accordion-chat-avatar-size, 3rem);
171
+ aspect-ratio: 1;
172
+ overflow: hidden;
173
+ border-radius: 50%;
174
+ background-color: var(--accordion-chat-avatar-surface, var(--theme-surface-inverted));
175
+ color: var(--accordion-chat-avatar-colour, var(--theme-text-inverted));
176
+ font-family: var(--accordion-chat-avatar-font-family, inherit);
177
+ font-size: var(--accordion-chat-avatar-font-size, 1.2rem);
178
+ font-weight: var(--accordion-chat-avatar-font-weight, 600);
179
+ line-height: 1;
180
+ white-space: nowrap;
181
+
182
+ & :where(img, svg) {
183
+ inline-size: 100%;
184
+ block-size: 100%;
185
+ object-fit: cover;
186
+ }
187
+ }
188
+
189
+ .accordion-chat-bubble {
190
+ min-inline-size: 0;
191
+ max-inline-size: var(--_accordion-chat-bubble-max-width);
192
+ padding-block: var(--accordion-chat-answer-padding-block, 1.4rem);
193
+ padding-inline: var(--accordion-chat-answer-padding-inline, 1.8rem);
194
+ border: var(--accordion-chat-answer-border-width, 0.1rem) solid
195
+ var(--accordion-chat-answer-border, var(--theme-surface-subtle));
196
+ border-radius: var(--_accordion-chat-radius);
197
+ border-end-start-radius: var(--_accordion-chat-tail-radius);
198
+ background-color: var(--accordion-chat-answer-surface, var(--theme-text-inverted));
199
+ color: var(--accordion-chat-answer-colour, var(--theme-text));
200
+ font-family: var(--accordion-chat-answer-font-family, inherit);
201
+ font-size: var(--accordion-chat-answer-font-size, 1.55rem);
202
+ font-weight: var(--accordion-chat-answer-font-weight, 400);
203
+ line-height: var(--accordion-chat-answer-line-height, 1.6);
204
+
205
+ & :where(img, video) {
206
+ max-inline-size: 100%;
207
+ block-size: auto;
208
+ }
209
+
210
+ @media (prefers-reduced-motion: no-preference) {
211
+ .expanding-panel-classic-details[open] + .expanding-panel-classic-content & {
212
+ animation: accordion-chat-answer-in var(--accordion-chat-entrance-duration, 260ms) ease-out both;
213
+ }
214
+ }
215
+ }
216
+ }
217
+ }
218
+
219
+ @keyframes accordion-chat-answer-in {
220
+ from {
221
+ opacity: 0;
222
+ translate: 0 0.6rem;
223
+ scale: 0.98;
224
+ }
225
+ }
226
+ }
227
+ </style>