@ds-mo/ui 12.0.0 → 12.2.0

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 (253) hide show
  1. package/README.md +18 -1
  2. package/dist/.build-stamp +1 -1
  3. package/dist/agent-patterns.json +3 -2
  4. package/dist/agent.json +314 -41
  5. package/dist/angular/ds-avatar.d.ts +1 -1
  6. package/dist/angular/ds-avatar.js +3 -3
  7. package/dist/angular/ds-bar-workflow.d.ts +28 -0
  8. package/dist/angular/ds-bar-workflow.js +48 -0
  9. package/dist/angular/ds-conversation-list.d.ts +1 -1
  10. package/dist/angular/ds-conversation-list.js +4 -3
  11. package/dist/angular/ds-scroll-overlay.d.ts +19 -0
  12. package/dist/angular/ds-scroll-overlay.js +43 -0
  13. package/dist/angular/index.d.ts +1 -1
  14. package/dist/angular/index.js +2 -0
  15. package/dist/angular/proxies.d.ts +2 -0
  16. package/dist/angular/proxies.js +2 -0
  17. package/dist/components/ds-agent-activity.js +1 -1
  18. package/dist/components/ds-agent-response.js +1 -1
  19. package/dist/components/ds-agent-source-list.js +1 -1
  20. package/dist/components/ds-agent-tool-call.js +1 -1
  21. package/dist/components/ds-attachment-list.js +1 -1
  22. package/dist/components/ds-avatar.js +1 -1
  23. package/dist/components/ds-avatar.js.map +1 -1
  24. package/dist/components/ds-badge.js +1 -1
  25. package/dist/components/ds-bar-nav.js +1 -1
  26. package/dist/components/ds-bar-nav.js.map +1 -1
  27. package/dist/components/ds-bar-title.js +1 -1
  28. package/dist/components/ds-bar-title.js.map +1 -1
  29. package/dist/components/ds-bar-workflow.d.ts +11 -0
  30. package/dist/components/ds-bar-workflow.js +2 -0
  31. package/dist/components/ds-bar-workflow.js.map +1 -0
  32. package/dist/components/ds-button-filled.js +1 -1
  33. package/dist/components/ds-button-unfilled.js +1 -1
  34. package/dist/components/ds-card-data-viz-donut.js +1 -1
  35. package/dist/components/ds-card-data-viz-donut.js.map +1 -1
  36. package/dist/components/ds-card-setting.js +1 -1
  37. package/dist/components/ds-card-setting.js.map +1 -1
  38. package/dist/components/ds-card-shell-data-viz.js +1 -1
  39. package/dist/components/ds-chart-bar.js +1 -1
  40. package/dist/components/ds-chart-donut.js +1 -1
  41. package/dist/components/ds-chart-legend.js +1 -1
  42. package/dist/components/ds-chart-line.js +1 -1
  43. package/dist/components/ds-checkbox.js +1 -1
  44. package/dist/components/ds-chip.js +1 -1
  45. package/dist/components/ds-chip.js.map +1 -1
  46. package/dist/components/ds-code-block.js +1 -1
  47. package/dist/components/ds-conversation-list-item.js +1 -1
  48. package/dist/components/ds-conversation-list-item.js.map +1 -1
  49. package/dist/components/ds-conversation-list-section.js +1 -1
  50. package/dist/components/ds-conversation-list-section.js.map +1 -1
  51. package/dist/components/ds-conversation-list.js +1 -1
  52. package/dist/components/ds-conversation-list.js.map +1 -1
  53. package/dist/components/ds-divider.js +1 -1
  54. package/dist/components/ds-empty-state.js +1 -1
  55. package/dist/components/ds-field.js +1 -1
  56. package/dist/components/ds-icon.js +1 -1
  57. package/dist/components/ds-input.js +1 -1
  58. package/dist/components/ds-input.js.map +1 -1
  59. package/dist/components/ds-markdown.js +1 -1
  60. package/dist/components/ds-menu.js +1 -1
  61. package/dist/components/ds-message-bubble.js +1 -1
  62. package/dist/components/ds-message-bubble.js.map +1 -1
  63. package/dist/components/ds-message-composer.js +1 -1
  64. package/dist/components/ds-message-composer.js.map +1 -1
  65. package/dist/components/ds-message-scroller.js +1 -1
  66. package/dist/components/ds-message-scroller.js.map +1 -1
  67. package/dist/components/ds-message.js +1 -1
  68. package/dist/components/ds-modal.js +1 -1
  69. package/dist/components/ds-panel-nav.js +1 -1
  70. package/dist/components/ds-panel-sub-nav.js +1 -1
  71. package/dist/components/ds-panel-sub-nav.js.map +1 -1
  72. package/dist/components/ds-panel-tool-header.js +1 -1
  73. package/dist/components/ds-panel-tool-search.js +1 -1
  74. package/dist/components/ds-panel-tools.js +1 -1
  75. package/dist/components/ds-radio.js +1 -1
  76. package/dist/components/ds-radio.js.map +1 -1
  77. package/dist/components/ds-scroll-overlay.d.ts +11 -0
  78. package/dist/components/ds-scroll-overlay.js +2 -0
  79. package/dist/components/ds-scroll-overlay.js.map +1 -0
  80. package/dist/components/ds-select-multi.js +1 -1
  81. package/dist/components/ds-select-multi.js.map +1 -1
  82. package/dist/components/ds-select.js +1 -1
  83. package/dist/components/ds-select.js.map +1 -1
  84. package/dist/components/ds-shell-app.js +1 -1
  85. package/dist/components/ds-shell-gradient-picker.js +1 -1
  86. package/dist/components/ds-shell-gradient-swatch.js +1 -1
  87. package/dist/components/ds-shell-gradient-swatch.js.map +1 -1
  88. package/dist/components/ds-shell-page.js +1 -1
  89. package/dist/components/ds-shell-page.js.map +1 -1
  90. package/dist/components/ds-skeleton.js +1 -1
  91. package/dist/components/ds-slider.js +1 -1
  92. package/dist/components/ds-slider.js.map +1 -1
  93. package/dist/components/ds-swatch-picker.js +1 -1
  94. package/dist/components/ds-switch.js +1 -1
  95. package/dist/components/ds-tab-group.js +1 -1
  96. package/dist/components/ds-tab-group.js.map +1 -1
  97. package/dist/components/ds-tag.js +1 -1
  98. package/dist/components/ds-tag.js.map +1 -1
  99. package/dist/components/ds-text.js +1 -1
  100. package/dist/components/ds-toast.js +1 -1
  101. package/dist/components/ds-tooltip-data-viz.js +1 -1
  102. package/dist/components/ds-tooltip.js +1 -1
  103. package/dist/components/ds-typing-indicator.js +1 -1
  104. package/dist/components/p-2W-ZjZCR.js +2 -0
  105. package/dist/components/{p-CwTVEAl2.js.map → p-2W-ZjZCR.js.map} +1 -1
  106. package/dist/components/p-8OOCLGph.js +2 -0
  107. package/dist/components/p-8OOCLGph.js.map +1 -0
  108. package/dist/components/{p-BYHKdFzV.js → p-B8CubPgQ.js} +2 -2
  109. package/dist/components/{p-DuNZGFkV.js → p-BAfG3M8z.js} +2 -2
  110. package/dist/components/p-BAfG3M8z.js.map +1 -0
  111. package/dist/components/p-BE_CDJeL.js +2 -0
  112. package/dist/components/p-BE_CDJeL.js.map +1 -0
  113. package/dist/components/{p-BH3es4p7.js → p-BRLDJmkh.js} +2 -2
  114. package/dist/components/p-BpA9uYKX.js +2 -0
  115. package/dist/components/p-BpA9uYKX.js.map +1 -0
  116. package/dist/components/{p-Bs6SS34m.js → p-C0_8K4y7.js} +2 -2
  117. package/dist/components/{p-BhKhX1er.js → p-CA3kLO7K.js} +2 -2
  118. package/dist/components/{p-DfIch2Dk.js → p-CCHqsF_H.js} +2 -2
  119. package/dist/components/p-CG5EX4NW.js +2 -0
  120. package/dist/components/{p-DABj3qTX.js.map → p-CG5EX4NW.js.map} +1 -1
  121. package/dist/components/{p-DVk0wxxX.js → p-CHkI3oIS.js} +2 -2
  122. package/dist/components/{p-liMPkhUP.js → p-CNffejay.js} +2 -2
  123. package/dist/components/{p-B3YvoGjS.js → p-Cxpl-rzP.js} +2 -2
  124. package/dist/components/p-CydY-v7X.js +2 -0
  125. package/dist/components/p-CydY-v7X.js.map +1 -0
  126. package/dist/components/{p-BkwBtZs3.js → p-D6jK2Hyi.js} +2 -2
  127. package/dist/components/{p-Cy04lXlc.js → p-DBi1bmOL.js} +2 -2
  128. package/dist/components/{p-DxeHimqk.js → p-DOCKtrS4.js} +2 -2
  129. package/dist/components/p-DbpfpeQT.js +2 -0
  130. package/dist/components/p-DbpfpeQT.js.map +1 -0
  131. package/dist/components/{p-6vj4kh70.js → p-DcGpdIlT.js} +2 -2
  132. package/dist/components/p-DyGB97Oy.js +2 -0
  133. package/dist/components/{p-Dy_sZ-dI.js.map → p-DyGB97Oy.js.map} +1 -1
  134. package/dist/components/p-GMDGgtY8.js +2 -0
  135. package/dist/components/{p-BUmDHPr3.js.map → p-GMDGgtY8.js.map} +1 -1
  136. package/dist/components/{p-Cf2J9MEo.js → p-XgIQ3oI_.js} +2 -2
  137. package/dist/components/{p-Cf-i-6aE.js → p-ky1msbJ8.js} +2 -2
  138. package/dist/components/p-xY3TjBF5.js +2 -0
  139. package/dist/components/{p-DUHcJWTQ.js.map → p-xY3TjBF5.js.map} +1 -1
  140. package/dist/docs/components.json +685 -56
  141. package/dist/lib/utils/anchored-popup.d.ts +5 -1
  142. package/dist/mcp-data/registry/agent-activity.json +1 -1
  143. package/dist/mcp-data/registry/agent-response.json +10 -5
  144. package/dist/mcp-data/registry/agent-source-list.json +1 -1
  145. package/dist/mcp-data/registry/agent-tool-call.json +1 -1
  146. package/dist/mcp-data/registry/attachment-list.json +1 -1
  147. package/dist/mcp-data/registry/avatar.json +27 -8
  148. package/dist/mcp-data/registry/badge.json +1 -1
  149. package/dist/mcp-data/registry/bar-nav.json +1 -1
  150. package/dist/mcp-data/registry/bar-title.json +13 -8
  151. package/dist/mcp-data/registry/bar-workflow.json +368 -0
  152. package/dist/mcp-data/registry/button-filled.json +2 -2
  153. package/dist/mcp-data/registry/button-unfilled.json +2 -2
  154. package/dist/mcp-data/registry/card-data-viz-donut.json +1 -1
  155. package/dist/mcp-data/registry/card-setting.json +1 -1
  156. package/dist/mcp-data/registry/card-shell-data-viz.json +1 -1
  157. package/dist/mcp-data/registry/chart-bar.json +1 -1
  158. package/dist/mcp-data/registry/chart-donut.json +1 -1
  159. package/dist/mcp-data/registry/chart-legend.json +1 -1
  160. package/dist/mcp-data/registry/chart-line.json +1 -1
  161. package/dist/mcp-data/registry/checkbox.json +2 -2
  162. package/dist/mcp-data/registry/chip.json +1 -1
  163. package/dist/mcp-data/registry/code-block.json +1 -1
  164. package/dist/mcp-data/registry/conversation-list-item.json +2 -2
  165. package/dist/mcp-data/registry/conversation-list-section.json +3 -3
  166. package/dist/mcp-data/registry/conversation-list.json +37 -10
  167. package/dist/mcp-data/registry/divider.json +1 -1
  168. package/dist/mcp-data/registry/empty-state.json +1 -1
  169. package/dist/mcp-data/registry/field.json +1 -1
  170. package/dist/mcp-data/registry/icon.json +2 -2
  171. package/dist/mcp-data/registry/input.json +1 -1
  172. package/dist/mcp-data/registry/loader.json +1 -1
  173. package/dist/mcp-data/registry/markdown.json +16 -11
  174. package/dist/mcp-data/registry/menu.json +10 -8
  175. package/dist/mcp-data/registry/message-bubble.json +1 -1
  176. package/dist/mcp-data/registry/message-composer.json +1 -1
  177. package/dist/mcp-data/registry/message-scroller.json +4 -4
  178. package/dist/mcp-data/registry/message.json +1 -1
  179. package/dist/mcp-data/registry/modal.json +1 -1
  180. package/dist/mcp-data/registry/panel-nav.json +1 -1
  181. package/dist/mcp-data/registry/panel-sub-nav.json +1 -1
  182. package/dist/mcp-data/registry/panel-tool-header.json +1 -1
  183. package/dist/mcp-data/registry/panel-tool-search.json +2 -2
  184. package/dist/mcp-data/registry/panel-tools.json +1 -1
  185. package/dist/mcp-data/registry/radio.json +1 -1
  186. package/dist/mcp-data/registry/registry.json +320 -41
  187. package/dist/mcp-data/registry/scroll-overlay.json +183 -0
  188. package/dist/mcp-data/registry/select-multi.json +1 -1
  189. package/dist/mcp-data/registry/select.json +1 -1
  190. package/dist/mcp-data/registry/shell-app.json +1 -1
  191. package/dist/mcp-data/registry/shell-gradient-picker.json +1 -1
  192. package/dist/mcp-data/registry/shell-gradient-swatch.json +2 -2
  193. package/dist/mcp-data/registry/shell-page.json +15 -11
  194. package/dist/mcp-data/registry/skeleton.json +1 -1
  195. package/dist/mcp-data/registry/slider.json +2 -2
  196. package/dist/mcp-data/registry/swatch-picker.json +1 -1
  197. package/dist/mcp-data/registry/switch.json +2 -2
  198. package/dist/mcp-data/registry/tab-group.json +2 -2
  199. package/dist/mcp-data/registry/tag.json +1 -1
  200. package/dist/mcp-data/registry/text.json +1 -1
  201. package/dist/mcp-data/registry/toast.json +1 -1
  202. package/dist/mcp-data/registry/tooltip-data-viz.json +1 -1
  203. package/dist/mcp-data/registry/tooltip.json +1 -1
  204. package/dist/mcp-data/registry/typing-indicator.json +1 -1
  205. package/dist/react/components.d.ts +2 -0
  206. package/dist/react/components.js +2 -0
  207. package/dist/react/ds-bar-workflow.d.ts +14 -0
  208. package/dist/react/ds-bar-workflow.js +16 -0
  209. package/dist/react/ds-scroll-overlay.d.ts +12 -0
  210. package/dist/react/ds-scroll-overlay.js +12 -0
  211. package/dist/styles/prose.css +214 -0
  212. package/dist/types/components/Avatar/Avatar.d.ts +4 -1
  213. package/dist/types/components/BarTitle/BarTitle.d.ts +2 -0
  214. package/dist/types/components/BarWorkflow/BarWorkflow.d.ts +37 -0
  215. package/dist/types/components/BarWorkflow/bar-workflow-types.d.ts +12 -0
  216. package/dist/types/components/ConversationList/ConversationList.d.ts +12 -0
  217. package/dist/types/components/Menu/Menu.d.ts +1 -2
  218. package/dist/types/components/MessageScroller/MessageScroller.d.ts +1 -6
  219. package/dist/types/components/ScrollOverlay/ScrollOverlay.d.ts +28 -0
  220. package/dist/types/components/ShellPage/ShellPage.d.ts +5 -0
  221. package/dist/types/components.d.ts +231 -3
  222. package/dist/types/stories/isolated-overlay-docs.d.ts +13 -0
  223. package/dist/types/utils/anchored-popup.d.ts +5 -1
  224. package/dist/types/utils/scroll-overlay-controller.d.ts +35 -0
  225. package/package.json +6 -5
  226. package/dist/components/p-BUmDHPr3.js +0 -2
  227. package/dist/components/p-BZZwQfmN.js +0 -2
  228. package/dist/components/p-BZZwQfmN.js.map +0 -1
  229. package/dist/components/p-CUik7PCi.js +0 -2
  230. package/dist/components/p-CUik7PCi.js.map +0 -1
  231. package/dist/components/p-CwTVEAl2.js +0 -2
  232. package/dist/components/p-DABj3qTX.js +0 -2
  233. package/dist/components/p-DUHcJWTQ.js +0 -2
  234. package/dist/components/p-DuNZGFkV.js.map +0 -1
  235. package/dist/components/p-Dy_sZ-dI.js +0 -2
  236. package/dist/components/p-l6PmnW_P.js +0 -2
  237. package/dist/components/p-l6PmnW_P.js.map +0 -1
  238. package/dist/components/p-xFbUt9Tc.js +0 -2
  239. package/dist/components/p-xFbUt9Tc.js.map +0 -1
  240. /package/dist/components/{p-BYHKdFzV.js.map → p-B8CubPgQ.js.map} +0 -0
  241. /package/dist/components/{p-BH3es4p7.js.map → p-BRLDJmkh.js.map} +0 -0
  242. /package/dist/components/{p-Bs6SS34m.js.map → p-C0_8K4y7.js.map} +0 -0
  243. /package/dist/components/{p-BhKhX1er.js.map → p-CA3kLO7K.js.map} +0 -0
  244. /package/dist/components/{p-DfIch2Dk.js.map → p-CCHqsF_H.js.map} +0 -0
  245. /package/dist/components/{p-DVk0wxxX.js.map → p-CHkI3oIS.js.map} +0 -0
  246. /package/dist/components/{p-liMPkhUP.js.map → p-CNffejay.js.map} +0 -0
  247. /package/dist/components/{p-B3YvoGjS.js.map → p-Cxpl-rzP.js.map} +0 -0
  248. /package/dist/components/{p-BkwBtZs3.js.map → p-D6jK2Hyi.js.map} +0 -0
  249. /package/dist/components/{p-Cy04lXlc.js.map → p-DBi1bmOL.js.map} +0 -0
  250. /package/dist/components/{p-DxeHimqk.js.map → p-DOCKtrS4.js.map} +0 -0
  251. /package/dist/components/{p-6vj4kh70.js.map → p-DcGpdIlT.js.map} +0 -0
  252. /package/dist/components/{p-Cf2J9MEo.js.map → p-XgIQ3oI_.js.map} +0 -0
  253. /package/dist/components/{p-Cf-i-6aE.js.map → p-ky1msbJ8.js.map} +0 -0
@@ -61,14 +61,14 @@
61
61
  "states": [
62
62
  "The host owns open state and responds to selection and close events.",
63
63
  "Use either flat items or sections, not both.",
64
- "After dsSelect from a switch row, update switchValue in the items property so the controlled indicator reflects the new state.",
65
- "For selectionMode single, keep exactly one item isSelected and update the items property after dsSelect.",
64
+ "After dsSelect from a switch row, replace the items or sections array with updated switchValue data so the controlled indicator reflects the new state.",
65
+ "For selectionMode single, keep exactly one item isSelected and replace the items or sections array after dsSelect; Menu never mutates consumer-owned selection data.",
66
66
  "Arrays, objects, and element anchors must be assigned as JavaScript properties.",
67
67
  "Use a swatch-picker section for a small curated visual preference and apply dsSwatchSelect immediately without closing the popup; the legacy gradient-picker section and dsGradientSelect remain compatibility-only."
68
68
  ],
69
69
  "responsiveBehavior": [
70
70
  "Keep the default choice-cell anchor alignment so the first or last interactive row edge aligns with its trigger, matching Select; use popup-frame only for deliberate custom frame geometry.",
71
- "Choose placement relative to the trigger and allow the component to clamp the popup to the viewport.",
71
+ "Treat side as the preferred placement. When the popup cannot fit there and the opposite side offers more room, Menu flips on that main axis before clamping the final position to the viewport.",
72
72
  "Avoid fixed widths that exceed narrow viewports."
73
73
  ],
74
74
  "frameworkCaveats": {
@@ -205,7 +205,8 @@
205
205
  "attribute": "side",
206
206
  "default": "'bottom'",
207
207
  "required": false,
208
- "mutable": false
208
+ "mutable": false,
209
+ "description": "Preferred side; placement flips to the opposite side when that offers a better fit."
209
210
  },
210
211
  "sideOffset": {
211
212
  "type": "number | string",
@@ -371,7 +372,8 @@
371
372
  "attribute": "side",
372
373
  "default": "'bottom'",
373
374
  "required": false,
374
- "mutable": false
375
+ "mutable": false,
376
+ "description": "Preferred side; placement flips to the opposite side when that offers a better fit."
375
377
  },
376
378
  "sideOffset": {
377
379
  "type": "number | string",
@@ -449,7 +451,7 @@
449
451
  "peerDependencies": {
450
452
  "required": [
451
453
  "@ds-mo/tokens ^3.1.1",
452
- "@ds-mo/icons ^6.3.0"
454
+ "@ds-mo/icons ^6.4.0"
453
455
  ],
454
456
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
455
457
  }
@@ -468,12 +470,12 @@
468
470
  "files": [
469
471
  {
470
472
  "path": "src/wc/components/Menu/Menu.css",
471
- "content": "@import '../../utils/choice-list.css';\n\n/* Inactive: `.ds-control-inactive` (25% opacity). */\n\n.menu-item--destructive .menu-item__label {\n /* Beat ds-text color-* host classes for destructive rows. */\n color: var(--color-foreground-bold-negative) !important;\n}\n\n/* Standard md icon zone keeps the dot aligned with other control suffixes. */\n.menu-item__dot-box {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--ds-control-icon, var(--dimension-iconography-md));\n height: var(--ds-control-icon, var(--dimension-iconography-md));\n pointer-events: none;\n}\n\n/* ── Switch suffix ───────────────────────────────────────────────────────── */\n\n.menu-item__switch {\n flex-shrink: 0;\n}\n\n/* ── Single-selection radio indicator ───────────────────────────────────── */\n\n.menu-item__radio-box {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--ds-control-icon, var(--dimension-iconography-md));\n height: var(--ds-control-icon, var(--dimension-iconography-md));\n pointer-events: none;\n}\n\n.menu-item__radio {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: var(--dimension-iconography-sm);\n height: var(--dimension-iconography-sm);\n border-radius: var(--dimension-radius-half);\n box-shadow: inset 0 0 0 var(--dimension-stroke-width-015) var(--color-foreground-tertiary);\n}\n\n.menu-item__radio--checked {\n background-color: var(--color-background-bold-brand);\n box-shadow: none;\n}\n\n.menu-item__radio-dot {\n width: var(--dimension-size-100);\n height: var(--dimension-size-100);\n border-radius: var(--dimension-radius-half);\n background-color: var(--color-foreground-on-bold-background-primary);\n}\n",
473
+ "content": "@import '../../utils/choice-list.css';\n\n/* Inactive: `.ds-control-inactive` (50% opacity). */\n\n.menu-item--destructive .menu-item__label {\n /* Beat ds-text color-* host classes for destructive rows. */\n color: var(--color-foreground-bold-negative) !important;\n}\n\n/* Standard md icon zone keeps the dot aligned with other control suffixes. */\n.menu-item__dot-box {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--ds-control-icon, var(--dimension-iconography-md));\n height: var(--ds-control-icon, var(--dimension-iconography-md));\n pointer-events: none;\n}\n\n/* ── Switch suffix ───────────────────────────────────────────────────────── */\n\n.menu-item__switch {\n flex-shrink: 0;\n}\n\n/* ── Single-selection radio indicator ───────────────────────────────────── */\n\n.menu-item__radio-box {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--ds-control-icon, var(--dimension-iconography-md));\n height: var(--ds-control-icon, var(--dimension-iconography-md));\n pointer-events: none;\n}\n\n.menu-item__radio {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: var(--dimension-iconography-sm);\n height: var(--dimension-iconography-sm);\n border-radius: var(--dimension-radius-half);\n box-shadow: inset 0 0 0 var(--dimension-stroke-width-015) var(--color-foreground-tertiary);\n}\n\n.menu-item__radio--checked {\n background-color: var(--color-background-bold-brand);\n box-shadow: none;\n}\n\n.menu-item__radio-dot {\n width: var(--dimension-size-100);\n height: var(--dimension-size-100);\n border-radius: var(--dimension-radius-half);\n background-color: var(--color-foreground-on-bold-background-primary);\n}\n",
472
474
  "type": "registry:ui"
473
475
  },
474
476
  {
475
477
  "path": "src/wc/components/Menu/Menu.tsx",
476
- "content": "import {\n Component,\n Prop,\n State,\n Event,\n EventEmitter,\n Element,\n Watch,\n Listen,\n h,\n Host,\n} from '@stencil/core';\nimport {\n choicePopupMinWidth,\n resolveChoicePopupAlignOffset,\n resolveCssLengthPx,\n resolveMotionTimeMs,\n TOKEN_CSS_LENGTHS,\n TOKEN_DEFAULTS,\n type ChoicePopupAnchorAlignment,\n} from '../../utils';\nimport { computeMenuPosition, type MenuAlign, type MenuSide } from './menu-position';\nimport type { MenuItemData, MenuSection } from './menu-types';\n\nexport type MenuSelectionMode = 'none' | 'single';\nimport {\n isMenuGradientPickerSection,\n isMenuPickerSection,\n isMenuSwatchPickerSection,\n} from './menu-types';\nimport {\n shellGradientPickerSections,\n type ShellGradientPreset,\n} from '../../shell/shell-gradient-presets';\n\n/** rAF retries while the popup mounts or the anchor resolves. */\nconst POSITION_RETRY_BUDGET = 8;\nconst MENU_FOCUSABLE_SELECTOR = [\n 'a[href]',\n 'button:not([disabled])',\n 'input:not([disabled]):not([type=\"hidden\"])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(',');\n\nfunction snapshotMenuSections(sections: MenuSection[]): MenuSection[] {\n return sections.map(section => {\n if (isMenuGradientPickerSection(section)) return { ...section };\n if (isMenuSwatchPickerSection(section)) {\n return {\n ...section,\n options: section.options?.map(option => ({ ...option })),\n sections: section.sections?.map(swatchSection => ({\n ...swatchSection,\n options: swatchSection.options.map(option => ({ ...option })),\n })),\n };\n }\n return { ...section, items: section.items.map(item => ({ ...item })) };\n });\n}\n\n@Component({\n tag: 'ds-menu',\n styleUrl: 'Menu.css',\n scoped: true,\n})\nexport class Menu {\n @Element() el!: HTMLElement;\n\n @Prop({ mutable: true }) open: boolean = false;\n @Prop() items: MenuItemData[] = [];\n /** Give ordinary rows mutually-exclusive radio-menu semantics using isSelected. */\n @Prop() selectionMode: MenuSelectionMode = 'none';\n @Prop() sections: MenuSection[] = [];\n @Prop() side: MenuSide = 'bottom';\n @Prop() align: MenuAlign = 'start';\n /** Align choice-row edges to the anchor by default; use `popup-frame` only for custom frame geometry. */\n @Prop() anchorAlignment: ChoicePopupAnchorAlignment = 'choice-cell';\n /** Gap between anchor and menu — number (px) or TokoMo length (`var(--dimension-space-050)`, etc.). */\n @Prop() sideOffset: number | string = TOKEN_CSS_LENGTHS.space050;\n /** Cross-axis offset — number (px) or TokoMo length. */\n @Prop() alignOffset: number | string = 0;\n @Prop() menuWidth: string | undefined;\n @Prop() minWidth: string | undefined;\n /** External trigger element to position against. Set via JS: menuEl.anchor = buttonEl */\n @Prop() anchor: HTMLElement | undefined;\n /** ID of the external trigger element for positioning */\n @Prop() anchorId: string | undefined;\n /** Show a visible ring on the initially focused menu item. Use only when the opener was keyboard-driven. */\n @Prop() initialFocusVisible: boolean = false;\n /** Accessible name for the popup menu. */\n @Prop() menuLabel: string = 'Menu';\n\n @State() private shouldRender: boolean = false;\n @State() private closing: boolean = false;\n @State() private pos: { x: number; y: number } = { x: 0, y: 0 };\n @State() private focusedIndex: number = 0;\n @State() private positionReady: boolean = false;\n @State() private focusRingVisible: boolean = false;\n\n @Event() dsClose!: EventEmitter<void>;\n /** Emitted after the popup's exit motion is complete and its rendered content is removed. */\n @Event() dsAfterClose!: EventEmitter<void>;\n @Event() dsSelect!: EventEmitter<MenuItemData>;\n /** Emitted when a `gradient-picker` section swatch is chosen. */\n @Event() dsGradientSelect!: EventEmitter<ShellGradientPreset>;\n /** Emitted when a generic `swatch-picker` section option is chosen. */\n @Event() dsSwatchSelect!: EventEmitter<string>;\n\n private clickOutsideHandler: ((e: MouseEvent) => void) | null = null;\n private scrollResizeHandler: (() => void) | null = null;\n private closeTimer: ReturnType<typeof setTimeout> | null = null;\n private itemEls: HTMLElement[] = [];\n private positionRetryRaf: number | null = null;\n private livePositionRaf: number | null = null;\n private listenersReady = false;\n /** Last content actually painted while open; retained unchanged through exit motion. */\n private lastRenderedSections: MenuSection[] = [];\n private closingSections: MenuSection[] | null = null;\n\n componentDidLoad() {\n if (this.open) this.onOpenChange(true);\n }\n\n componentDidRender() {\n if (!this.shouldRender) return;\n const popup = this.el.querySelector<HTMLElement>('.menu-popup');\n if (!popup || typeof popup.showPopover !== 'function') return;\n if (!popup.matches(':popover-open')) popup.showPopover();\n }\n\n disconnectedCallback() {\n this.cancelPositionRetry();\n this.cancelLivePositionUpdate();\n this.teardownListeners();\n }\n\n @Watch('open')\n onOpenChange(isOpen: boolean) {\n if (isOpen) {\n this.teardownListeners();\n this.closingSections = null;\n this.shouldRender = true;\n this.closing = false;\n this.positionReady = false;\n this.listenersReady = false;\n this.focusRingVisible = this.initialFocusVisible;\n this.listenersReady = true;\n this.setupListeners();\n this.schedulePositionUpdate(() => {\n this.focusInitialItem();\n });\n } else if (this.shouldRender) {\n this.cancelPositionRetry();\n this.captureClosingSections();\n this.closing = true;\n this.teardownListeners();\n this.listenersReady = false;\n const closeAnimationMs = this.closeAnimationMs;\n if (closeAnimationMs <= 0) {\n this.finishClose();\n return;\n }\n this.closeTimer = setTimeout(() => {\n this.closeTimer = null;\n this.finishClose();\n }, closeAnimationMs);\n }\n }\n\n @Watch('anchor')\n @Watch('anchorId')\n onAnchorChange() {\n if (this.open) this.schedulePositionUpdate();\n }\n\n @Watch('side')\n @Watch('align')\n @Watch('anchorAlignment')\n @Watch('sideOffset')\n @Watch('alignOffset')\n onPositionPropsChange() {\n if (this.open) this.schedulePositionUpdate();\n }\n\n private get viewportPadPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n }\n\n private get sideOffsetPx(): number {\n return resolveCssLengthPx(this.sideOffset, TOKEN_DEFAULTS.space050);\n }\n\n private get alignOffsetPx(): number {\n return resolveCssLengthPx(this.alignOffset, 0);\n }\n\n private get popupFallbackWidthPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuWidthXs, TOKEN_DEFAULTS.menuWidthXs);\n }\n\n private get popupFallbackHeightPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuFallbackHeight, TOKEN_DEFAULTS.menuFallbackHeight);\n }\n\n private get closeAnimationMs(): number {\n return resolveMotionTimeMs(TOKEN_DEFAULTS.motionShort2, TOKEN_DEFAULTS.animationDurationShort3);\n }\n\n private finishClose() {\n const popup = this.el.querySelector<HTMLElement>('.menu-popup');\n if (popup?.matches(':popover-open')) popup.hidePopover();\n this.shouldRender = false;\n this.closing = false;\n this.lastRenderedSections = [];\n this.closingSections = null;\n this.dsAfterClose.emit();\n }\n\n private captureClosingSections() {\n if (this.closingSections) return;\n const visibleSections = this.lastRenderedSections.length\n ? this.lastRenderedSections\n : this.activeSections;\n this.closingSections = snapshotMenuSections(visibleSections);\n }\n\n private get resolvedAnchor(): HTMLElement | null {\n if (this.anchor) return this.anchor;\n if (this.anchorId) return document.getElementById(this.anchorId);\n return null;\n }\n\n private get anchorFocusTarget(): HTMLElement | null {\n const anchor = this.resolvedAnchor;\n if (!anchor) return null;\n if (anchor.matches(MENU_FOCUSABLE_SELECTOR)) return anchor;\n return anchor.querySelector<HTMLElement>(MENU_FOCUSABLE_SELECTOR) ?? anchor;\n }\n\n private focusAnchor() {\n const anchor = this.resolvedAnchor as\n | (HTMLElement & { setFocus?: () => Promise<void> | void })\n | null;\n if (anchor?.setFocus) {\n anchor.setFocus();\n return;\n }\n this.anchorFocusTarget?.focus();\n }\n\n private moveFocusAfterTab(backwards: boolean) {\n const anchor = this.anchorFocusTarget;\n if (!anchor) return;\n if (backwards) {\n anchor.focus();\n return;\n }\n\n const candidates = Array.from(\n document.querySelectorAll<HTMLElement>(MENU_FOCUSABLE_SELECTOR)\n ).filter(\n element =>\n !element.closest('.menu-popup') &&\n !element.closest('[inert]') &&\n element.getClientRects().length > 0\n );\n const index = candidates.indexOf(anchor);\n (candidates[index + 1] ?? anchor).focus();\n }\n\n private compositeTabLeavesPopup(event: KeyboardEvent): boolean {\n const popup = this.el.querySelector<HTMLElement>('.menu-popup');\n if (!popup) return true;\n const focusables = Array.from(\n popup.querySelectorAll<HTMLElement>(MENU_FOCUSABLE_SELECTOR)\n ).filter(element => element.getClientRects().length > 0);\n const currentIndex = focusables.indexOf(event.target as HTMLElement);\n if (currentIndex < 0) return true;\n return event.shiftKey ? currentIndex === 0 : currentIndex === focusables.length - 1;\n }\n\n private get activeSections(): MenuSection[] {\n if (this.sections.length > 0) return this.sections;\n if (this.items.length > 0) return [{ items: this.items }];\n return [];\n }\n\n private get flatItems(): MenuItemData[] {\n return this.activeSections.flatMap(section =>\n isMenuPickerSection(section) ? [] : section.items\n );\n }\n\n /** Rich preference content is a non-modal dialog, not an ARIA action menu. */\n private get hasCompositeSections(): boolean {\n return this.activeSections.some(isMenuPickerSection);\n }\n\n private cancelPositionRetry() {\n if (this.positionRetryRaf !== null) {\n cancelAnimationFrame(this.positionRetryRaf);\n this.positionRetryRaf = null;\n }\n }\n\n private cancelLivePositionUpdate() {\n if (this.livePositionRaf !== null) {\n cancelAnimationFrame(this.livePositionRaf);\n this.livePositionRaf = null;\n }\n }\n\n /** Reposition after the current scroll/layout frame without hiding the open popup. */\n private scheduleLivePositionUpdate() {\n if (this.livePositionRaf !== null) return;\n this.livePositionRaf = requestAnimationFrame(() => {\n if (this.shouldRender && !this.closing) this.calculatePosition();\n // Scroll-driven owners such as ShellPage may commit their compact header\n // render in this frame. Re-read once after that render, then stop tracking.\n this.livePositionRaf = requestAnimationFrame(() => {\n this.livePositionRaf = null;\n if (this.shouldRender && !this.closing) this.calculatePosition();\n });\n });\n }\n\n /** Retry until anchor + popup exist — do not reveal at 0,0 on a failed first pass. */\n private schedulePositionUpdate(onReady?: () => void) {\n if (!this.open) return;\n\n this.cancelPositionRetry();\n this.positionReady = false;\n\n let remaining = POSITION_RETRY_BUDGET;\n\n const attempt = () => {\n this.positionRetryRaf = null;\n if (!this.open) return;\n\n if (this.calculatePosition()) {\n this.positionReady = true;\n onReady?.();\n return;\n }\n\n if (remaining > 0) {\n remaining -= 1;\n this.positionRetryRaf = requestAnimationFrame(attempt);\n }\n };\n\n this.positionRetryRaf = requestAnimationFrame(attempt);\n }\n\n /** @returns `true` when anchor and popup were found and `pos` was updated. */\n private calculatePosition(): boolean {\n const anchorEl = this.resolvedAnchor;\n if (!anchorEl) return false;\n const popup = this.el.querySelector('.menu-popup') as HTMLElement | null;\n if (!popup) return false;\n\n const anchorRect = anchorEl.getBoundingClientRect();\n const sectionInsetPx = this.viewportPadPx;\n if (!this.minWidth) {\n if (\n this.anchorAlignment === 'choice-cell' &&\n (this.side === 'top' || this.side === 'bottom')\n ) {\n popup.style.minWidth = `max(var(--dimension-menu-width-xs), ${choicePopupMinWidth(\n anchorRect.width,\n sectionInsetPx\n )}px)`;\n } else {\n popup.style.removeProperty('min-width');\n }\n }\n\n const next = computeMenuPosition({\n anchorRect,\n popupWidth: popup.offsetWidth || this.popupFallbackWidthPx,\n popupHeight: popup.offsetHeight || this.popupFallbackHeightPx,\n side: this.side,\n align: this.align,\n sideOffsetPx: this.sideOffsetPx,\n alignOffsetPx: resolveChoicePopupAlignOffset({\n align: this.align,\n alignOffsetPx: this.alignOffsetPx,\n sectionInsetPx,\n anchorAlignment: this.anchorAlignment,\n }),\n viewportPadPx: this.viewportPadPx,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n });\n if (next.x !== this.pos.x || next.y !== this.pos.y) this.pos = next;\n return true;\n }\n\n /** Focus the selected item when present, otherwise the first enabled item. */\n private focusInitialItem() {\n if (this.hasCompositeSections) {\n requestAnimationFrame(() => {\n this.el.querySelector<HTMLElement>('.menu-popup [tabindex=\"0\"]')?.focus();\n });\n return;\n }\n\n const flat = this.flatItems;\n const selectedIdx = flat.findIndex(it => it.isSelected && !it.isInactive);\n const firstEnabledIdx = flat.findIndex(it => !it.isInactive);\n this.focusedIndex = selectedIdx >= 0 ? selectedIdx : firstEnabledIdx >= 0 ? firstEnabledIdx : 0;\n\n requestAnimationFrame(() => {\n const btns = this.el.querySelectorAll<HTMLElement>('.menu-item');\n btns[this.focusedIndex]?.focus();\n });\n }\n\n private setupListeners() {\n this.clickOutsideHandler = (e: MouseEvent) => {\n const t = e.target as Node;\n const popup = this.el.querySelector('.menu-popup');\n const anchorEl = this.resolvedAnchor;\n if (popup?.contains(t) || anchorEl?.contains(t)) return;\n this.close();\n };\n\n this.scrollResizeHandler = () => this.scheduleLivePositionUpdate();\n\n document.addEventListener('mousedown', this.clickOutsideHandler, true);\n window.addEventListener('scroll', this.scrollResizeHandler, true);\n window.addEventListener('resize', this.scrollResizeHandler);\n }\n\n private teardownListeners() {\n this.cancelLivePositionUpdate();\n if (this.clickOutsideHandler) {\n document.removeEventListener('mousedown', this.clickOutsideHandler, true);\n this.clickOutsideHandler = null;\n }\n if (this.scrollResizeHandler) {\n window.removeEventListener('scroll', this.scrollResizeHandler, true);\n window.removeEventListener('resize', this.scrollResizeHandler);\n this.scrollResizeHandler = null;\n }\n if (this.closeTimer) {\n clearTimeout(this.closeTimer);\n this.closeTimer = null;\n }\n }\n\n private close(restoreFocus = true) {\n this.captureClosingSections();\n if (restoreFocus) this.focusAnchor();\n this.dsClose.emit();\n this.open = false;\n this.onOpenChange(false);\n }\n\n @Listen('keydown')\n handleKeyDown(e: KeyboardEvent) {\n if (!this.shouldRender || this.closing) return;\n\n if (e.key === 'Escape') {\n e.preventDefault();\n this.close();\n return;\n }\n\n if (e.key === 'Tab') {\n if (this.hasCompositeSections && !this.compositeTabLeavesPopup(e)) return;\n e.preventDefault();\n this.close(false);\n this.moveFocusAfterTab(e.shiftKey);\n return;\n }\n\n if (this.hasCompositeSections) return;\n\n const flat = this.flatItems;\n const enabled = flat\n .map((it, i) => ({ it, i }))\n .filter(({ it }) => !it.isInactive)\n .map(({ i }) => i);\n if (!enabled.length) return;\n\n const cur = enabled.indexOf(this.focusedIndex);\n const safe = cur < 0 ? 0 : cur;\n\n switch (e.key) {\n case 'ArrowDown':\n e.preventDefault();\n e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[(safe + 1) % enabled.length];\n this.focusItem(this.focusedIndex);\n break;\n case 'ArrowUp':\n e.preventDefault();\n e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[(safe - 1 + enabled.length) % enabled.length];\n this.focusItem(this.focusedIndex);\n break;\n case 'Home':\n e.preventDefault();\n e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[0];\n this.focusItem(this.focusedIndex);\n break;\n case 'End':\n e.preventDefault();\n e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[enabled.length - 1];\n this.focusItem(this.focusedIndex);\n break;\n }\n }\n\n private focusItem(idx: number) {\n const btns = this.el.querySelectorAll<HTMLElement>('.menu-item');\n btns[idx]?.focus();\n }\n\n private handleItemClick(item: MenuItemData) {\n if (item.isInactive) return;\n // Consumers commonly change the item source while handling selection.\n // Capture first so the exit animation never paints the next menu context.\n this.captureClosingSections();\n this.dsSelect.emit(item);\n }\n\n private handleGradientSelect(preset: ShellGradientPreset) {\n this.dsGradientSelect.emit(preset);\n }\n\n private handleSwatchSelect(value: string) {\n this.dsSwatchSelect.emit(value);\n }\n\n render() {\n if (!this.shouldRender) return <Host style={{ display: 'contents' }} />;\n\n const sections = this.closing\n ? (this.closingSections ?? this.lastRenderedSections)\n : this.activeSections;\n const hasCompositeSections = sections.some(isMenuPickerSection);\n if (!this.closing) {\n this.lastRenderedSections = snapshotMenuSections(sections);\n this.closingSections = null;\n }\n let flatIdx = 0;\n\n const popupStyle: Record<string, string> = {\n position: 'fixed',\n left: '0',\n top: '0',\n transform: `translate(${Math.round(this.pos.x)}px, ${Math.round(this.pos.y)}px)`,\n zIndex: '9998',\n visibility: this.positionReady ? 'visible' : 'hidden',\n };\n\n if (this.menuWidth) popupStyle['width'] = this.menuWidth;\n if (this.minWidth) popupStyle['min-width'] = this.minWidth;\n\n return (\n <Host style={{ display: 'contents' }}>\n <div\n popover=\"manual\"\n class={{\n 'menu-popup': true,\n 'menu-popup--closing': this.closing,\n 'ds-choice-popup': true,\n 'ds-choice-popup--closing': this.closing,\n }}\n style={popupStyle}\n role={hasCompositeSections ? 'dialog' : 'menu'}\n aria-label={this.menuLabel}\n aria-orientation={hasCompositeSections ? undefined : 'vertical'}\n >\n <div class=\"ds-choice-list\">\n {sections.map((section, si) => (\n <div\n key={si}\n class={{\n 'menu-section': true,\n 'menu-section--divided': si < sections.length - 1,\n 'menu-section--gradient-picker': isMenuPickerSection(section),\n 'ds-choice-section': true,\n 'ds-choice-section--divided': si < sections.length - 1,\n }}\n role={section.header ? 'group' : undefined}\n aria-label={section.header}\n >\n {section.header && (\n <ds-text\n class=\"section-header ds-choice-section__header ds-control--md\"\n as=\"span\"\n variant=\"text-body-small\"\n emphasis\n color=\"primary\"\n aria-hidden=\"true\"\n >\n {section.header}\n </ds-text>\n )}\n {isMenuGradientPickerSection(section) ? (\n <ds-swatch-picker\n value={section.value}\n groupLabel={section.header ?? 'Shell gradient theme'}\n sections={shellGradientPickerSections()}\n onDsChange={(e: CustomEvent<string>) => {\n e.stopPropagation();\n this.handleGradientSelect(e.detail as ShellGradientPreset);\n }}\n />\n ) : isMenuSwatchPickerSection(section) ? (\n <ds-swatch-picker\n value={section.value}\n groupLabel={section.groupLabel ?? section.header ?? 'Swatch options'}\n options={section.options ?? []}\n sections={section.sections ?? []}\n onDsChange={(e: CustomEvent<string>) => {\n e.stopPropagation();\n this.handleSwatchSelect(e.detail);\n }}\n />\n ) : (\n section.items.map(item => {\n const idx = flatIdx++;\n const isFocused = this.focusedIndex === idx;\n const isRadioItem =\n !hasCompositeSections &&\n this.selectionMode === 'single' &&\n !item.showSwitch;\n return (\n <button\n key={idx}\n type=\"button\"\n class={{\n 'menu-item': true,\n 'ds-choice-item': true,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-focus-ring--visible': isFocused && this.focusRingVisible,\n 'ds-interaction-fill': !item.isInactive,\n 'ds-interaction-fill--selected': !!item.isSelected && !item.isInactive,\n 'menu-item--selected': !!item.isSelected,\n 'ds-control-inactive': !!item.isInactive,\n 'menu-item--destructive': !!item.isDestructive,\n 'menu-item--focused': isFocused,\n }}\n role={\n hasCompositeSections\n ? undefined\n : item.showSwitch\n ? 'menuitemcheckbox'\n : isRadioItem\n ? 'menuitemradio'\n : 'menuitem'\n }\n aria-checked={\n !hasCompositeSections && item.showSwitch\n ? String(!!item.switchValue)\n : isRadioItem\n ? String(!!item.isSelected)\n : undefined\n }\n aria-pressed={\n hasCompositeSections\n ? String(item.showSwitch ? !!item.switchValue : !!item.isSelected)\n : undefined\n }\n aria-current={\n !hasCompositeSections &&\n !item.showSwitch &&\n !isRadioItem &&\n item.isSelected\n ? 'true'\n : undefined\n }\n disabled={item.isInactive}\n tabIndex={hasCompositeSections ? 0 : isFocused ? 0 : -1}\n onMouseDown={() => {\n this.focusRingVisible = false;\n }}\n onClick={() => this.handleItemClick(item)}\n onFocus={() => {\n this.focusedIndex = idx;\n }}\n >\n {isRadioItem && (\n <span\n class=\"menu-item__radio-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <span\n class={{\n 'menu-item__radio': true,\n 'menu-item__radio--checked': !!item.isSelected,\n }}\n >\n {item.isSelected && <span class=\"menu-item__radio-dot\" />}\n </span>\n </span>\n )}\n <div class=\"menu-item__content ds-choice-item__content ds-interaction-fill__content\">\n <ds-text\n class=\"menu-item__label ds-choice-item__label\"\n as=\"span\"\n variant=\"text-body-medium\"\n color={item.isSelected ? 'primary' : 'secondary'}\n >\n {item.label}\n </ds-text>\n {item.subtext && (\n <ds-text\n class=\"menu-item__subtext ds-choice-item__subtext\"\n as=\"span\"\n variant=\"text-body-small\"\n color=\"secondary\"\n >\n {item.subtext}\n </ds-text>\n )}\n </div>\n {item.dot && (\n <span\n class=\"menu-item__dot-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-badge\n class=\"menu-item__dot\"\n variant=\"dot\"\n hasRing={false}\n label=\"\"\n />\n </span>\n )}\n {item.showSwitch && (\n <ds-switch\n class=\"menu-item__switch ds-interaction-fill__content\"\n size=\"md\"\n checked={!!item.switchValue}\n presentation\n />\n )}\n </button>\n );\n })\n )}\n </div>\n ))}\n </div>\n </div>\n </Host>\n );\n }\n}\n",
478
+ "content": "import {\n Component,\n Prop,\n State,\n Event,\n EventEmitter,\n Element,\n Watch,\n Listen,\n h,\n Host,\n} from '@stencil/core';\nimport {\n choicePopupMinWidth,\n resolveChoicePopupAlignOffset,\n resolveCssLengthPx,\n resolveMotionTimeMs,\n TOKEN_CSS_LENGTHS,\n TOKEN_DEFAULTS,\n type ChoicePopupAnchorAlignment,\n} from '../../utils';\nimport { computeMenuPosition, type MenuAlign, type MenuSide } from './menu-position';\nimport type { MenuItemData, MenuSection } from './menu-types';\n\nexport type MenuSelectionMode = 'none' | 'single';\nimport {\n isMenuGradientPickerSection,\n isMenuPickerSection,\n isMenuSwatchPickerSection,\n} from './menu-types';\nimport {\n shellGradientPickerSections,\n type ShellGradientPreset,\n} from '../../shell/shell-gradient-presets';\n\n/** rAF retries while the popup mounts or the anchor resolves. */\nconst POSITION_RETRY_BUDGET = 8;\nconst MENU_FOCUSABLE_SELECTOR = [\n 'a[href]',\n 'button:not([disabled])',\n 'input:not([disabled]):not([type=\"hidden\"])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(',');\n\nfunction snapshotMenuSections(sections: MenuSection[]): MenuSection[] {\n return sections.map(section => {\n if (isMenuGradientPickerSection(section)) return { ...section };\n if (isMenuSwatchPickerSection(section)) {\n return {\n ...section,\n options: section.options?.map(option => ({ ...option })),\n sections: section.sections?.map(swatchSection => ({\n ...swatchSection,\n options: swatchSection.options.map(option => ({ ...option })),\n })),\n };\n }\n return { ...section, items: section.items.map(item => ({ ...item })) };\n });\n}\n\n@Component({\n tag: 'ds-menu',\n styleUrl: 'Menu.css',\n scoped: true,\n})\nexport class Menu {\n @Element() el!: HTMLElement;\n\n @Prop({ mutable: true }) open: boolean = false;\n @Prop() items: MenuItemData[] = [];\n /** Give ordinary rows mutually-exclusive radio-menu semantics using isSelected. */\n @Prop() selectionMode: MenuSelectionMode = 'none';\n @Prop() sections: MenuSection[] = [];\n /** Preferred side; placement flips to the opposite side when that offers a better fit. */\n @Prop() side: MenuSide = 'bottom';\n @Prop() align: MenuAlign = 'start';\n /** Align choice-row edges to the anchor by default; use `popup-frame` only for custom frame geometry. */\n @Prop() anchorAlignment: ChoicePopupAnchorAlignment = 'choice-cell';\n /** Gap between anchor and menu — number (px) or TokoMo length (`var(--dimension-space-050)`, etc.). */\n @Prop() sideOffset: number | string = TOKEN_CSS_LENGTHS.space050;\n /** Cross-axis offset — number (px) or TokoMo length. */\n @Prop() alignOffset: number | string = 0;\n @Prop() menuWidth: string | undefined;\n @Prop() minWidth: string | undefined;\n /** External trigger element to position against. Set via JS: menuEl.anchor = buttonEl */\n @Prop() anchor: HTMLElement | undefined;\n /** ID of the external trigger element for positioning */\n @Prop() anchorId: string | undefined;\n /** Show a visible ring on the initially focused menu item. Use only when the opener was keyboard-driven. */\n @Prop() initialFocusVisible: boolean = false;\n /** Accessible name for the popup menu. */\n @Prop() menuLabel: string = 'Menu';\n\n @State() private shouldRender: boolean = false;\n @State() private closing: boolean = false;\n @State() private pos: { x: number; y: number } = { x: 0, y: 0 };\n @State() private focusedIndex: number = 0;\n @State() private positionReady: boolean = false;\n @State() private focusRingVisible: boolean = false;\n\n @Event() dsClose!: EventEmitter<void>;\n /** Emitted after the popup's exit motion is complete and its rendered content is removed. */\n @Event() dsAfterClose!: EventEmitter<void>;\n @Event() dsSelect!: EventEmitter<MenuItemData>;\n /** Emitted when a `gradient-picker` section swatch is chosen. */\n @Event() dsGradientSelect!: EventEmitter<ShellGradientPreset>;\n /** Emitted when a generic `swatch-picker` section option is chosen. */\n @Event() dsSwatchSelect!: EventEmitter<string>;\n\n private clickOutsideHandler: ((e: MouseEvent) => void) | null = null;\n private scrollResizeHandler: (() => void) | null = null;\n private closeTimer: ReturnType<typeof setTimeout> | null = null;\n private positionRetryRaf: number | null = null;\n private livePositionRaf: number | null = null;\n /** Last content actually painted while open; retained unchanged through exit motion. */\n private lastRenderedSections: MenuSection[] = [];\n private closingSections: MenuSection[] | null = null;\n\n componentDidLoad() {\n if (this.open) this.onOpenChange(true);\n }\n\n componentDidRender() {\n if (!this.shouldRender) return;\n const popup = this.el.querySelector<HTMLElement>('.menu-popup');\n if (!popup || typeof popup.showPopover !== 'function') return;\n if (!popup.matches(':popover-open')) popup.showPopover();\n }\n\n disconnectedCallback() {\n this.cancelPositionRetry();\n this.cancelLivePositionUpdate();\n this.teardownListeners();\n }\n\n @Watch('open')\n onOpenChange(isOpen: boolean) {\n if (isOpen) {\n this.teardownListeners();\n this.closingSections = null;\n this.shouldRender = true;\n this.closing = false;\n this.positionReady = false;\n this.focusRingVisible = this.initialFocusVisible;\n this.setupListeners();\n this.schedulePositionUpdate(() => {\n this.focusInitialItem();\n });\n } else if (this.shouldRender) {\n this.cancelPositionRetry();\n this.captureClosingSections();\n this.closing = true;\n this.teardownListeners();\n const closeAnimationMs = this.closeAnimationMs;\n if (closeAnimationMs <= 0) {\n this.finishClose();\n return;\n }\n this.closeTimer = setTimeout(() => {\n this.closeTimer = null;\n this.finishClose();\n }, closeAnimationMs);\n }\n }\n\n @Watch('anchor')\n @Watch('anchorId')\n onAnchorChange() {\n if (this.open) this.schedulePositionUpdate();\n }\n\n @Watch('side')\n @Watch('align')\n @Watch('anchorAlignment')\n @Watch('sideOffset')\n @Watch('alignOffset')\n onPositionPropsChange() {\n if (this.open) this.schedulePositionUpdate();\n }\n\n private get viewportPadPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n }\n\n private get sideOffsetPx(): number {\n return resolveCssLengthPx(this.sideOffset, TOKEN_DEFAULTS.space050);\n }\n\n private get alignOffsetPx(): number {\n return resolveCssLengthPx(this.alignOffset, 0);\n }\n\n private get popupFallbackWidthPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuWidthXs, TOKEN_DEFAULTS.menuWidthXs);\n }\n\n private get popupFallbackHeightPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuFallbackHeight, TOKEN_DEFAULTS.menuFallbackHeight);\n }\n\n private get closeAnimationMs(): number {\n return resolveMotionTimeMs(TOKEN_DEFAULTS.motionShort2, TOKEN_DEFAULTS.animationDurationShort3);\n }\n\n private finishClose() {\n const popup = this.el.querySelector<HTMLElement>('.menu-popup');\n if (popup?.matches(':popover-open')) popup.hidePopover();\n this.shouldRender = false;\n this.closing = false;\n this.lastRenderedSections = [];\n this.closingSections = null;\n this.dsAfterClose.emit();\n }\n\n private captureClosingSections() {\n if (this.closingSections) return;\n const visibleSections = this.lastRenderedSections.length\n ? this.lastRenderedSections\n : this.activeSections;\n this.closingSections = snapshotMenuSections(visibleSections);\n }\n\n private get resolvedAnchor(): HTMLElement | null {\n if (this.anchor) return this.anchor;\n if (this.anchorId) return document.getElementById(this.anchorId);\n return null;\n }\n\n private get anchorFocusTarget(): HTMLElement | null {\n const anchor = this.resolvedAnchor;\n if (!anchor) return null;\n if (anchor.matches(MENU_FOCUSABLE_SELECTOR)) return anchor;\n return anchor.querySelector<HTMLElement>(MENU_FOCUSABLE_SELECTOR) ?? anchor;\n }\n\n private focusAnchor() {\n const anchor = this.resolvedAnchor as\n | (HTMLElement & { setFocus?: () => Promise<void> | void })\n | null;\n if (anchor?.setFocus) {\n anchor.setFocus();\n return;\n }\n this.anchorFocusTarget?.focus();\n }\n\n private moveFocusAfterTab(backwards: boolean) {\n const anchor = this.anchorFocusTarget;\n if (!anchor) return;\n if (backwards) {\n anchor.focus();\n return;\n }\n\n const candidates = Array.from(\n document.querySelectorAll<HTMLElement>(MENU_FOCUSABLE_SELECTOR)\n ).filter(\n element =>\n !element.closest('.menu-popup') &&\n !element.closest('[inert]') &&\n element.getClientRects().length > 0\n );\n const index = candidates.indexOf(anchor);\n (candidates[index + 1] ?? anchor).focus();\n }\n\n private compositeTabLeavesPopup(event: KeyboardEvent): boolean {\n const popup = this.el.querySelector<HTMLElement>('.menu-popup');\n if (!popup) return true;\n const focusables = Array.from(\n popup.querySelectorAll<HTMLElement>(MENU_FOCUSABLE_SELECTOR)\n ).filter(element => element.getClientRects().length > 0);\n const currentIndex = focusables.indexOf(event.target as HTMLElement);\n if (currentIndex < 0) return true;\n return event.shiftKey ? currentIndex === 0 : currentIndex === focusables.length - 1;\n }\n\n private get activeSections(): MenuSection[] {\n if (this.sections.length > 0) return this.sections;\n if (this.items.length > 0) return [{ items: this.items }];\n return [];\n }\n\n private get flatItems(): MenuItemData[] {\n return this.activeSections.flatMap(section =>\n isMenuPickerSection(section) ? [] : section.items\n );\n }\n\n /** Rich preference content is a non-modal dialog, not an ARIA action menu. */\n private get hasCompositeSections(): boolean {\n return this.activeSections.some(isMenuPickerSection);\n }\n\n private cancelPositionRetry() {\n if (this.positionRetryRaf !== null) {\n cancelAnimationFrame(this.positionRetryRaf);\n this.positionRetryRaf = null;\n }\n }\n\n private cancelLivePositionUpdate() {\n if (this.livePositionRaf !== null) {\n cancelAnimationFrame(this.livePositionRaf);\n this.livePositionRaf = null;\n }\n }\n\n /** Reposition after the current scroll/layout frame without hiding the open popup. */\n private scheduleLivePositionUpdate() {\n if (this.livePositionRaf !== null) return;\n this.livePositionRaf = requestAnimationFrame(() => {\n if (this.shouldRender && !this.closing) this.calculatePosition();\n // Scroll-driven owners such as ShellPage may commit their compact header\n // render in this frame. Re-read once after that render, then stop tracking.\n this.livePositionRaf = requestAnimationFrame(() => {\n this.livePositionRaf = null;\n if (this.shouldRender && !this.closing) this.calculatePosition();\n });\n });\n }\n\n /** Retry until anchor + popup exist — do not reveal at 0,0 on a failed first pass. */\n private schedulePositionUpdate(onReady?: () => void) {\n if (!this.open) return;\n\n this.cancelPositionRetry();\n this.positionReady = false;\n\n let remaining = POSITION_RETRY_BUDGET;\n\n const attempt = () => {\n this.positionRetryRaf = null;\n if (!this.open) return;\n\n if (this.calculatePosition()) {\n this.positionReady = true;\n onReady?.();\n return;\n }\n\n if (remaining > 0) {\n remaining -= 1;\n this.positionRetryRaf = requestAnimationFrame(attempt);\n }\n };\n\n this.positionRetryRaf = requestAnimationFrame(attempt);\n }\n\n /** @returns `true` when anchor and popup were found and `pos` was updated. */\n private calculatePosition(): boolean {\n const anchorEl = this.resolvedAnchor;\n if (!anchorEl) return false;\n const popup = this.el.querySelector('.menu-popup') as HTMLElement | null;\n if (!popup) return false;\n\n const anchorRect = anchorEl.getBoundingClientRect();\n const sectionInsetPx = this.viewportPadPx;\n if (!this.minWidth) {\n if (\n this.anchorAlignment === 'choice-cell' &&\n (this.side === 'top' || this.side === 'bottom')\n ) {\n popup.style.minWidth = `max(var(--dimension-menu-width-xs), ${choicePopupMinWidth(\n anchorRect.width,\n sectionInsetPx\n )}px)`;\n } else {\n popup.style.removeProperty('min-width');\n }\n }\n\n const next = computeMenuPosition({\n anchorRect,\n popupWidth: popup.offsetWidth || this.popupFallbackWidthPx,\n popupHeight: popup.offsetHeight || this.popupFallbackHeightPx,\n side: this.side,\n align: this.align,\n sideOffsetPx: this.sideOffsetPx,\n alignOffsetPx: resolveChoicePopupAlignOffset({\n align: this.align,\n alignOffsetPx: this.alignOffsetPx,\n sectionInsetPx,\n anchorAlignment: this.anchorAlignment,\n }),\n viewportPadPx: this.viewportPadPx,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n });\n if (next.x !== this.pos.x || next.y !== this.pos.y) this.pos = next;\n return true;\n }\n\n /** Focus the selected item when present, otherwise the first enabled item. */\n private focusInitialItem() {\n if (this.hasCompositeSections) {\n requestAnimationFrame(() => {\n this.el.querySelector<HTMLElement>('.menu-popup [tabindex=\"0\"]')?.focus();\n });\n return;\n }\n\n const flat = this.flatItems;\n const selectedIdx = flat.findIndex(it => it.isSelected && !it.isInactive);\n const firstEnabledIdx = flat.findIndex(it => !it.isInactive);\n this.focusedIndex = selectedIdx >= 0 ? selectedIdx : firstEnabledIdx >= 0 ? firstEnabledIdx : 0;\n\n requestAnimationFrame(() => {\n const btns = this.el.querySelectorAll<HTMLElement>('.menu-item');\n btns[this.focusedIndex]?.focus();\n });\n }\n\n private setupListeners() {\n this.clickOutsideHandler = (e: MouseEvent) => {\n const t = e.target as Node;\n const popup = this.el.querySelector('.menu-popup');\n const anchorEl = this.resolvedAnchor;\n if (popup?.contains(t) || anchorEl?.contains(t)) return;\n this.close();\n };\n\n this.scrollResizeHandler = () => this.scheduleLivePositionUpdate();\n\n document.addEventListener('mousedown', this.clickOutsideHandler, true);\n window.addEventListener('scroll', this.scrollResizeHandler, true);\n window.addEventListener('resize', this.scrollResizeHandler);\n }\n\n private teardownListeners() {\n this.cancelLivePositionUpdate();\n if (this.clickOutsideHandler) {\n document.removeEventListener('mousedown', this.clickOutsideHandler, true);\n this.clickOutsideHandler = null;\n }\n if (this.scrollResizeHandler) {\n window.removeEventListener('scroll', this.scrollResizeHandler, true);\n window.removeEventListener('resize', this.scrollResizeHandler);\n this.scrollResizeHandler = null;\n }\n if (this.closeTimer) {\n clearTimeout(this.closeTimer);\n this.closeTimer = null;\n }\n }\n\n private close(restoreFocus = true) {\n this.captureClosingSections();\n if (restoreFocus) this.focusAnchor();\n this.dsClose.emit();\n this.open = false;\n this.onOpenChange(false);\n }\n\n @Listen('keydown')\n handleKeyDown(e: KeyboardEvent) {\n if (!this.shouldRender || this.closing) return;\n\n if (e.key === 'Escape') {\n e.preventDefault();\n this.close();\n return;\n }\n\n if (e.key === 'Tab') {\n if (this.hasCompositeSections && !this.compositeTabLeavesPopup(e)) return;\n e.preventDefault();\n this.close(false);\n this.moveFocusAfterTab(e.shiftKey);\n return;\n }\n\n if (this.hasCompositeSections) return;\n\n const flat = this.flatItems;\n const enabled = flat\n .map((it, i) => ({ it, i }))\n .filter(({ it }) => !it.isInactive)\n .map(({ i }) => i);\n if (!enabled.length) return;\n\n const cur = enabled.indexOf(this.focusedIndex);\n const safe = cur < 0 ? 0 : cur;\n\n switch (e.key) {\n case 'ArrowDown':\n e.preventDefault();\n e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[(safe + 1) % enabled.length];\n this.focusItem(this.focusedIndex);\n break;\n case 'ArrowUp':\n e.preventDefault();\n e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[(safe - 1 + enabled.length) % enabled.length];\n this.focusItem(this.focusedIndex);\n break;\n case 'Home':\n e.preventDefault();\n e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[0];\n this.focusItem(this.focusedIndex);\n break;\n case 'End':\n e.preventDefault();\n e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[enabled.length - 1];\n this.focusItem(this.focusedIndex);\n break;\n }\n }\n\n private focusItem(idx: number) {\n const btns = this.el.querySelectorAll<HTMLElement>('.menu-item');\n btns[idx]?.focus();\n }\n\n private handleItemClick(item: MenuItemData) {\n if (item.isInactive) return;\n // Consumers commonly change the item source while handling selection.\n // Capture first so the exit animation never paints the next menu context.\n this.captureClosingSections();\n this.dsSelect.emit(item);\n }\n\n private handleGradientSelect(preset: ShellGradientPreset) {\n this.dsGradientSelect.emit(preset);\n }\n\n private handleSwatchSelect(value: string) {\n this.dsSwatchSelect.emit(value);\n }\n\n render() {\n if (!this.shouldRender) return <Host style={{ display: 'contents' }} />;\n\n const sections = this.closing\n ? (this.closingSections ?? this.lastRenderedSections)\n : this.activeSections;\n const hasCompositeSections = sections.some(isMenuPickerSection);\n if (!this.closing) {\n this.lastRenderedSections = snapshotMenuSections(sections);\n this.closingSections = null;\n }\n let flatIdx = 0;\n\n const popupStyle: Record<string, string> = {\n position: 'fixed',\n left: '0',\n top: '0',\n transform: `translate(${Math.round(this.pos.x)}px, ${Math.round(this.pos.y)}px)`,\n zIndex: '9998',\n visibility: this.positionReady ? 'visible' : 'hidden',\n };\n\n if (this.menuWidth) popupStyle['width'] = this.menuWidth;\n if (this.minWidth) popupStyle['min-width'] = this.minWidth;\n\n return (\n <Host style={{ display: 'contents' }}>\n <div\n popover=\"manual\"\n class={{\n 'menu-popup': true,\n 'menu-popup--closing': this.closing,\n 'ds-choice-popup': true,\n 'ds-choice-popup--closing': this.closing,\n }}\n style={popupStyle}\n role={hasCompositeSections ? 'dialog' : 'menu'}\n aria-label={this.menuLabel}\n aria-orientation={hasCompositeSections ? undefined : 'vertical'}\n >\n <div class=\"ds-choice-list\">\n {sections.map((section, si) => (\n <div\n key={si}\n class={{\n 'menu-section': true,\n 'menu-section--divided': si < sections.length - 1,\n 'menu-section--gradient-picker': isMenuPickerSection(section),\n 'ds-choice-section': true,\n 'ds-choice-section--divided': si < sections.length - 1,\n }}\n role={section.header ? 'group' : undefined}\n aria-label={section.header}\n >\n {section.header && (\n <ds-text\n class=\"section-header ds-choice-section__header ds-control--md\"\n as=\"span\"\n variant=\"text-body-small\"\n emphasis\n color=\"primary\"\n aria-hidden=\"true\"\n >\n {section.header}\n </ds-text>\n )}\n {isMenuGradientPickerSection(section) ? (\n <ds-swatch-picker\n value={section.value}\n groupLabel={section.header ?? 'Shell gradient theme'}\n sections={shellGradientPickerSections()}\n onDsChange={(e: CustomEvent<string>) => {\n e.stopPropagation();\n this.handleGradientSelect(e.detail as ShellGradientPreset);\n }}\n />\n ) : isMenuSwatchPickerSection(section) ? (\n <ds-swatch-picker\n value={section.value}\n groupLabel={section.groupLabel ?? section.header ?? 'Swatch options'}\n options={section.options ?? []}\n sections={section.sections ?? []}\n onDsChange={(e: CustomEvent<string>) => {\n e.stopPropagation();\n this.handleSwatchSelect(e.detail);\n }}\n />\n ) : (\n section.items.map(item => {\n const idx = flatIdx++;\n const isFocused = this.focusedIndex === idx;\n const isRadioItem =\n !hasCompositeSections &&\n this.selectionMode === 'single' &&\n !item.showSwitch;\n return (\n <button\n key={idx}\n type=\"button\"\n class={{\n 'menu-item': true,\n 'ds-choice-item': true,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-focus-ring--visible': isFocused && this.focusRingVisible,\n 'ds-interaction-fill': !item.isInactive,\n 'ds-interaction-fill--selected': !!item.isSelected && !item.isInactive,\n 'menu-item--selected': !!item.isSelected,\n 'ds-control-inactive': !!item.isInactive,\n 'menu-item--destructive': !!item.isDestructive,\n 'menu-item--focused': isFocused,\n }}\n role={\n hasCompositeSections\n ? undefined\n : item.showSwitch\n ? 'menuitemcheckbox'\n : isRadioItem\n ? 'menuitemradio'\n : 'menuitem'\n }\n aria-checked={\n !hasCompositeSections && item.showSwitch\n ? String(!!item.switchValue)\n : isRadioItem\n ? String(!!item.isSelected)\n : undefined\n }\n aria-pressed={\n hasCompositeSections\n ? String(item.showSwitch ? !!item.switchValue : !!item.isSelected)\n : undefined\n }\n aria-current={\n !hasCompositeSections &&\n !item.showSwitch &&\n !isRadioItem &&\n item.isSelected\n ? 'true'\n : undefined\n }\n disabled={item.isInactive}\n tabIndex={hasCompositeSections ? 0 : isFocused ? 0 : -1}\n onMouseDown={() => {\n this.focusRingVisible = false;\n }}\n onClick={() => this.handleItemClick(item)}\n onFocus={() => {\n this.focusedIndex = idx;\n }}\n >\n {isRadioItem && (\n <span\n class=\"menu-item__radio-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <span\n class={{\n 'menu-item__radio': true,\n 'menu-item__radio--checked': !!item.isSelected,\n }}\n >\n {item.isSelected && <span class=\"menu-item__radio-dot\" />}\n </span>\n </span>\n )}\n <div class=\"menu-item__content ds-choice-item__content ds-interaction-fill__content\">\n <ds-text\n class=\"menu-item__label ds-choice-item__label\"\n as=\"span\"\n variant=\"text-body-medium\"\n color={item.isSelected ? 'primary' : 'secondary'}\n >\n {item.label}\n </ds-text>\n {item.subtext && (\n <ds-text\n class=\"menu-item__subtext ds-choice-item__subtext\"\n as=\"span\"\n variant=\"text-body-small\"\n color=\"secondary\"\n >\n {item.subtext}\n </ds-text>\n )}\n </div>\n {item.dot && (\n <span\n class=\"menu-item__dot-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-badge\n class=\"menu-item__dot\"\n variant=\"dot\"\n hasRing={false}\n label=\"\"\n />\n </span>\n )}\n {item.showSwitch && (\n <ds-switch\n class=\"menu-item__switch ds-interaction-fill__content\"\n size=\"md\"\n checked={!!item.switchValue}\n presentation\n />\n )}\n </button>\n );\n })\n )}\n </div>\n ))}\n </div>\n </div>\n </Host>\n );\n }\n}\n",
477
479
  "type": "registry:ui"
478
480
  },
479
481
  {
@@ -98,7 +98,7 @@
98
98
  "peerDependencies": {
99
99
  "required": [
100
100
  "@ds-mo/tokens ^3.1.1",
101
- "@ds-mo/icons ^6.3.0"
101
+ "@ds-mo/icons ^6.4.0"
102
102
  ],
103
103
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
104
104
  }
@@ -239,7 +239,7 @@
239
239
  "peerDependencies": {
240
240
  "required": [
241
241
  "@ds-mo/tokens ^3.1.1",
242
- "@ds-mo/icons ^6.3.0"
242
+ "@ds-mo/icons ^6.4.0"
243
243
  ],
244
244
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
245
245
  }
@@ -36,7 +36,7 @@
36
36
  "Selection and transcript interaction release automatic following.",
37
37
  "Opening saved history, message jumps, and prepended history preserve deliberate position.",
38
38
  "The scroll-to-latest action is absent at the live edge, appears only after the reader moves away, and disappears again after returning to the newest message.",
39
- "Place a composer container with 8px padding on every side in the overlay slot so messages scroll behind it; the scroller measures its live height, reserves matching transcript clearance, and keeps scroll-to-latest above it."
39
+ "Place a composer container with 8px padding on every side in the overlay slot so messages scroll behind it; the shared scroll-overlay controller tracks every composer growth step, reserves matching transcript clearance, begins the fade 8px before the visible composer, and keeps scroll-to-latest above it."
40
40
  ],
41
41
  "responsiveBehavior": [
42
42
  "Transcript width is readable while the scroll region fills available height."
@@ -189,7 +189,7 @@
189
189
  "peerDependencies": {
190
190
  "required": [
191
191
  "@ds-mo/tokens ^3.1.1",
192
- "@ds-mo/icons ^6.3.0"
192
+ "@ds-mo/icons ^6.4.0"
193
193
  ],
194
194
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
195
195
  }
@@ -206,12 +206,12 @@
206
206
  "files": [
207
207
  {
208
208
  "path": "src/wc/components/MessageScroller/MessageScroller.css",
209
- "content": ":host {\n display: block;\n width: 100%;\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n}\n\n.message-scroller {\n position: relative;\n width: 100%;\n height: 100%;\n min-height: 0;\n}\n\n.message-scroller__viewport {\n width: 100%;\n height: 100%;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n box-sizing: border-box;\n}\n\n.message-scroller__viewport:focus-visible {\n outline: var(--dimension-stroke-width-025) solid var(--color-border-bold-brand);\n outline-offset: calc(-1 * var(--dimension-stroke-width-025));\n}\n\n.message-scroller__content {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-300);\n width: 100%;\n max-width: var(--dimension-panel-width-2xl);\n min-height: 100%;\n margin-inline: auto;\n padding-block-start: var(--dimension-space-200);\n padding-inline: var(--dimension-space-200);\n padding-block-end: calc(\n var(--dimension-space-200) + var(--ds-message-scroller-overlay-block-size, 0px)\n );\n box-sizing: border-box;\n}\n\n.message-scroller__overlay {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: var(--dimension-z-index-floating);\n}\n\n.message-scroller__control {\n position: absolute;\n bottom: calc(\n var(--ds-message-scroller-overlay-content-block-size, 0px) + var(--dimension-space-200)\n );\n left: 50%;\n z-index: var(--dimension-z-index-floating);\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--dimension-radius-half);\n background: var(--color-background-transparent);\n box-shadow: var(--effect-elevation-elevated-md);\n transform: translateX(-50%);\n -webkit-backdrop-filter: blur(var(--effect-blur-sm));\n backdrop-filter: blur(var(--effect-blur-sm));\n}\n",
209
+ "content": ":host {\n display: block;\n width: 100%;\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n}\n\n.message-scroller {\n position: relative;\n width: 100%;\n height: 100%;\n min-height: 0;\n}\n\n.message-scroller__viewport {\n width: 100%;\n height: 100%;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n box-sizing: border-box;\n}\n\n.message-scroller__viewport:focus-visible {\n outline: var(--dimension-stroke-width-025) solid var(--color-border-bold-brand);\n outline-offset: calc(-1 * var(--dimension-stroke-width-025));\n}\n\n.message-scroller__content {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-300);\n width: 100%;\n max-width: var(--dimension-panel-width-2xl);\n min-height: 100%;\n margin-inline: auto;\n padding-block-start: var(--dimension-space-200);\n padding-inline: var(--dimension-space-200);\n padding-block-end: calc(\n var(--dimension-space-200) + var(--ds-scroll-overlay-block-size, 0px)\n );\n box-sizing: border-box;\n}\n\n.message-scroller__overlay {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: var(--dimension-z-index-floating);\n}\n\n.message-scroller__control {\n position: absolute;\n bottom: calc(\n var(--ds-scroll-overlay-content-block-size, 0px) + var(--dimension-space-200)\n );\n left: 50%;\n z-index: var(--dimension-z-index-floating);\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--dimension-radius-half);\n background: var(--color-background-transparent);\n box-shadow: var(--effect-elevation-elevated-md);\n transform: translateX(-50%);\n -webkit-backdrop-filter: blur(var(--effect-blur-sm));\n backdrop-filter: blur(var(--effect-blur-sm));\n}\n",
210
210
  "type": "registry:ui"
211
211
  },
212
212
  {
213
213
  "path": "src/wc/components/MessageScroller/MessageScroller.tsx",
214
- "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n State,\n} from '@stencil/core';\nimport type { MessageScrollerPosition } from '../conversation-types';\n\nconst LIVE_EDGE_THRESHOLD = 24;\n\n@Component({\n tag: 'ds-message-scroller',\n styleUrl: 'MessageScroller.css',\n styleUrls: ['../../utils/scroll-edge-fade.css'],\n scoped: true,\n})\nexport class MessageScroller {\n @Element() el!: HTMLElement;\n\n @Prop() busy: boolean = false;\n @Prop() autoFollow: boolean = true;\n @Prop() defaultPosition: MessageScrollerPosition = 'last-anchor';\n @Prop() messagesLabel: string = 'Messages';\n\n @Event() dsReachStart!: EventEmitter<void>;\n\n @State() private showScrollToLatest = false;\n\n private viewport?: HTMLElement;\n private content?: HTMLElement;\n private overlay?: HTMLElement;\n private resizeObserver?: ResizeObserver;\n private overlayResizeObserver?: ResizeObserver;\n private mutationObserver?: MutationObserver;\n private following = true;\n private atStart = false;\n private programmatic = false;\n private previousScrollHeight = 0;\n private overlayBlockSize = 0;\n private overlayContentBlockSize = 0;\n\n componentDidLoad() {\n this.connectObservers();\n requestAnimationFrame(() => this.applyDefaultPosition());\n document.addEventListener('selectionchange', this.handleSelectionChange);\n }\n\n disconnectedCallback() {\n this.resizeObserver?.disconnect();\n this.overlayResizeObserver?.disconnect();\n this.mutationObserver?.disconnect();\n document.removeEventListener('selectionchange', this.handleSelectionChange);\n }\n\n @Method()\n async scrollToMessage(id: string): Promise<boolean> {\n const message = Array.from(\n this.el.querySelectorAll<HTMLElement & { messageId?: string }>('ds-message')\n ).find(item => item.messageId === id || item.getAttribute('message-id') === id);\n if (!message) return false;\n this.following = false;\n this.showScrollToLatest = true;\n message.scrollIntoView({ block: 'start', behavior: this.motionBehavior() });\n return true;\n }\n\n @Method()\n async scrollToStart() {\n this.scrollTo(0);\n this.following = false;\n this.showScrollToLatest = Boolean(\n this.viewport && this.viewport.scrollHeight > this.viewport.clientHeight\n );\n }\n\n @Method()\n async scrollToEnd() {\n this.following = true;\n this.showScrollToLatest = false;\n this.scrollTo(this.viewport?.scrollHeight ?? 0);\n }\n\n private motionBehavior(): ScrollBehavior {\n return matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth';\n }\n\n private scrollTo(top: number) {\n if (!this.viewport) return;\n this.programmatic = true;\n this.viewport.scrollTo({ top, behavior: this.motionBehavior() });\n requestAnimationFrame(() => {\n this.programmatic = false;\n });\n }\n\n private connectObservers() {\n if (!this.viewport || !this.content) return;\n this.previousScrollHeight = this.viewport.scrollHeight;\n this.resizeObserver = new ResizeObserver(() => this.handleContentGrowth());\n this.resizeObserver.observe(this.content);\n this.resizeObserver.observe(this.viewport);\n if (this.overlay) {\n this.overlayResizeObserver = new ResizeObserver(entries => {\n const blockSize = Math.ceil(entries[0]?.contentRect.height ?? 0);\n this.overlayBlockSize = blockSize;\n this.el.style.setProperty('--ds-message-scroller-overlay-block-size', `${blockSize}px`);\n this.syncOverlayContentBlockSize();\n this.handleContentGrowth();\n });\n this.overlayResizeObserver.observe(this.overlay);\n }\n this.mutationObserver = new MutationObserver(() => this.handleContentGrowth());\n this.mutationObserver.observe(this.content, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n }\n\n private handleContentGrowth() {\n if (!this.viewport) return;\n const nextHeight = this.viewport.scrollHeight;\n const delta = nextHeight - this.previousScrollHeight;\n if (delta > 0 && this.viewport.scrollTop <= LIVE_EDGE_THRESHOLD && !this.following) {\n this.viewport.scrollTop += delta;\n } else if (this.autoFollow && this.following) {\n this.viewport.scrollTop = nextHeight;\n }\n this.previousScrollHeight = nextHeight;\n this.showScrollToLatest = !this.following && !this.isAtLiveEdge();\n this.syncContentFade();\n }\n\n private syncContentFade() {\n if (!this.viewport || !this.content) return;\n const fadeEnd = Math.max(this.viewport.scrollTop + this.viewport.clientHeight, 0);\n const fadeStart = Math.max(fadeEnd - this.overlayContentBlockSize, 0);\n this.content.style.setProperty('--ds-scroll-edge-fade-window-start', `${fadeStart}px`);\n this.content.style.setProperty('--ds-scroll-edge-fade-window-end', `${fadeEnd}px`);\n }\n\n private syncOverlayContentBlockSize() {\n if (!this.overlay) return;\n const assigned = this.el.querySelector<HTMLElement>('[slot=\"overlay\"]');\n if (!(assigned instanceof HTMLElement)) {\n this.overlayContentBlockSize = this.overlayBlockSize;\n } else {\n const visibleContent =\n assigned.firstElementChild instanceof HTMLElement ? assigned.firstElementChild : assigned;\n const topInset = Math.max(\n Math.ceil(visibleContent.getBoundingClientRect().top - this.overlay.getBoundingClientRect().top),\n 0\n );\n this.overlayContentBlockSize = Math.max(this.overlayBlockSize - topInset, 0);\n }\n this.el.style.setProperty(\n '--ds-message-scroller-overlay-content-block-size',\n `${this.overlayContentBlockSize}px`\n );\n }\n\n private applyDefaultPosition() {\n if (!this.viewport) return;\n if (this.defaultPosition === 'start') {\n this.viewport.scrollTop = 0;\n this.following = false;\n this.showScrollToLatest = this.viewport.scrollHeight > this.viewport.clientHeight;\n this.syncContentFade();\n return;\n }\n if (this.defaultPosition === 'last-anchor') {\n const anchors = this.el.querySelectorAll<HTMLElement>('ds-message[scroll-anchor]');\n const last = anchors.item(anchors.length - 1);\n if (last) {\n last.scrollIntoView({ block: 'start' });\n this.following = this.isAtLiveEdge();\n this.showScrollToLatest = !this.following;\n this.syncContentFade();\n return;\n }\n }\n this.viewport.scrollTop = this.viewport.scrollHeight;\n this.following = true;\n this.showScrollToLatest = false;\n this.syncContentFade();\n }\n\n private isAtLiveEdge(): boolean {\n if (!this.viewport) return true;\n return (\n this.viewport.scrollHeight - this.viewport.clientHeight - this.viewport.scrollTop <=\n LIVE_EDGE_THRESHOLD\n );\n }\n\n private handleScroll = () => {\n if (!this.viewport) return;\n this.syncContentFade();\n if (this.programmatic) return;\n this.following = this.isAtLiveEdge();\n this.showScrollToLatest = !this.following;\n const nextAtStart = this.viewport.scrollTop <= LIVE_EDGE_THRESHOLD;\n if (nextAtStart && !this.atStart) this.dsReachStart.emit();\n this.atStart = nextAtStart;\n };\n\n private releaseFollow = () => {\n this.following = false;\n this.showScrollToLatest = !this.isAtLiveEdge();\n };\n\n private handleSelectionChange = () => {\n const selection = document.getSelection();\n if (\n selection &&\n !selection.isCollapsed &&\n selection.anchorNode &&\n this.el.contains(selection.anchorNode)\n ) {\n this.following = false;\n this.showScrollToLatest = !this.isAtLiveEdge();\n }\n };\n\n render() {\n return (\n <Host>\n <div class=\"message-scroller\">\n <div\n class=\"message-scroller__viewport\"\n ref={element => {\n this.viewport = element;\n }}\n role=\"region\"\n aria-label={this.messagesLabel}\n tabIndex={0}\n onScroll={this.handleScroll}\n onPointerDown={this.releaseFollow}\n onKeyDown={(event: KeyboardEvent) => {\n if (['ArrowUp', 'PageUp', 'Home'].includes(event.key)) this.releaseFollow();\n }}\n >\n <div\n class=\"message-scroller__content scroll-edge-fade--block-window\"\n ref={element => {\n this.content = element;\n }}\n role=\"log\"\n aria-live=\"polite\"\n aria-relevant=\"additions\"\n aria-busy={this.busy ? 'true' : undefined}\n >\n <slot />\n </div>\n </div>\n <div\n class=\"message-scroller__overlay\"\n ref={element => {\n this.overlay = element;\n }}\n >\n <slot name=\"overlay\" />\n </div>\n {this.showScrollToLatest ? (\n <div class=\"message-scroller__control\">\n <ds-tooltip label=\"Scroll to latest message\" side=\"top\" size=\"sm\">\n <ds-button-unfilled\n variant=\"icon\"\n icon=\"ChevronDown\"\n size=\"md\"\n rounded\n hasBorder={false}\n aria-label=\"Scroll to latest message\"\n onDsClick={() => void this.scrollToEnd()}\n />\n </ds-tooltip>\n </div>\n ) : null}\n </div>\n </Host>\n );\n }\n}\n",
214
+ "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n State,\n} from '@stencil/core';\nimport type { MessageScrollerPosition } from '../conversation-types';\nimport { ScrollOverlayController } from '../../utils/scroll-overlay-controller';\n\nconst LIVE_EDGE_THRESHOLD = 24;\n\n@Component({\n tag: 'ds-message-scroller',\n styleUrl: 'MessageScroller.css',\n styleUrls: ['../../utils/scroll-edge-fade.css'],\n scoped: true,\n})\nexport class MessageScroller {\n @Element() el!: HTMLElement;\n\n @Prop() busy: boolean = false;\n @Prop() autoFollow: boolean = true;\n @Prop() defaultPosition: MessageScrollerPosition = 'last-anchor';\n @Prop() messagesLabel: string = 'Messages';\n\n @Event() dsReachStart!: EventEmitter<void>;\n\n @State() private showScrollToLatest = false;\n\n private viewport?: HTMLElement;\n private content?: HTMLElement;\n private overlay?: HTMLElement;\n private scrollOverlayController?: ScrollOverlayController;\n private mutationObserver?: MutationObserver;\n private following = true;\n private atStart = false;\n private programmatic = false;\n private previousScrollHeight = 0;\n\n componentDidLoad() {\n this.connectObservers();\n requestAnimationFrame(() => this.applyDefaultPosition());\n document.addEventListener('selectionchange', this.handleSelectionChange);\n }\n\n disconnectedCallback() {\n this.scrollOverlayController?.disconnect();\n this.mutationObserver?.disconnect();\n document.removeEventListener('selectionchange', this.handleSelectionChange);\n }\n\n @Method()\n async scrollToMessage(id: string): Promise<boolean> {\n const message = Array.from(\n this.el.querySelectorAll<HTMLElement & { messageId?: string }>('ds-message')\n ).find(item => item.messageId === id || item.getAttribute('message-id') === id);\n if (!message) return false;\n this.following = false;\n this.showScrollToLatest = true;\n message.scrollIntoView({ block: 'start', behavior: this.motionBehavior() });\n return true;\n }\n\n @Method()\n async scrollToStart() {\n this.scrollTo(0);\n this.following = false;\n this.showScrollToLatest = Boolean(\n this.viewport && this.viewport.scrollHeight > this.viewport.clientHeight\n );\n }\n\n @Method()\n async scrollToEnd() {\n this.following = true;\n this.showScrollToLatest = false;\n this.scrollTo(this.viewport?.scrollHeight ?? 0);\n }\n\n private motionBehavior(): ScrollBehavior {\n return matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth';\n }\n\n private scrollTo(top: number) {\n if (!this.viewport) return;\n this.programmatic = true;\n this.viewport.scrollTo({ top, behavior: this.motionBehavior() });\n requestAnimationFrame(() => {\n this.programmatic = false;\n });\n }\n\n private connectObservers() {\n if (!this.viewport || !this.content || !this.overlay) return;\n this.previousScrollHeight = this.viewport.scrollHeight;\n this.scrollOverlayController = new ScrollOverlayController({\n host: this.el,\n viewport: this.viewport,\n content: this.content,\n overlay: this.overlay,\n onGeometryChange: () => this.handleContentGrowth(),\n });\n this.scrollOverlayController.connect();\n this.mutationObserver = new MutationObserver(() => this.handleContentGrowth());\n this.mutationObserver.observe(this.content, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n }\n\n private handleContentGrowth() {\n if (!this.viewport) return;\n const nextHeight = this.viewport.scrollHeight;\n const delta = nextHeight - this.previousScrollHeight;\n if (delta > 0 && this.viewport.scrollTop <= LIVE_EDGE_THRESHOLD && !this.following) {\n this.viewport.scrollTop += delta;\n } else if (this.autoFollow && this.following) {\n this.viewport.scrollTop = nextHeight;\n }\n this.previousScrollHeight = nextHeight;\n this.showScrollToLatest = !this.following && !this.isAtLiveEdge();\n this.scrollOverlayController?.sync();\n }\n\n private applyDefaultPosition() {\n if (!this.viewport) return;\n if (this.defaultPosition === 'start') {\n this.viewport.scrollTop = 0;\n this.following = false;\n this.showScrollToLatest = this.viewport.scrollHeight > this.viewport.clientHeight;\n this.scrollOverlayController?.sync();\n return;\n }\n if (this.defaultPosition === 'last-anchor') {\n const anchors = this.el.querySelectorAll<HTMLElement>('ds-message[scroll-anchor]');\n const last = anchors.item(anchors.length - 1);\n if (last) {\n last.scrollIntoView({ block: 'start' });\n this.following = this.isAtLiveEdge();\n this.showScrollToLatest = !this.following;\n this.scrollOverlayController?.sync();\n return;\n }\n }\n this.viewport.scrollTop = this.viewport.scrollHeight;\n this.following = true;\n this.showScrollToLatest = false;\n this.scrollOverlayController?.sync();\n }\n\n private isAtLiveEdge(): boolean {\n if (!this.viewport) return true;\n return (\n this.viewport.scrollHeight - this.viewport.clientHeight - this.viewport.scrollTop <=\n LIVE_EDGE_THRESHOLD\n );\n }\n\n private handleScroll = () => {\n if (!this.viewport) return;\n this.scrollOverlayController?.sync();\n if (this.programmatic) return;\n this.following = this.isAtLiveEdge();\n this.showScrollToLatest = !this.following;\n const nextAtStart = this.viewport.scrollTop <= LIVE_EDGE_THRESHOLD;\n if (nextAtStart && !this.atStart) this.dsReachStart.emit();\n this.atStart = nextAtStart;\n };\n\n private releaseFollow = () => {\n this.following = false;\n this.showScrollToLatest = !this.isAtLiveEdge();\n };\n\n private handleSelectionChange = () => {\n const selection = document.getSelection();\n if (\n selection &&\n !selection.isCollapsed &&\n selection.anchorNode &&\n this.el.contains(selection.anchorNode)\n ) {\n this.following = false;\n this.showScrollToLatest = !this.isAtLiveEdge();\n }\n };\n\n render() {\n return (\n <Host>\n <div class=\"message-scroller\">\n <div\n class=\"message-scroller__viewport\"\n ref={element => {\n this.viewport = element;\n }}\n role=\"region\"\n aria-label={this.messagesLabel}\n tabIndex={0}\n onScroll={this.handleScroll}\n onPointerDown={this.releaseFollow}\n onKeyDown={(event: KeyboardEvent) => {\n if (['ArrowUp', 'PageUp', 'Home'].includes(event.key)) this.releaseFollow();\n }}\n >\n <div\n class=\"message-scroller__content scroll-edge-fade--block-window\"\n ref={element => {\n this.content = element;\n }}\n role=\"log\"\n aria-live=\"polite\"\n aria-relevant=\"additions\"\n aria-busy={this.busy ? 'true' : undefined}\n >\n <slot />\n </div>\n </div>\n <div\n class=\"message-scroller__overlay\"\n ref={element => {\n this.overlay = element;\n }}\n >\n <slot name=\"overlay\" />\n </div>\n {this.showScrollToLatest ? (\n <div class=\"message-scroller__control\">\n <ds-tooltip label=\"Scroll to latest message\" side=\"top\" size=\"sm\">\n <ds-button-unfilled\n variant=\"icon\"\n icon=\"ChevronDown\"\n size=\"md\"\n rounded\n hasBorder={false}\n aria-label=\"Scroll to latest message\"\n onDsClick={() => void this.scrollToEnd()}\n />\n </ds-tooltip>\n </div>\n ) : null}\n </div>\n </Host>\n );\n }\n}\n",
215
215
  "type": "registry:ui"
216
216
  }
217
217
  ]
@@ -228,7 +228,7 @@
228
228
  "peerDependencies": {
229
229
  "required": [
230
230
  "@ds-mo/tokens ^3.1.1",
231
- "@ds-mo/icons ^6.3.0"
231
+ "@ds-mo/icons ^6.4.0"
232
232
  ],
233
233
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
234
234
  }
@@ -251,7 +251,7 @@
251
251
  "peerDependencies": {
252
252
  "required": [
253
253
  "@ds-mo/tokens ^3.1.1",
254
- "@ds-mo/icons ^6.3.0"
254
+ "@ds-mo/icons ^6.4.0"
255
255
  ],
256
256
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
257
257
  }
@@ -598,7 +598,7 @@
598
598
  "peerDependencies": {
599
599
  "required": [
600
600
  "@ds-mo/tokens ^3.1.1",
601
- "@ds-mo/icons ^6.3.0"
601
+ "@ds-mo/icons ^6.4.0"
602
602
  ],
603
603
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
604
604
  }
@@ -216,7 +216,7 @@
216
216
  "peerDependencies": {
217
217
  "required": [
218
218
  "@ds-mo/tokens ^3.1.1",
219
- "@ds-mo/icons ^6.3.0"
219
+ "@ds-mo/icons ^6.4.0"
220
220
  ],
221
221
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
222
222
  }
@@ -313,7 +313,7 @@
313
313
  "peerDependencies": {
314
314
  "required": [
315
315
  "@ds-mo/tokens ^3.1.1",
316
- "@ds-mo/icons ^6.3.0"
316
+ "@ds-mo/icons ^6.4.0"
317
317
  ],
318
318
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
319
319
  }
@@ -30,7 +30,7 @@
30
30
  ],
31
31
  "accessibility": [
32
32
  "Provide an aria-label that names the searched domain, such as Search agent chats or Search messages.",
33
- "The standard search clear action restores focus, exposes its localized name in a tooltip, and emits both the empty query and dsClear.",
33
+ "The standard search clear action restores focus, uses the concise localized clearLabel by itself for both its tooltip and accessible name, and emits both the empty query and dsClear.",
34
34
  "When showFilter is enabled, provide stable filterTriggerId and filterControls values, synchronize filterExpanded with the controlled menu, and restore focus after selection; filterAriaLabel also labels its tooltip."
35
35
  ],
36
36
  "states": [
@@ -355,7 +355,7 @@
355
355
  "peerDependencies": {
356
356
  "required": [
357
357
  "@ds-mo/tokens ^3.1.1",
358
- "@ds-mo/icons ^6.3.0"
358
+ "@ds-mo/icons ^6.4.0"
359
359
  ],
360
360
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
361
361
  }
@@ -474,7 +474,7 @@
474
474
  "peerDependencies": {
475
475
  "required": [
476
476
  "@ds-mo/tokens ^3.1.1",
477
- "@ds-mo/icons ^6.3.0"
477
+ "@ds-mo/icons ^6.4.0"
478
478
  ],
479
479
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
480
480
  }
@@ -346,7 +346,7 @@
346
346
  "peerDependencies": {
347
347
  "required": [
348
348
  "@ds-mo/tokens ^3.1.1",
349
- "@ds-mo/icons ^6.3.0"
349
+ "@ds-mo/icons ^6.4.0"
350
350
  ],
351
351
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
352
352
  }