@ds-mo/ui 15.5.1 → 15.6.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 (250) hide show
  1. package/dist/.package-ready.json +1 -1
  2. package/dist/agent-patterns.json +1 -1
  3. package/dist/agent.json +46 -21
  4. package/dist/angular/ds-data-preferences.d.ts +1 -1
  5. package/dist/angular/ds-data-preferences.js +3 -3
  6. package/dist/angular/ds-menu.d.ts +7 -1
  7. package/dist/angular/ds-menu.js +5 -2
  8. package/dist/angular/ds-table.d.ts +1 -1
  9. package/dist/angular/ds-table.js +3 -3
  10. package/dist/components/THIRD-PARTY-NOTICES +1 -1
  11. package/dist/components/bundled-dependencies.json +1 -1
  12. package/dist/components/ds-agent-questionnaire.js +1 -1
  13. package/dist/components/ds-agent-response.js +1 -1
  14. package/dist/components/ds-banner.js +1 -1
  15. package/dist/components/ds-bar-nav.js +1 -1
  16. package/dist/components/ds-bar-page-title.js +1 -1
  17. package/dist/components/ds-bar-title.js +1 -1
  18. package/dist/components/ds-bar-workflow.js +1 -1
  19. package/dist/components/ds-button-unfilled.js +1 -1
  20. package/dist/components/ds-calendar.js +1 -1
  21. package/dist/components/ds-card-chart.js +1 -1
  22. package/dist/components/ds-card-overview.js +1 -1
  23. package/dist/components/ds-card-setting.js +1 -1
  24. package/dist/components/ds-code-block.js +1 -1
  25. package/dist/components/ds-data-customize.js +1 -1
  26. package/dist/components/ds-data-filter.js +1 -1
  27. package/dist/components/ds-data-group.js +1 -1
  28. package/dist/components/ds-data-preferences.js +1 -1
  29. package/dist/components/ds-data-preferences.js.map +1 -1
  30. package/dist/components/ds-data-saved-views.js +1 -1
  31. package/dist/components/ds-data-search.js +1 -1
  32. package/dist/components/ds-data-search.js.map +1 -1
  33. package/dist/components/ds-data-sort.js +1 -1
  34. package/dist/components/ds-filter-menu.js +1 -1
  35. package/dist/components/ds-input-date.js +1 -1
  36. package/dist/components/ds-input-time.js +1 -1
  37. package/dist/components/ds-input.js +1 -1
  38. package/dist/components/ds-markdown.js +1 -1
  39. package/dist/components/ds-menu.js +1 -1
  40. package/dist/components/ds-message-actions.js +1 -1
  41. package/dist/components/ds-message-composer.js +1 -1
  42. package/dist/components/ds-message-scroller.js +1 -1
  43. package/dist/components/ds-mobile-header.js +1 -1
  44. package/dist/components/ds-mobile-section-switcher.js +1 -1
  45. package/dist/components/ds-mobile-sheet-nav.js +1 -1
  46. package/dist/components/ds-modal.js +1 -1
  47. package/dist/components/ds-pagination.js +1 -1
  48. package/dist/components/ds-panel-nav.js +1 -1
  49. package/dist/components/ds-panel-tool-header.js +1 -1
  50. package/dist/components/ds-panel-tool-search.js +1 -1
  51. package/dist/components/ds-panel-tools.js +1 -1
  52. package/dist/components/ds-select.js +1 -1
  53. package/dist/components/ds-shell-app.js +1 -1
  54. package/dist/components/ds-shell-tools.js +1 -1
  55. package/dist/components/ds-table.js +1 -1
  56. package/dist/components/ds-table.js.map +1 -1
  57. package/dist/components/ds-toast.js +1 -1
  58. package/dist/components/ds-toast.js.map +1 -1
  59. package/dist/components/ds-tooltip.js +1 -1
  60. package/dist/components/{p-sDm8BEMy.js → p-B-pkoIX9.js} +2 -2
  61. package/dist/components/{p-DYsRXun3.js → p-B3o9zn3Y.js} +2 -2
  62. package/dist/components/p-BR9Pjp15.js +2 -0
  63. package/dist/components/{p-CXlklL-0.js.map → p-BR9Pjp15.js.map} +1 -1
  64. package/dist/components/{p-CcwxySms.js → p-BZY40-pE.js} +2 -2
  65. package/dist/components/{p-BLeHHoVb.js → p-BqipGGua.js} +2 -2
  66. package/dist/components/{p-BuDxlhnQ.js → p-C6kUn-vb.js} +2 -2
  67. package/dist/components/p-CElsKlxj.js +2 -0
  68. package/dist/components/{p-CzdE3M3g.js.map → p-CElsKlxj.js.map} +1 -1
  69. package/dist/components/p-CIMyD_6H.js +2 -0
  70. package/dist/components/{p-C5BBIDVm.js.map → p-CIMyD_6H.js.map} +1 -1
  71. package/dist/components/{p-C35Tki38.js → p-CSxwWcwW.js} +2 -2
  72. package/dist/components/p-CUYLGe5d.js +2 -0
  73. package/dist/components/{p-D9_iDlkT.js.map → p-CUYLGe5d.js.map} +1 -1
  74. package/dist/components/{p-BlbfbYg6.js → p-CaFXrn_8.js} +2 -2
  75. package/dist/components/{p-DZLtFn-r.js → p-DObhRLBa.js} +2 -2
  76. package/dist/components/{p-bQE1NZVe.js → p-DP07JCed.js} +2 -2
  77. package/dist/components/p-DYGllrVT.js +2 -0
  78. package/dist/components/{p-CNeDJTGX.js.map → p-DYGllrVT.js.map} +1 -1
  79. package/dist/components/{p-C378wwRj.js → p-DYtVsnDS.js} +2 -2
  80. package/dist/components/p-DbW8S5qe.js +2 -0
  81. package/dist/components/p-DbW8S5qe.js.map +1 -0
  82. package/dist/components/p-DcxY_pdJ.js +2 -0
  83. package/dist/components/{p-Df9S0aWi.js.map → p-DcxY_pdJ.js.map} +1 -1
  84. package/dist/components/{p-C9H-3JOA.js → p-DdFeWC-e.js} +2 -2
  85. package/dist/components/p-DlCLfwP4.js +2 -0
  86. package/dist/components/p-DuCfCvPs.js +2 -0
  87. package/dist/components/p-DuCfCvPs.js.map +1 -0
  88. package/dist/components/{p-BYrBU_U9.js → p-HMDczkQf.js} +2 -2
  89. package/dist/components/p-HfZ1gI2q.js +2 -0
  90. package/dist/components/{p-Da3aFRk2.js.map → p-HfZ1gI2q.js.map} +1 -1
  91. package/dist/components/{p--VQc5GDy.js → p-KA8srEed.js} +2 -2
  92. package/dist/components/{p-BfVMaZZE.js → p-OJ5WeR05.js} +2 -2
  93. package/dist/components/p-Ouk6FCxc.js +2 -0
  94. package/dist/components/{p-a-cIN_BX.js.map → p-Ouk6FCxc.js.map} +1 -1
  95. package/dist/components/p-leZFu-cd.js +2 -0
  96. package/dist/components/{p-BuGlC5r8.js.map → p-leZFu-cd.js.map} +1 -1
  97. package/dist/components/{p-DPwptvZl.js → p-wXKIVUTn.js} +2 -2
  98. package/dist/components/p-xMBaOeKa.js +2 -0
  99. package/dist/components/p-xMBaOeKa.js.map +1 -0
  100. package/dist/docs/components.json +207 -29
  101. package/dist/lint/index.js +1 -1
  102. package/dist/mcp-data/registry/agent-activity.json +1 -1
  103. package/dist/mcp-data/registry/agent-questionnaire.json +1 -1
  104. package/dist/mcp-data/registry/agent-response.json +1 -1
  105. package/dist/mcp-data/registry/agent-source-list.json +1 -1
  106. package/dist/mcp-data/registry/agent-tool-call.json +1 -1
  107. package/dist/mcp-data/registry/attachment-list.json +1 -1
  108. package/dist/mcp-data/registry/avatar.json +1 -1
  109. package/dist/mcp-data/registry/badge.json +1 -1
  110. package/dist/mcp-data/registry/banner.json +1 -1
  111. package/dist/mcp-data/registry/bar-action.json +1 -1
  112. package/dist/mcp-data/registry/bar-nav.json +1 -1
  113. package/dist/mcp-data/registry/bar-page-title.json +1 -1
  114. package/dist/mcp-data/registry/bar-title.json +1 -1
  115. package/dist/mcp-data/registry/bar-workflow.json +1 -1
  116. package/dist/mcp-data/registry/breadcrumb.json +1 -1
  117. package/dist/mcp-data/registry/button-filled.json +1 -1
  118. package/dist/mcp-data/registry/button-inverted.json +1 -1
  119. package/dist/mcp-data/registry/button-unfilled.json +1 -1
  120. package/dist/mcp-data/registry/calendar.json +1 -1
  121. package/dist/mcp-data/registry/card-action-center.json +1 -1
  122. package/dist/mcp-data/registry/card-chart.json +1 -1
  123. package/dist/mcp-data/registry/card-navigation.json +1 -1
  124. package/dist/mcp-data/registry/card-overview.json +1 -1
  125. package/dist/mcp-data/registry/card-setting.json +1 -1
  126. package/dist/mcp-data/registry/card-settings-scope.json +1 -1
  127. package/dist/mcp-data/registry/chart-legend.json +1 -1
  128. package/dist/mcp-data/registry/chart.json +1 -1
  129. package/dist/mcp-data/registry/checkbox-group.json +1 -1
  130. package/dist/mcp-data/registry/checkbox.json +1 -1
  131. package/dist/mcp-data/registry/chip.json +1 -1
  132. package/dist/mcp-data/registry/code-block.json +1 -1
  133. package/dist/mcp-data/registry/conversation-list-item.json +1 -1
  134. package/dist/mcp-data/registry/conversation-list-section.json +1 -1
  135. package/dist/mcp-data/registry/conversation-list.json +1 -1
  136. package/dist/mcp-data/registry/data-customize.json +1 -1
  137. package/dist/mcp-data/registry/data-filter.json +1 -1
  138. package/dist/mcp-data/registry/data-group.json +1 -1
  139. package/dist/mcp-data/registry/data-preferences.json +22 -6
  140. package/dist/mcp-data/registry/data-saved-views.json +1 -1
  141. package/dist/mcp-data/registry/data-search.json +1 -1
  142. package/dist/mcp-data/registry/data-sort.json +1 -1
  143. package/dist/mcp-data/registry/data-toolbar.json +1 -1
  144. package/dist/mcp-data/registry/divider.json +1 -1
  145. package/dist/mcp-data/registry/empty-state.json +1 -1
  146. package/dist/mcp-data/registry/field.json +1 -1
  147. package/dist/mcp-data/registry/filter-menu.json +1 -1
  148. package/dist/mcp-data/registry/icon.json +1 -1
  149. package/dist/mcp-data/registry/inline-banner-settings.json +1 -1
  150. package/dist/mcp-data/registry/input-date.json +1 -1
  151. package/dist/mcp-data/registry/input-time.json +1 -1
  152. package/dist/mcp-data/registry/input.json +1 -1
  153. package/dist/mcp-data/registry/loader.json +1 -1
  154. package/dist/mcp-data/registry/map-cluster-marker.json +1 -1
  155. package/dist/mcp-data/registry/map-entity-marker.json +1 -1
  156. package/dist/mcp-data/registry/markdown.json +1 -1
  157. package/dist/mcp-data/registry/menu.json +31 -12
  158. package/dist/mcp-data/registry/message-actions.json +1 -1
  159. package/dist/mcp-data/registry/message-bubble.json +1 -1
  160. package/dist/mcp-data/registry/message-composer.json +1 -1
  161. package/dist/mcp-data/registry/message-scroller.json +1 -1
  162. package/dist/mcp-data/registry/message.json +1 -1
  163. package/dist/mcp-data/registry/mobile-bar-nav.json +1 -1
  164. package/dist/mcp-data/registry/mobile-header.json +1 -1
  165. package/dist/mcp-data/registry/mobile-section-switcher.json +1 -1
  166. package/dist/mcp-data/registry/mobile-sheet-nav.json +1 -1
  167. package/dist/mcp-data/registry/modal.json +1 -1
  168. package/dist/mcp-data/registry/pagination.json +1 -1
  169. package/dist/mcp-data/registry/panel-nav.json +1 -1
  170. package/dist/mcp-data/registry/panel-sub-nav.json +1 -1
  171. package/dist/mcp-data/registry/panel-tool-header.json +1 -1
  172. package/dist/mcp-data/registry/panel-tool-search.json +1 -1
  173. package/dist/mcp-data/registry/panel-tools.json +1 -1
  174. package/dist/mcp-data/registry/paper-texture.json +1 -1
  175. package/dist/mcp-data/registry/radio-tile.json +1 -1
  176. package/dist/mcp-data/registry/radio.json +1 -1
  177. package/dist/mcp-data/registry/registry.json +46 -21
  178. package/dist/mcp-data/registry/score.json +1 -1
  179. package/dist/mcp-data/registry/scroll-overlay.json +1 -1
  180. package/dist/mcp-data/registry/select.json +1 -1
  181. package/dist/mcp-data/registry/setting-row-checkbox.json +1 -1
  182. package/dist/mcp-data/registry/setting-row-radio.json +1 -1
  183. package/dist/mcp-data/registry/setting-row-toggle.json +1 -1
  184. package/dist/mcp-data/registry/shell-app.json +1 -1
  185. package/dist/mcp-data/registry/shell-page.json +1 -1
  186. package/dist/mcp-data/registry/shell-tools.json +1 -1
  187. package/dist/mcp-data/registry/skeleton.json +1 -1
  188. package/dist/mcp-data/registry/slider.json +1 -1
  189. package/dist/mcp-data/registry/swatch-picker.json +1 -1
  190. package/dist/mcp-data/registry/switch.json +1 -1
  191. package/dist/mcp-data/registry/tab-group.json +1 -1
  192. package/dist/mcp-data/registry/table.json +59 -19
  193. package/dist/mcp-data/registry/tag.json +1 -1
  194. package/dist/mcp-data/registry/text.json +1 -1
  195. package/dist/mcp-data/registry/textarea.json +1 -1
  196. package/dist/mcp-data/registry/time-picker.json +1 -1
  197. package/dist/mcp-data/registry/toast.json +1 -1
  198. package/dist/mcp-data/registry/tooltip-chart.json +1 -1
  199. package/dist/mcp-data/registry/tooltip.json +1 -1
  200. package/dist/mcp-data/registry/typing-indicator.json +1 -1
  201. package/dist/react/ds-menu.d.ts +2 -1
  202. package/dist/react/ds-menu.js +1 -0
  203. package/dist/styles/table.css +41 -0
  204. package/dist/types/components/DataPreferences/DataPreferences.d.ts +3 -0
  205. package/dist/types/components/Menu/Menu.d.ts +5 -1
  206. package/dist/types/components/Menu/menu-types.d.ts +36 -0
  207. package/dist/types/components/Table/Table.d.ts +11 -2
  208. package/dist/types/components/Table/index.d.ts +1 -1
  209. package/dist/types/components/Table/table-column-customizer.d.ts +23 -4
  210. package/dist/types/components/Table/table-layout-controller.d.ts +2 -1
  211. package/dist/types/components/Table/table-types.d.ts +3 -1
  212. package/dist/types/components.d.ts +42 -6
  213. package/dist/vue/ds-data-preferences.js +1 -0
  214. package/dist/vue/ds-menu.js +2 -0
  215. package/dist/vue/ds-table.js +2 -0
  216. package/package.json +3 -3
  217. package/dist/components/p-BuGlC5r8.js +0 -2
  218. package/dist/components/p-C5BBIDVm.js +0 -2
  219. package/dist/components/p-CE6MaXBg.js +0 -2
  220. package/dist/components/p-CE6MaXBg.js.map +0 -1
  221. package/dist/components/p-CNeDJTGX.js +0 -2
  222. package/dist/components/p-CXlklL-0.js +0 -2
  223. package/dist/components/p-CzdE3M3g.js +0 -2
  224. package/dist/components/p-D9_iDlkT.js +0 -2
  225. package/dist/components/p-Da3aFRk2.js +0 -2
  226. package/dist/components/p-Df9S0aWi.js +0 -2
  227. package/dist/components/p-DvGrsLgt.js +0 -2
  228. package/dist/components/p-DvGrsLgt.js.map +0 -1
  229. package/dist/components/p-a-cIN_BX.js +0 -2
  230. package/dist/components/p-aD5cw_m2.js +0 -2
  231. package/dist/components/p-aD5cw_m2.js.map +0 -1
  232. package/dist/components/p-kDZ0sEII.js +0 -2
  233. package/dist/components/p-t2Ebn8si.js +0 -2
  234. package/dist/components/p-t2Ebn8si.js.map +0 -1
  235. /package/dist/components/{p-sDm8BEMy.js.map → p-B-pkoIX9.js.map} +0 -0
  236. /package/dist/components/{p-DYsRXun3.js.map → p-B3o9zn3Y.js.map} +0 -0
  237. /package/dist/components/{p-CcwxySms.js.map → p-BZY40-pE.js.map} +0 -0
  238. /package/dist/components/{p-BLeHHoVb.js.map → p-BqipGGua.js.map} +0 -0
  239. /package/dist/components/{p-BuDxlhnQ.js.map → p-C6kUn-vb.js.map} +0 -0
  240. /package/dist/components/{p-C35Tki38.js.map → p-CSxwWcwW.js.map} +0 -0
  241. /package/dist/components/{p-BlbfbYg6.js.map → p-CaFXrn_8.js.map} +0 -0
  242. /package/dist/components/{p-DZLtFn-r.js.map → p-DObhRLBa.js.map} +0 -0
  243. /package/dist/components/{p-bQE1NZVe.js.map → p-DP07JCed.js.map} +0 -0
  244. /package/dist/components/{p-C378wwRj.js.map → p-DYtVsnDS.js.map} +0 -0
  245. /package/dist/components/{p-C9H-3JOA.js.map → p-DdFeWC-e.js.map} +0 -0
  246. /package/dist/components/{p-kDZ0sEII.js.map → p-DlCLfwP4.js.map} +0 -0
  247. /package/dist/components/{p-BYrBU_U9.js.map → p-HMDczkQf.js.map} +0 -0
  248. /package/dist/components/{p--VQc5GDy.js.map → p-KA8srEed.js.map} +0 -0
  249. /package/dist/components/{p-BfVMaZZE.js.map → p-OJ5WeR05.js.map} +0 -0
  250. /package/dist/components/{p-DPwptvZl.js.map → p-wXKIVUTn.js.map} +0 -0
@@ -218,7 +218,7 @@
218
218
  },
219
219
  "peerDependencies": {
220
220
  "required": [
221
- "@ds-mo/tokens ^6.6.1",
221
+ "@ds-mo/tokens ^6.7.0",
222
222
  "@ds-mo/icons ^8.1.1"
223
223
  ],
224
224
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -899,7 +899,7 @@
899
899
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: anchor, boundary, filters, matchModes, values.",
900
900
  "peerDependencies": {
901
901
  "required": [
902
- "@ds-mo/tokens ^6.6.1",
902
+ "@ds-mo/tokens ^6.7.0",
903
903
  "@ds-mo/icons ^8.1.1"
904
904
  ],
905
905
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -221,7 +221,7 @@
221
221
  },
222
222
  "peerDependencies": {
223
223
  "required": [
224
- "@ds-mo/tokens ^6.6.1",
224
+ "@ds-mo/tokens ^6.7.0",
225
225
  "@ds-mo/icons ^8.1.1"
226
226
  ],
227
227
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -146,7 +146,7 @@
146
146
  },
147
147
  "peerDependencies": {
148
148
  "required": [
149
- "@ds-mo/tokens ^6.6.1",
149
+ "@ds-mo/tokens ^6.7.0",
150
150
  "@ds-mo/icons ^8.1.1"
151
151
  ],
152
152
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -532,7 +532,7 @@
532
532
  },
533
533
  "peerDependencies": {
534
534
  "required": [
535
- "@ds-mo/tokens ^6.6.1",
535
+ "@ds-mo/tokens ^6.7.0",
536
536
  "@ds-mo/icons ^8.1.1"
537
537
  ],
538
538
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -570,7 +570,7 @@
570
570
  },
571
571
  "peerDependencies": {
572
572
  "required": [
573
- "@ds-mo/tokens ^6.6.1",
573
+ "@ds-mo/tokens ^6.7.0",
574
574
  "@ds-mo/icons ^8.1.1"
575
575
  ],
576
576
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -1037,7 +1037,7 @@
1037
1037
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: suggestions, tokens.",
1038
1038
  "peerDependencies": {
1039
1039
  "required": [
1040
- "@ds-mo/tokens ^6.6.1",
1040
+ "@ds-mo/tokens ^6.7.0",
1041
1041
  "@ds-mo/icons ^8.1.1"
1042
1042
  ],
1043
1043
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -191,7 +191,7 @@
191
191
  },
192
192
  "peerDependencies": {
193
193
  "required": [
194
- "@ds-mo/tokens ^6.6.1",
194
+ "@ds-mo/tokens ^6.7.0",
195
195
  "@ds-mo/icons ^8.1.1"
196
196
  ],
197
197
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -234,7 +234,7 @@
234
234
  },
235
235
  "peerDependencies": {
236
236
  "required": [
237
- "@ds-mo/tokens ^6.6.1",
237
+ "@ds-mo/tokens ^6.7.0",
238
238
  "@ds-mo/icons ^8.1.1"
239
239
  ],
240
240
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -290,7 +290,7 @@
290
290
  },
291
291
  "peerDependencies": {
292
292
  "required": [
293
- "@ds-mo/tokens ^6.6.1",
293
+ "@ds-mo/tokens ^6.7.0",
294
294
  "@ds-mo/icons ^8.1.1"
295
295
  ],
296
296
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -138,7 +138,7 @@
138
138
  },
139
139
  "peerDependencies": {
140
140
  "required": [
141
- "@ds-mo/tokens ^6.6.1",
141
+ "@ds-mo/tokens ^6.7.0",
142
142
  "@ds-mo/icons ^8.1.1"
143
143
  ],
144
144
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
3
  "name": "menu",
4
4
  "title": "Menu",
5
- "description": "Anchored action menu with flat commands, grouped sections, single-selection choices, supplemental notification dots or trailing tags, prefix icons, reorderable rows, switches, and destructive actions.",
5
+ "description": "Anchored action menu with flat commands, grouped sections, single-selection choices, supplemental notification dots or trailing tags, prefix icons, reorderable rows, switches, independent trailing actions, separate trailing toggles, and destructive actions.",
6
6
  "type": "registry:ui",
7
7
  "meta": {
8
8
  "library": "@ds-mo/ui",
@@ -13,7 +13,7 @@
13
13
  "intent": {
14
14
  "audience": "general",
15
15
  "status": "stable",
16
- "summary": "Anchored action menu with flat commands, grouped sections, single-selection choices, supplemental notification dots or trailing tags, prefix icons, reorderable rows, switches, and destructive actions.",
16
+ "summary": "Anchored action menu with flat commands, grouped sections, single-selection choices, supplemental notification dots or trailing tags, prefix icons, reorderable rows, switches, independent trailing actions, separate trailing toggles, and destructive actions.",
17
17
  "useWhen": [
18
18
  "A trigger reveals a short contextual list of commands or state-changing actions.",
19
19
  "Actions need grouping, mutually exclusive selected state, supplemental notification dots or trailing tags, prefix icons, reorderable switch or command rows, or destructive treatment."
@@ -60,7 +60,9 @@
60
60
  "When the anchor is inside an open shadow tree, pass its HTMLElement through anchor. Outside activation, focus return, and forward Tab continuation follow the composed tree across that boundary.",
61
61
  "Use notification dots only as supplemental status and not as the sole indication of important information.",
62
62
  "Trailing tags are non-interactive supplemental content; their visible label remains part of the menu item's accessible name.",
63
- "Reorderable rows keep a presentation Drag handle. Pointer drag on the handle and Alt+Arrow Up or Down emit dsReorder without closing. Pointer cancellation abandons the pending move. Hide-locked reorderable rows expose aria-disabled for the unavailable toggle but stay focusable so they can move; fully locked rows stay out of the contiguous reorderable run.",
63
+ "Reorderable rows keep a presentation Drag handle. Pointer drag on the handle and Alt+Arrow Up or Down emit dsReorder without closing. Pointer cancellation abandons the pending move. Hide-locked reorderable rows expose aria-disabled for the unavailable toggle but stay focusable so they can move; fully locked rows stay out of the contiguous reorderable run. Use reorderHandleInactive to retain quaternary Drag dots without exposing drag interaction when a temporarily fixed row still needs visual alignment.",
64
+ "A row may append one separately focusable icon toggle after a decorative tertiary divider. The popup uses non-modal dialog semantics for this composite content, the row retains its primary command or visibility toggle, and dsItemToggle reports the requested controlled state without closing the surface.",
65
+ "A composite row may expose independent trailing icon actions without making its non-actionable row surface appear clickable. Active Drag handles and trailing actions own separate inset hover and pressed feedback. Each action has its own accessible name, does not use aria-pressed, and reports its stable action identity through dsItemToggle without closing the surface.",
64
66
  "Announce the new position through a polite live region that is a sibling of the role=menu popup, so the menu retains only permitted descendants."
65
67
  ],
66
68
  "states": [
@@ -74,7 +76,7 @@
74
76
  "Use a swatch-picker section for a small curated visual preference and apply dsSwatchSelect immediately without closing the popup.",
75
77
  "Ordinary rows, section headings, and supporting text consume the Menu size. At xs, both the primary label and supporting text use caption metrics.",
76
78
  "A trailing tag resolves one density smaller than its menu row and uses the inset recipe so it nests without changing row height. Do not combine tag, dot, and switch suffixes on one item.",
77
- "Reorderable rows use the Drag prefix and ignore icon. Prefix and drag-handle icons inherit the choice-popup secondary foreground, matching Select. When any row in a section has an icon or is reorderable, the remaining rows get a spacer so labels align.",
79
+ "Reorderable rows use the Drag prefix and ignore icon. Active drag handles inherit the choice-popup secondary foreground; inactive drag handles use quaternary foreground. When any row in a section has an icon, an active handle, or an inactive handle, the remaining rows get a spacer so labels align.",
78
80
  "Embedded mode renders the same controlled content inside a parent-owned surface; the parent owns positioning, initial focus, Escape, Tab exit, and dismissal."
79
81
  ],
80
82
  "responsiveBehavior": [
@@ -88,13 +90,13 @@
88
90
  "frameworkCaveats": {
89
91
  "customElements": [
90
92
  "Assign items, sections, and HTMLElement anchors as properties after the element is defined.",
91
- "Handle dsReorder by replacing the items or section items with the emitted items array; Menu never mutates order."
93
+ "Handle dsReorder by replacing the items or section items with the emitted items array; Menu never mutates order. Handle dsItemToggle by replacing either the controlled trailingToggle state or the requested trailingActions state without changing unrelated row state."
92
94
  ],
93
95
  "react": [
94
- "Pass arrays and objects as wrapper props and handle generated onDsSelect, onDsReorder, and onDsClose event props."
96
+ "Pass arrays and objects as wrapper props and handle generated onDsSelect, onDsItemToggle, onDsReorder, and onDsClose event props."
95
97
  ],
96
98
  "angular": [
97
- "Use property bindings for arrays, objects, and anchors and event bindings for dsSelect, dsReorder, and dsClose."
99
+ "Use property bindings for arrays, objects, and anchors and event bindings for dsSelect, dsItemToggle, dsReorder, and dsClose."
98
100
  ]
99
101
  },
100
102
  "references": [
@@ -308,6 +310,14 @@
308
310
  "cancelable": true,
309
311
  "composed": true
310
312
  },
313
+ {
314
+ "name": "dsItemToggle",
315
+ "detail": "MenuItemToggleDetail",
316
+ "bubbles": true,
317
+ "cancelable": true,
318
+ "composed": true,
319
+ "description": "Emitted when a row's separate trailing toggle requests a controlled state change."
320
+ },
311
321
  {
312
322
  "name": "dsReorder",
313
323
  "detail": "MenuReorderDetail",
@@ -509,6 +519,14 @@
509
519
  "cancelable": true,
510
520
  "composed": true
511
521
  },
522
+ {
523
+ "name": "dsItemToggle",
524
+ "detail": "MenuItemToggleDetail",
525
+ "bubbles": true,
526
+ "cancelable": true,
527
+ "composed": true,
528
+ "description": "Emitted when a row's separate trailing toggle requests a controlled state change."
529
+ },
512
530
  {
513
531
  "name": "dsReorder",
514
532
  "detail": "MenuReorderDetail",
@@ -563,7 +581,7 @@
563
581
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: anchor, boundary, items, sections.",
564
582
  "peerDependencies": {
565
583
  "required": [
566
- "@ds-mo/tokens ^6.6.1",
584
+ "@ds-mo/tokens ^6.7.0",
567
585
  "@ds-mo/icons ^8.1.1"
568
586
  ],
569
587
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -577,6 +595,7 @@
577
595
  ],
578
596
  "registryDependencies": [
579
597
  "badge",
598
+ "button-unfilled",
580
599
  "icon",
581
600
  "swatch-picker",
582
601
  "switch",
@@ -586,12 +605,12 @@
586
605
  "files": [
587
606
  {
588
607
  "path": "src/wc/components/Menu/Menu.css",
589
- "content": "@import '../../utils/radius.css';\n@import '../../utils/choice-list.css';\n@import '../../utils/visually-hidden.css';\n@import '../../utils/forced-colors.css';\n\n/* Inactive: `.ds-control-inactive` (50% opacity). */\n\n.menu-empty {\n padding: var(--dimension-space-200);\n box-sizing: border-box;\n overflow-wrap: anywhere;\n}\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 keeps an 8px label-to-control gap without changing the\n density gap between a prefix icon or drag handle and the label. */\n.menu-item__switch {\n flex-shrink: 0;\n margin-inline-start: calc(\n var(--dimension-space-100) - var(--ds-control-gap, var(--dimension-space-050))\n );\n}\n\n/* ── Prefix icon / reorder handle ────────────────────────────────────────── */\n\n.menu-item > .menu-item__handle {\n pointer-events: auto;\n cursor: grab;\n touch-action: none;\n}\n\n.menu-item > .menu-item__handle:active,\n.menu-item--dragging > .menu-item__handle {\n cursor: grabbing;\n}\n\n.menu-item--dragging {\n opacity: var(--effect-opacity-medium);\n}\n\n.menu-item__drop-rail {\n position: absolute;\n z-index: var(--dimension-z-index-raised);\n inset-inline: var(--dimension-space-050);\n block-size: var(--dimension-stroke-width-025);\n border-radius: var(--dimension-radius-half);\n background: var(--color-border-bold-brand);\n pointer-events: none;\n}\n\n.menu-item__drop-rail--before {\n inset-block-start: calc(-1 * var(--dimension-space-025));\n transform: translateY(-50%);\n}\n\n.menu-item__drop-rail--after {\n inset-block-end: calc(-1 * var(--dimension-space-025));\n transform: translateY(50%);\n}\n\n@media (forced-colors: active) {\n .menu-item__drop-rail {\n background: var(--ds-forced-color-selected);\n }\n}\n\n.data-preferences-embedded {\n display: flex;\n flex-direction: column;\n min-width: 0;\n min-height: 0;\n color: var(--color-foreground-secondary);\n}\n",
608
+ "content": "@import '../../utils/radius.css';\n@import '../../utils/choice-list.css';\n@import '../../utils/visually-hidden.css';\n@import '../../utils/forced-colors.css';\n\n/* Inactive: `.ds-control-inactive` (50% opacity). */\n\n.menu-empty {\n padding: var(--dimension-space-200);\n box-sizing: border-box;\n overflow-wrap: anywhere;\n}\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 keeps an 8px label-to-control gap without changing the\n density gap between a prefix icon or drag handle and the label. */\n.menu-item__switch {\n flex-shrink: 0;\n margin-inline-start: calc(\n var(--dimension-space-100) - var(--ds-control-gap, var(--dimension-space-050))\n );\n}\n\n.menu-item-row {\n display: contents;\n}\n\n.menu-item-row--trailing-toggle {\n display: flex;\n align-items: center;\n min-width: 0;\n}\n\n.menu-item-row--actions {\n display: flex;\n align-items: center;\n min-width: 0;\n padding-inline-end: var(--dimension-space-025);\n}\n\n.menu-item-row--trailing-toggle > .menu-item {\n flex: 1;\n min-width: 0;\n width: auto;\n}\n\n.menu-item-row__trailing-toggle {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n}\n\n.menu-item-row__divider {\n flex-shrink: 0;\n width: var(--dimension-stroke-width-012);\n height: var(--dimension-size-250);\n margin-inline: var(--dimension-space-025);\n background: var(--color-border-tertiary);\n}\n\n.menu-item-row__trailing-toggle > ds-button-unfilled {\n flex-shrink: 0;\n}\n\n.menu-item-row__actions,\n.menu-item-row__action {\n display: inline-flex;\n align-items: center;\n flex-shrink: 0;\n}\n\n.menu-item-row__action > ds-button-unfilled {\n flex-shrink: 0;\n}\n\n.menu-subsection-divider {\n display: block;\n flex-shrink: 0;\n height: var(--dimension-stroke-width-012);\n margin-inline: calc(\n var(--ds-control-padding-inline, var(--dimension-space-075)) +\n var(--ds-control-label-inset, var(--dimension-space-025))\n );\n background: var(--color-border-tertiary);\n}\n\n/* ── Prefix icon / reorder handle ────────────────────────────────────────── */\n\n.menu-item > .menu-item__handle {\n pointer-events: auto;\n cursor: grab;\n touch-action: none;\n}\n\n.menu-item > .menu-item__handle--action-row {\n --ds-menu-action-handle-size: calc(\n var(--ds-control-height, var(--dimension-size-400)) - var(--dimension-space-050)\n );\n\n width: var(--ds-menu-action-handle-size);\n height: var(--ds-menu-action-handle-size);\n margin-inline: calc(\n (var(--ds-control-icon, var(--dimension-iconography-md)) - var(--ds-menu-action-handle-size)) /\n 2\n );\n border-radius: var(--ds-control-radius, var(--dimension-radius-025));\n}\n\n.menu-item > .menu-item__handle--inactive {\n color: var(--color-foreground-quaternary);\n pointer-events: none;\n cursor: default;\n touch-action: auto;\n}\n\n.menu-item > .menu-item__handle:active,\n.menu-item--dragging > .menu-item__handle {\n cursor: grabbing;\n}\n\n.menu-item--dragging {\n opacity: var(--effect-opacity-medium);\n}\n\n.menu-item__drop-rail {\n position: absolute;\n z-index: var(--dimension-z-index-raised);\n inset-inline: var(--dimension-space-050);\n block-size: var(--dimension-stroke-width-025);\n border-radius: var(--dimension-radius-half);\n background: var(--color-border-bold-brand);\n pointer-events: none;\n}\n\n.menu-item__drop-rail--before {\n inset-block-start: calc(-1 * var(--dimension-space-025));\n transform: translateY(-50%);\n}\n\n.menu-item__drop-rail--after {\n inset-block-end: calc(-1 * var(--dimension-space-025));\n transform: translateY(50%);\n}\n\n@media (forced-colors: active) {\n .menu-item-row__divider {\n background: var(--ds-forced-color-content);\n }\n\n .menu-subsection-divider {\n background: var(--ds-forced-color-content);\n }\n\n .menu-item__drop-rail {\n background: var(--ds-forced-color-selected);\n }\n}\n\n.data-preferences-embedded {\n display: flex;\n flex-direction: column;\n min-width: 0;\n min-height: 0;\n color: var(--color-foreground-secondary);\n}\n",
590
609
  "type": "registry:ui"
591
610
  },
592
611
  {
593
612
  "path": "src/wc/components/Menu/Menu.tsx",
594
- "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 CONTROL_SUPPORTING_TEXT_VARIANT,\n CONTROL_TEXT_VARIANT,\n resolveChoicePopupAlignOffset,\n resolveCssLengthPx,\n resolveMotionTimeMs,\n TOKEN_CSS_LENGTHS,\n TOKEN_DEFAULTS,\n type ChoicePopupAnchorAlignment,\n type ControlSize,\n} from '../../utils';\nimport { AnchoredPositionController } from '../../utils/anchored-position-controller';\nimport { ConnectionTasks } from '../../utils/connection-tasks';\nimport { AnchoredOverlayInteractionController } from '../../utils/anchored-overlay-interaction-controller';\nimport { resolveAnchoredOverlayBoundaryRect } from '../../utils/anchored-overlay-boundary';\nimport { computeAnchoredPosition, type AnchoredPositionInput } from '../../utils/anchored-position';\nimport type { MenuAlign, MenuSide } from './menu-position';\nimport {\n isMenuPickerSection,\n isMenuSwatchPickerSection,\n type MenuItemData,\n type MenuReorderDetail,\n type MenuSection,\n} from './menu-types';\nimport { snapshotMenuSections } from './menu-sections';\nimport {\n createMenuReorderDetail,\n locateMenuItem,\n menuReorderableRange,\n moveReorderableMenuItemBefore,\n moveReorderableMenuItemBy,\n} from './menu-reorder';\n\nexport type MenuSelectionMode = 'none' | 'single';\nexport type MenuSize = ControlSize;\nlet menuId = 0;\nconst MENU_ITEM_TAG_SIZE: Record<MenuSize, 'md' | 'sm' | 'xs'> = {\n lg: 'md',\n md: 'sm',\n sm: 'xs',\n xs: 'xs',\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 /** Render content inside a parent-owned surface; the parent owns dismissal and positioning. */\n @Prop() embedded: boolean = false;\n @Prop({ mutable: true }) open: boolean = false;\n @Prop() items: MenuItemData[] = [];\n /** Choice-row density. */\n @Prop() size: MenuSize = 'md';\n /** Give ordinary rows mutually-exclusive 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 /** Explicit collision owner; otherwise the nearest data-ds-overlay-boundary ancestor is used. */\n @Prop() boundary: HTMLElement | 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 /** Informational message shown when there are no choices. Uses a non-modal dialog, not a fake menu item. */\n @Prop() emptyMessage: string = '';\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 availableHeight: number | null = null;\n @State() private focusRingVisible: boolean = false;\n @State() private reorderFromFlat: number | null = null;\n @State() private reorderInsertBefore: number | null = null;\n @State() private reorderAnnouncement = '';\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 generic `swatch-picker` section option is chosen. */\n @Event() dsSwatchSelect!: EventEmitter<string>;\n /** Emitted after a pointer drop or keyboard move; Menu never mutates item order. */\n @Event() dsReorder!: EventEmitter<MenuReorderDetail>;\n\n private closeTimer: ReturnType<typeof setTimeout> | null = null;\n private loaded = false;\n private readonly tasks = new ConnectionTasks(() => this.el.isConnected);\n private positionReadyCallback: (() => void) | undefined;\n private suppressItemClick = false;\n private reorderPointer: {\n pointerId: number;\n sectionIndex: number;\n fromIndex: number;\n } | null = null;\n /**\n * Anchored placement. The choice-cell measurement stays here on purpose: the\n * inner-cell align offset and the `--dimension-menu-width-xs` floor are Menu\n * semantics, not shared geometry.\n */\n private readonly position = new AnchoredPositionController({\n getAnchor: () => this.resolvedAnchor ?? null,\n getPopup: () => this.el.querySelector<HTMLElement>('.menu-popup'),\n getOwnerDocument: () => this.el.ownerDocument,\n measure: (anchorEl, popup) => {\n if (!this.open) return null;\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 viewportPadPx = this.viewportPadPx;\n const list = popup.querySelector<HTMLElement>('.ds-choice-list');\n const naturalPopupHeight = Math.min(\n Math.max(popup.offsetHeight, list?.scrollHeight ?? 0),\n window.innerHeight - viewportPadPx * 2\n );\n const input: AnchoredPositionInput = {\n anchorRect,\n popupWidth: popup.offsetWidth || this.popupFallbackWidthPx,\n popupHeight: naturalPopupHeight || 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,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n collisionRect: resolveAnchoredOverlayBoundaryRect(anchorEl, this.boundary),\n };\n const unconstrained = computeAnchoredPosition(input);\n return {\n ...input,\n popupHeight: Math.min(input.popupHeight, unconstrained.availableHeight),\n };\n },\n apply: ({ x, y, availableHeight }) => {\n this.pos = { x, y };\n this.availableHeight = availableHeight;\n },\n onReady: () => {\n this.positionReady = true;\n },\n liveUpdate: 'double-frame',\n topLayer: true,\n observeResize: true,\n observeAnchorMotion: true,\n });\n private readonly interaction = new AnchoredOverlayInteractionController({\n getAnchor: () => this.resolvedAnchor,\n getPopup: () => this.el.querySelector<HTMLElement>('.menu-popup'),\n getOwnerDocument: () => this.el.ownerDocument,\n onOutsideActivation: () => this.close(),\n });\n /** Last content actually painted while open; retained unchanged through exit motion. */\n private lastRenderedSections: MenuSection[] = [];\n private closingSections: MenuSection[] | null = null;\n private readonly emptyMessageId = `ds-menu-empty-${++menuId}`;\n private lastRenderedEmptyMessage = '';\n\n componentDidLoad() {\n this.loaded = true;\n if (this.open) this.onOpenChange(true);\n }\n\n connectedCallback() {\n if (this.loaded && this.open) this.onOpenChange(true);\n }\n\n disconnectedCallback() {\n this.cancelPositionRetry();\n this.cancelLivePositionUpdate();\n this.clearReorderPointer();\n this.teardownListeners();\n this.positionReadyCallback = undefined;\n this.shouldRender = false;\n this.closing = false;\n this.positionReady = false;\n this.closingSections = null;\n this.lastRenderedSections = [];\n this.suppressItemClick = false;\n this.reorderAnnouncement = '';\n }\n\n @Watch('open')\n onOpenChange(isOpen: boolean) {\n if (this.embedded || !this.loaded || !this.el.isConnected) return;\n if (isOpen) {\n this.teardownListeners();\n this.closingSections = null;\n this.shouldRender = true;\n this.closing = false;\n this.positionReady = false;\n this.availableHeight = null;\n this.focusRingVisible = this.initialFocusVisible;\n this.setupListeners();\n this.schedulePositionUpdate(() => {\n this.focusInitialItem();\n });\n } else if (this.shouldRender && !this.closing) {\n this.positionReadyCallback = undefined;\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 @Watch('boundary')\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 if (this.open || !this.closing || !this.el.isConnected) return;\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.clearReorderPointer();\n this.reorderAnnouncement = '';\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 compositeTabLeavesPopup(event: KeyboardEvent): boolean {\n return this.interaction.tabLeavesPopup(event);\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 this.position.cancel();\n }\n\n private cancelLivePositionUpdate() {\n this.position.cancel();\n }\n\n /** Reposition after the current scroll/layout frame without hiding the open popup. */\n private scheduleLivePositionUpdate() {\n this.position.scheduleLiveUpdate();\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 || !this.el.isConnected) return;\n this.positionReady = false;\n // Opening often sets `open` and `anchorId` together. Each Watch reschedules\n // placement; keep the open-path ready callback so initial focus is not dropped.\n if (onReady) this.positionReadyCallback = onReady;\n const callback = this.positionReadyCallback;\n this.position.schedule(() => {\n if (this.positionReadyCallback === callback) {\n this.positionReadyCallback = undefined;\n }\n callback?.();\n });\n }\n\n /** @returns `true` when anchor and popup were found and `pos` was updated. */\n private calculatePosition(): boolean {\n return this.position.update();\n }\n\n /** Focus the selected item when present, otherwise the first enabled item. */\n private focusInitialItem() {\n if (!this.hasCompositeSections && !this.flatItems.length && this.emptyMessage.trim()) {\n this.tasks.frame(() => {\n if (this.open) this.el.querySelector<HTMLElement>('.menu-popup')?.focus();\n });\n return;\n }\n if (this.hasCompositeSections) {\n this.tasks.frame(() => {\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 || !!it.reorderable);\n this.focusedIndex = selectedIdx >= 0 ? selectedIdx : firstEnabledIdx >= 0 ? firstEnabledIdx : 0;\n\n this.tasks.frame(() => {\n const btns = this.el.querySelectorAll<HTMLElement>('.menu-item');\n btns[this.focusedIndex]?.focus();\n });\n }\n\n private setupListeners() {\n this.interaction.connect();\n this.position.observe();\n }\n\n private teardownListeners() {\n this.tasks.cancel();\n this.position.unobserve();\n this.interaction.disconnect();\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.interaction.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.embedded && (e.key === 'Escape' || e.key === 'Tab')) return;\n if ((!this.shouldRender && !this.embedded) || 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.interaction.moveFocusAfterTab(e.shiftKey);\n return;\n }\n\n if (this.hasCompositeSections) return;\n\n if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && e.altKey && !e.ctrlKey && !e.shiftKey) {\n this.handleReorderKey(e);\n return;\n }\n\n const flat = this.flatItems;\n const enabled = flat\n .map((it, i) => ({ it, i }))\n .filter(({ it }) => !it.isInactive || !!it.reorderable)\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 (this.suppressItemClick) {\n this.suppressItemClick = false;\n return;\n }\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 handleSwatchSelect(value: string) {\n this.dsSwatchSelect.emit(value);\n }\n\n private handleReorderKey(event: KeyboardEvent): void {\n const located = locateMenuItem(this.activeSections, this.focusedIndex);\n if (!located || !located.items[located.itemIndex]?.reorderable) return;\n\n const nextItems = moveReorderableMenuItemBy(\n located.items,\n located.itemIndex,\n event.key === 'ArrowUp' ? -1 : 1\n );\n if (!nextItems) return;\n\n event.preventDefault();\n event.stopPropagation();\n this.emitReorder(located.items, located.itemIndex, nextItems, located.sectionIndex);\n }\n\n private emitReorder(\n items: MenuItemData[],\n fromIndex: number,\n nextItems: MenuItemData[],\n sectionIndex: number\n ): void {\n const detail = createMenuReorderDetail(items, fromIndex, nextItems, sectionIndex);\n if (!detail) return;\n this.focusedIndex = this.flatIndexFor(sectionIndex, detail.toIndex);\n this.announceReorder(detail);\n this.dsReorder.emit(detail);\n }\n\n private flatIndexFor(sectionIndex: number, itemIndex: number): number {\n let flat = 0;\n for (let index = 0; index < this.activeSections.length; index += 1) {\n const section = this.activeSections[index];\n if (isMenuPickerSection(section)) continue;\n if (index === sectionIndex) return flat + itemIndex;\n flat += section.items.length;\n }\n return itemIndex;\n }\n\n private announceReorder(detail: MenuReorderDetail): void {\n const range = menuReorderableRange(detail.items, detail.toIndex);\n const count = range ? range.end - range.start + 1 : detail.items.length;\n const position = range ? detail.toIndex - range.start + 1 : detail.toIndex + 1;\n this.reorderAnnouncement = `${detail.item.label} moved to position ${position} of ${count}`;\n }\n\n private onReorderPointerDown(\n event: PointerEvent,\n sectionIndex: number,\n itemIndex: number,\n flatIndex: number\n ): void {\n if (event.button !== 0) return;\n const section = this.activeSections[sectionIndex];\n if (!section || isMenuPickerSection(section)) return;\n if (!section.items[itemIndex]?.reorderable) return;\n\n event.preventDefault();\n event.stopPropagation();\n this.suppressItemClick = true;\n this.focusRingVisible = false;\n this.focusedIndex = flatIndex;\n this.focusItem(flatIndex);\n this.reorderPointer = {\n pointerId: event.pointerId,\n sectionIndex,\n fromIndex: itemIndex,\n };\n this.reorderFromFlat = flatIndex;\n this.reorderInsertBefore = itemIndex;\n window.addEventListener('pointermove', this.onReorderPointerMove);\n window.addEventListener('pointerup', this.onReorderPointerUp);\n window.addEventListener('pointercancel', this.onReorderPointerCancel);\n }\n\n private onReorderPointerMove = (event: PointerEvent) => {\n const drag = this.reorderPointer;\n if (!drag || event.pointerId !== drag.pointerId) return;\n const section = this.activeSections[drag.sectionIndex];\n if (!section || isMenuPickerSection(section)) return;\n\n const insertBefore = this.resolveReorderInsertBefore(\n event.clientY,\n drag.sectionIndex,\n drag.fromIndex,\n section.items\n );\n if (insertBefore !== this.reorderInsertBefore) this.reorderInsertBefore = insertBefore;\n };\n\n private onReorderPointerUp = (event: PointerEvent) => {\n const drag = this.reorderPointer;\n if (!drag || event.pointerId !== drag.pointerId) return;\n\n const section = this.activeSections[drag.sectionIndex];\n const insertBefore = this.reorderInsertBefore;\n this.clearReorderPointer();\n this.tasks.microtask(() => {\n this.suppressItemClick = false;\n });\n if (!section || isMenuPickerSection(section) || insertBefore === null) return;\n\n const nextItems = moveReorderableMenuItemBefore(section.items, drag.fromIndex, insertBefore);\n if (!nextItems) return;\n this.emitReorder(section.items, drag.fromIndex, nextItems, drag.sectionIndex);\n };\n\n private onReorderPointerCancel = (event: PointerEvent) => {\n if (!this.reorderPointer || event.pointerId !== this.reorderPointer.pointerId) return;\n this.clearReorderPointer();\n this.tasks.microtask(() => {\n this.suppressItemClick = false;\n });\n };\n\n private resolveReorderInsertBefore(\n clientY: number,\n sectionIndex: number,\n fromIndex: number,\n items: MenuItemData[]\n ): number {\n const range = menuReorderableRange(items, fromIndex);\n if (!range) return fromIndex;\n\n const buttons = [...this.el.querySelectorAll<HTMLElement>('.menu-item')];\n const sectionStart = this.flatIndexFor(sectionIndex, 0);\n let insertBefore = range.end + 1;\n for (let itemIndex = range.start; itemIndex <= range.end; itemIndex += 1) {\n const row = buttons[sectionStart + itemIndex];\n if (!row) continue;\n const rect = row.getBoundingClientRect();\n if (clientY < rect.top + rect.height / 2) {\n insertBefore = itemIndex;\n break;\n }\n }\n return Math.min(Math.max(insertBefore, range.start), range.end + 1);\n }\n\n private clearReorderPointer(): void {\n if (this.reorderPointer) {\n window.removeEventListener('pointermove', this.onReorderPointerMove);\n window.removeEventListener('pointerup', this.onReorderPointerUp);\n window.removeEventListener('pointercancel', this.onReorderPointerCancel);\n }\n this.reorderPointer = null;\n this.reorderFromFlat = null;\n this.reorderInsertBefore = null;\n }\n\n render() {\n if (!this.shouldRender && !this.embedded) 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 const emptyMessage = this.closing ? this.lastRenderedEmptyMessage : this.emptyMessage.trim();\n const showEmpty =\n Boolean(emptyMessage) &&\n !hasCompositeSections &&\n sections.every(section => !isMenuPickerSection(section) && section.items.length === 0);\n if (!this.closing) {\n this.lastRenderedEmptyMessage = emptyMessage;\n this.lastRenderedSections = snapshotMenuSections(sections);\n this.closingSections = null;\n }\n let flatIdx = 0;\n const dragLocated =\n this.reorderFromFlat === null ? null : locateMenuItem(sections, this.reorderFromFlat);\n const dragRange = dragLocated\n ? menuReorderableRange(dragLocated.items, dragLocated.itemIndex)\n : null;\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 if (this.availableHeight !== null) {\n popupStyle['max-height'] = `${Math.floor(this.availableHeight)}px`;\n }\n\n return (\n <Host style={{ display: 'contents' }}>\n <div\n popover={this.embedded ? undefined : 'manual'}\n class={{\n 'menu-popup': true,\n 'menu-popup--closing': this.closing,\n 'ds-choice-popup': !this.embedded,\n 'data-preferences-embedded': this.embedded,\n 'ds-choice-popup--closing': this.closing,\n 'ds-focus-ring': showEmpty,\n 'ds-focus-ring--visible': showEmpty && this.focusRingVisible,\n }}\n style={this.embedded ? undefined : popupStyle}\n role={hasCompositeSections || showEmpty ? 'dialog' : 'menu'}\n aria-label={this.menuLabel}\n aria-orientation={hasCompositeSections || showEmpty ? undefined : 'vertical'}\n aria-describedby={showEmpty ? this.emptyMessageId : undefined}\n tabIndex={showEmpty ? -1 : undefined}\n >\n <div class=\"ds-choice-list\">\n {showEmpty ? (\n <ds-text\n textId={this.emptyMessageId}\n class=\"menu-empty\"\n as=\"p\"\n variant={CONTROL_TEXT_VARIANT[this.size]}\n color=\"secondary\"\n >\n {emptyMessage}\n </ds-text>\n ) : (\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--swatch-picker': isMenuPickerSection(section),\n 'ds-choice-section': true,\n 'ds-chrome-column': true,\n 'ds-chrome-space--sm': true,\n 'ds-choice-section--divided': si < sections.length - 1,\n 'ds-choice-section--headed-after-first': si > 0 && Boolean(section.header),\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-section-heading ds-control--${this.size}`}\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n emphasis\n color=\"primary\"\n aria-hidden=\"true\"\n >\n <span class=\"ds-choice-section__header-label\">{section.header}</span>\n </ds-text>\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, itemIndex) => {\n const idx = flatIdx++;\n const isFocused = this.focusedIndex === idx;\n const isSingleSelectionItem =\n !hasCompositeSections &&\n this.selectionMode === 'single' &&\n !item.showSwitch;\n const usesLeading = section.items.some(\n candidate => candidate.reorderable || !!candidate.icon\n );\n const locked = !!item.isInactive && !item.reorderable;\n const dragging = this.reorderFromFlat === idx;\n const dropBefore =\n dragLocated?.sectionIndex === si &&\n dragRange !== null &&\n itemIndex <= dragRange.end &&\n this.reorderInsertBefore === itemIndex;\n const dropAfter =\n dragLocated?.sectionIndex === si &&\n dragRange !== null &&\n this.reorderInsertBefore === dragRange.end + 1 &&\n itemIndex === dragRange.end;\n return (\n <button\n key={item.value ?? `${si}-${item.label}`}\n type=\"button\"\n class={{\n 'menu-item': true,\n 'ds-choice-item': true,\n 'ds-control-frame': true,\n [`ds-control--${this.size}`]: true,\n 'ds-focus-ring-inset': true,\n 'ds-focus-ring--visible': isFocused && this.focusRingVisible,\n 'ds-interaction-fill': !locked,\n 'ds-interaction-fill--selected': !!item.isSelected && !locked,\n 'menu-item--selected': !!item.isSelected,\n 'menu-item--switch': !!item.showSwitch,\n 'ds-control-inactive': locked,\n 'menu-item--destructive': !!item.isDestructive,\n 'menu-item--focused': isFocused,\n 'menu-item--dragging': dragging,\n }}\n role={\n hasCompositeSections\n ? undefined\n : item.showSwitch\n ? 'menuitemcheckbox'\n : isSingleSelectionItem\n ? 'menuitemradio'\n : 'menuitem'\n }\n aria-checked={\n !hasCompositeSections && item.showSwitch\n ? String(!!item.switchValue)\n : isSingleSelectionItem\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 !isSingleSelectionItem &&\n item.isSelected\n ? 'true'\n : undefined\n }\n aria-description={\n item.reorderable\n ? item.isInactive\n ? 'Visibility cannot be changed. Alt + Arrow Up or Alt + Arrow Down moves this row.'\n : 'Drag to reorder. Alt + Arrow Up or Alt + Arrow Down moves this row.'\n : undefined\n }\n aria-disabled={item.isInactive ? 'true' : undefined}\n disabled={locked}\n tabIndex={\n hasCompositeSections || (this.embedded && idx === 0)\n ? 0\n : isFocused\n ? 0\n : -1\n }\n onMouseDown={() => {\n this.focusRingVisible = false;\n }}\n onClick={() => this.handleItemClick(item)}\n onFocus={() => {\n this.focusedIndex = idx;\n }}\n >\n {(dropBefore || dropAfter) && (\n <span\n class={{\n 'menu-item__drop-rail': true,\n 'menu-item__drop-rail--before': dropBefore,\n 'menu-item__drop-rail--after': dropAfter,\n }}\n data-menu-drop-rail\n aria-hidden=\"true\"\n />\n )}\n {item.reorderable ? (\n <span\n class=\"menu-item__handle ds-choice-item__icon ds-control-icon-box ds-interaction-fill__content\"\n data-menu-handle\n aria-hidden=\"true\"\n onPointerDown={event =>\n this.onReorderPointerDown(event, si, itemIndex, idx)\n }\n onClick={event => {\n event.preventDefault();\n event.stopPropagation();\n }}\n >\n <ds-icon name=\"DragDots\" size={this.size} color=\"inherit\" />\n </span>\n ) : usesLeading && item.icon ? (\n <span\n class=\"menu-item__icon ds-choice-item__icon ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-icon name={item.icon} size={this.size} color=\"inherit\" />\n </span>\n ) : usesLeading ? (\n <span\n class=\"menu-item__icon-spacer ds-choice-item__icon ds-control-icon-box\"\n aria-hidden=\"true\"\n />\n ) : null}\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 ds-control-label-box\"\n as=\"span\"\n variant={CONTROL_TEXT_VARIANT[this.size]}\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 ds-control-label-box\"\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n color=\"secondary\"\n >\n {item.subtext}\n </ds-text>\n )}\n </div>\n {item.tag && (\n <ds-tag\n class=\"ds-choice-item__tag ds-interaction-fill__content\"\n label={item.tag.label}\n size={MENU_ITEM_TAG_SIZE[this.size]}\n intent={item.tag.intent ?? 'neutral'}\n contrast={item.tag.contrast ?? 'faint'}\n rounded={item.tag.rounded ?? false}\n isInset\n />\n )}\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={this.size}\n checked={!!item.switchValue}\n isInactive={!!item.isInactive}\n presentation\n />\n )}\n </button>\n );\n })\n )}\n </div>\n ))\n )}\n </div>\n </div>\n <div class=\"ds-visually-hidden\" role=\"status\" aria-live=\"polite\">\n {this.reorderAnnouncement}\n </div>\n </Host>\n );\n }\n}\n",
613
+ "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 CONTROL_SUPPORTING_TEXT_VARIANT,\n CONTROL_TEXT_VARIANT,\n resolveChoicePopupAlignOffset,\n resolveCssLengthPx,\n resolveMotionTimeMs,\n TOKEN_CSS_LENGTHS,\n TOKEN_DEFAULTS,\n type ChoicePopupAnchorAlignment,\n type ControlSize,\n} from '../../utils';\nimport { AnchoredPositionController } from '../../utils/anchored-position-controller';\nimport { ConnectionTasks } from '../../utils/connection-tasks';\nimport { AnchoredOverlayInteractionController } from '../../utils/anchored-overlay-interaction-controller';\nimport { resolveAnchoredOverlayBoundaryRect } from '../../utils/anchored-overlay-boundary';\nimport { computeAnchoredPosition, type AnchoredPositionInput } from '../../utils/anchored-position';\nimport type { MenuAlign, MenuSide } from './menu-position';\nimport {\n isMenuPickerSection,\n isMenuSwatchPickerSection,\n type MenuItemData,\n type MenuItemToggleDetail,\n type MenuReorderDetail,\n type MenuSection,\n} from './menu-types';\nimport { snapshotMenuSections } from './menu-sections';\nimport {\n createMenuReorderDetail,\n locateMenuItem,\n menuReorderableRange,\n moveReorderableMenuItemBefore,\n moveReorderableMenuItemBy,\n} from './menu-reorder';\n\nexport type MenuSelectionMode = 'none' | 'single';\nexport type MenuSize = ControlSize;\nlet menuId = 0;\nconst MENU_ITEM_TAG_SIZE: Record<MenuSize, 'md' | 'sm' | 'xs'> = {\n lg: 'md',\n md: 'sm',\n sm: 'xs',\n xs: 'xs',\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 /** Render content inside a parent-owned surface; the parent owns dismissal and positioning. */\n @Prop() embedded: boolean = false;\n @Prop({ mutable: true }) open: boolean = false;\n @Prop() items: MenuItemData[] = [];\n /** Choice-row density. */\n @Prop() size: MenuSize = 'md';\n /** Give ordinary rows mutually-exclusive 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 /** Explicit collision owner; otherwise the nearest data-ds-overlay-boundary ancestor is used. */\n @Prop() boundary: HTMLElement | 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 /** Informational message shown when there are no choices. Uses a non-modal dialog, not a fake menu item. */\n @Prop() emptyMessage: string = '';\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 availableHeight: number | null = null;\n @State() private focusRingVisible: boolean = false;\n @State() private reorderFromFlat: number | null = null;\n @State() private reorderInsertBefore: number | null = null;\n @State() private reorderAnnouncement = '';\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 row's separate trailing toggle requests a controlled state change. */\n @Event() dsItemToggle!: EventEmitter<MenuItemToggleDetail>;\n /** Emitted when a generic `swatch-picker` section option is chosen. */\n @Event() dsSwatchSelect!: EventEmitter<string>;\n /** Emitted after a pointer drop or keyboard move; Menu never mutates item order. */\n @Event() dsReorder!: EventEmitter<MenuReorderDetail>;\n\n private closeTimer: ReturnType<typeof setTimeout> | null = null;\n private loaded = false;\n private readonly tasks = new ConnectionTasks(() => this.el.isConnected);\n private positionReadyCallback: (() => void) | undefined;\n private suppressItemClick = false;\n private reorderPointer: {\n pointerId: number;\n sectionIndex: number;\n fromIndex: number;\n } | null = null;\n /**\n * Anchored placement. The choice-cell measurement stays here on purpose: the\n * inner-cell align offset and the `--dimension-menu-width-xs` floor are Menu\n * semantics, not shared geometry.\n */\n private readonly position = new AnchoredPositionController({\n getAnchor: () => this.resolvedAnchor ?? null,\n getPopup: () => this.el.querySelector<HTMLElement>('.menu-popup'),\n getOwnerDocument: () => this.el.ownerDocument,\n measure: (anchorEl, popup) => {\n if (!this.open) return null;\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 viewportPadPx = this.viewportPadPx;\n const list = popup.querySelector<HTMLElement>('.ds-choice-list');\n const naturalPopupHeight = Math.min(\n Math.max(popup.offsetHeight, list?.scrollHeight ?? 0),\n window.innerHeight - viewportPadPx * 2\n );\n const input: AnchoredPositionInput = {\n anchorRect,\n popupWidth: popup.offsetWidth || this.popupFallbackWidthPx,\n popupHeight: naturalPopupHeight || 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,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n collisionRect: resolveAnchoredOverlayBoundaryRect(anchorEl, this.boundary),\n };\n const unconstrained = computeAnchoredPosition(input);\n return {\n ...input,\n popupHeight: Math.min(input.popupHeight, unconstrained.availableHeight),\n };\n },\n apply: ({ x, y, availableHeight }) => {\n this.pos = { x, y };\n this.availableHeight = availableHeight;\n },\n onReady: () => {\n this.positionReady = true;\n },\n liveUpdate: 'double-frame',\n topLayer: true,\n observeResize: true,\n observeAnchorMotion: true,\n });\n private readonly interaction = new AnchoredOverlayInteractionController({\n getAnchor: () => this.resolvedAnchor,\n getPopup: () => this.el.querySelector<HTMLElement>('.menu-popup'),\n getOwnerDocument: () => this.el.ownerDocument,\n onOutsideActivation: () => this.close(),\n });\n /** Last content actually painted while open; retained unchanged through exit motion. */\n private lastRenderedSections: MenuSection[] = [];\n private closingSections: MenuSection[] | null = null;\n private readonly emptyMessageId = `ds-menu-empty-${++menuId}`;\n private lastRenderedEmptyMessage = '';\n\n componentDidLoad() {\n this.loaded = true;\n if (this.open) this.onOpenChange(true);\n }\n\n connectedCallback() {\n if (this.loaded && this.open) this.onOpenChange(true);\n }\n\n disconnectedCallback() {\n this.cancelPositionRetry();\n this.cancelLivePositionUpdate();\n this.clearReorderPointer();\n this.teardownListeners();\n this.positionReadyCallback = undefined;\n this.shouldRender = false;\n this.closing = false;\n this.positionReady = false;\n this.closingSections = null;\n this.lastRenderedSections = [];\n this.suppressItemClick = false;\n this.reorderAnnouncement = '';\n }\n\n @Watch('open')\n onOpenChange(isOpen: boolean) {\n if (this.embedded || !this.loaded || !this.el.isConnected) return;\n if (isOpen) {\n this.teardownListeners();\n this.closingSections = null;\n this.shouldRender = true;\n this.closing = false;\n this.positionReady = false;\n this.availableHeight = null;\n this.focusRingVisible = this.initialFocusVisible;\n this.setupListeners();\n this.schedulePositionUpdate(() => {\n this.focusInitialItem();\n });\n } else if (this.shouldRender && !this.closing) {\n this.positionReadyCallback = undefined;\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 @Watch('boundary')\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 if (this.open || !this.closing || !this.el.isConnected) return;\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.clearReorderPointer();\n this.reorderAnnouncement = '';\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 compositeTabLeavesPopup(event: KeyboardEvent): boolean {\n return this.interaction.tabLeavesPopup(event);\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.sectionsHaveCompositeControls(this.activeSections);\n }\n\n private sectionsHaveCompositeControls(sections: MenuSection[]): boolean {\n return sections.some(\n section =>\n isMenuPickerSection(section) ||\n section.items.some(item => Boolean(item.trailingToggle || item.trailingActions?.length))\n );\n }\n\n private sectionUsesInsetDivider(section: MenuSection | undefined): boolean {\n return Boolean(section && !isMenuPickerSection(section) && section.insetDividerBefore);\n }\n\n private cancelPositionRetry() {\n this.position.cancel();\n }\n\n private cancelLivePositionUpdate() {\n this.position.cancel();\n }\n\n /** Reposition after the current scroll/layout frame without hiding the open popup. */\n private scheduleLivePositionUpdate() {\n this.position.scheduleLiveUpdate();\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 || !this.el.isConnected) return;\n this.positionReady = false;\n // Opening often sets `open` and `anchorId` together. Each Watch reschedules\n // placement; keep the open-path ready callback so initial focus is not dropped.\n if (onReady) this.positionReadyCallback = onReady;\n const callback = this.positionReadyCallback;\n this.position.schedule(() => {\n if (this.positionReadyCallback === callback) {\n this.positionReadyCallback = undefined;\n }\n callback?.();\n });\n }\n\n /** @returns `true` when anchor and popup were found and `pos` was updated. */\n private calculatePosition(): boolean {\n return this.position.update();\n }\n\n /** Focus the selected item when present, otherwise the first enabled item. */\n private focusInitialItem() {\n if (!this.hasCompositeSections && !this.flatItems.length && this.emptyMessage.trim()) {\n this.tasks.frame(() => {\n if (this.open) this.el.querySelector<HTMLElement>('.menu-popup')?.focus();\n });\n return;\n }\n if (this.hasCompositeSections) {\n this.tasks.frame(() => {\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 || !!it.reorderable);\n this.focusedIndex = selectedIdx >= 0 ? selectedIdx : firstEnabledIdx >= 0 ? firstEnabledIdx : 0;\n\n this.tasks.frame(() => {\n const btns = this.el.querySelectorAll<HTMLElement>('.menu-item');\n btns[this.focusedIndex]?.focus();\n });\n }\n\n private setupListeners() {\n this.interaction.connect();\n this.position.observe();\n }\n\n private teardownListeners() {\n this.tasks.cancel();\n this.position.unobserve();\n this.interaction.disconnect();\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.interaction.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.embedded && (e.key === 'Escape' || e.key === 'Tab')) return;\n if ((!this.shouldRender && !this.embedded) || 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.interaction.moveFocusAfterTab(e.shiftKey);\n return;\n }\n\n if (\n (e.key === 'ArrowUp' || e.key === 'ArrowDown') &&\n e.altKey &&\n !e.ctrlKey &&\n !e.shiftKey &&\n (e.target as HTMLElement).closest('.menu-item')\n ) {\n this.handleReorderKey(e);\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 || !!it.reorderable)\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 rows = this.el.querySelectorAll<HTMLElement>('.menu-item');\n const row = rows[idx];\n if (!row) return;\n if (row.matches('button')) row.focus();\n else row.querySelector<HTMLElement>('[tabindex=\"0\"]')?.focus();\n }\n\n private handleItemClick(item: MenuItemData) {\n if (this.suppressItemClick) {\n this.suppressItemClick = false;\n return;\n }\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 handleSwatchSelect(value: string) {\n this.dsSwatchSelect.emit(value);\n }\n\n private handleReorderKey(event: KeyboardEvent): void {\n const located = locateMenuItem(this.activeSections, this.focusedIndex);\n if (!located || !located.items[located.itemIndex]?.reorderable) return;\n\n const nextItems = moveReorderableMenuItemBy(\n located.items,\n located.itemIndex,\n event.key === 'ArrowUp' ? -1 : 1\n );\n if (!nextItems) return;\n\n event.preventDefault();\n event.stopPropagation();\n this.emitReorder(located.items, located.itemIndex, nextItems, located.sectionIndex);\n }\n\n private emitReorder(\n items: MenuItemData[],\n fromIndex: number,\n nextItems: MenuItemData[],\n sectionIndex: number\n ): void {\n const detail = createMenuReorderDetail(items, fromIndex, nextItems, sectionIndex);\n if (!detail) return;\n this.focusedIndex = this.flatIndexFor(sectionIndex, detail.toIndex);\n this.announceReorder(detail);\n this.dsReorder.emit(detail);\n }\n\n private flatIndexFor(sectionIndex: number, itemIndex: number): number {\n let flat = 0;\n for (let index = 0; index < this.activeSections.length; index += 1) {\n const section = this.activeSections[index];\n if (isMenuPickerSection(section)) continue;\n if (index === sectionIndex) return flat + itemIndex;\n flat += section.items.length;\n }\n return itemIndex;\n }\n\n private announceReorder(detail: MenuReorderDetail): void {\n const range = menuReorderableRange(detail.items, detail.toIndex);\n const count = range ? range.end - range.start + 1 : detail.items.length;\n const position = range ? detail.toIndex - range.start + 1 : detail.toIndex + 1;\n this.reorderAnnouncement = `${detail.item.label} moved to position ${position} of ${count}`;\n }\n\n private onReorderPointerDown(\n event: PointerEvent,\n sectionIndex: number,\n itemIndex: number,\n flatIndex: number\n ): void {\n if (event.button !== 0) return;\n const section = this.activeSections[sectionIndex];\n if (!section || isMenuPickerSection(section)) return;\n if (!section.items[itemIndex]?.reorderable) return;\n\n event.preventDefault();\n event.stopPropagation();\n this.suppressItemClick = true;\n this.focusRingVisible = false;\n this.focusedIndex = flatIndex;\n this.focusItem(flatIndex);\n this.reorderPointer = {\n pointerId: event.pointerId,\n sectionIndex,\n fromIndex: itemIndex,\n };\n this.reorderFromFlat = flatIndex;\n this.reorderInsertBefore = itemIndex;\n window.addEventListener('pointermove', this.onReorderPointerMove);\n window.addEventListener('pointerup', this.onReorderPointerUp);\n window.addEventListener('pointercancel', this.onReorderPointerCancel);\n }\n\n private onReorderPointerMove = (event: PointerEvent) => {\n const drag = this.reorderPointer;\n if (!drag || event.pointerId !== drag.pointerId) return;\n const section = this.activeSections[drag.sectionIndex];\n if (!section || isMenuPickerSection(section)) return;\n\n const insertBefore = this.resolveReorderInsertBefore(\n event.clientY,\n drag.sectionIndex,\n drag.fromIndex,\n section.items\n );\n if (insertBefore !== this.reorderInsertBefore) this.reorderInsertBefore = insertBefore;\n };\n\n private onReorderPointerUp = (event: PointerEvent) => {\n const drag = this.reorderPointer;\n if (!drag || event.pointerId !== drag.pointerId) return;\n\n const section = this.activeSections[drag.sectionIndex];\n const insertBefore = this.reorderInsertBefore;\n this.clearReorderPointer();\n this.tasks.microtask(() => {\n this.suppressItemClick = false;\n });\n if (!section || isMenuPickerSection(section) || insertBefore === null) return;\n\n const nextItems = moveReorderableMenuItemBefore(section.items, drag.fromIndex, insertBefore);\n if (!nextItems) return;\n this.emitReorder(section.items, drag.fromIndex, nextItems, drag.sectionIndex);\n };\n\n private onReorderPointerCancel = (event: PointerEvent) => {\n if (!this.reorderPointer || event.pointerId !== this.reorderPointer.pointerId) return;\n this.clearReorderPointer();\n this.tasks.microtask(() => {\n this.suppressItemClick = false;\n });\n };\n\n private resolveReorderInsertBefore(\n clientY: number,\n sectionIndex: number,\n fromIndex: number,\n items: MenuItemData[]\n ): number {\n const range = menuReorderableRange(items, fromIndex);\n if (!range) return fromIndex;\n\n const buttons = [...this.el.querySelectorAll<HTMLElement>('.menu-item')];\n const sectionStart = this.flatIndexFor(sectionIndex, 0);\n let insertBefore = range.end + 1;\n for (let itemIndex = range.start; itemIndex <= range.end; itemIndex += 1) {\n const row = buttons[sectionStart + itemIndex];\n if (!row) continue;\n const rect = row.getBoundingClientRect();\n if (clientY < rect.top + rect.height / 2) {\n insertBefore = itemIndex;\n break;\n }\n }\n return Math.min(Math.max(insertBefore, range.start), range.end + 1);\n }\n\n private clearReorderPointer(): void {\n if (this.reorderPointer) {\n window.removeEventListener('pointermove', this.onReorderPointerMove);\n window.removeEventListener('pointerup', this.onReorderPointerUp);\n window.removeEventListener('pointercancel', this.onReorderPointerCancel);\n }\n this.reorderPointer = null;\n this.reorderFromFlat = null;\n this.reorderInsertBefore = null;\n }\n\n render() {\n if (!this.shouldRender && !this.embedded) return <Host style={{ display: 'contents' }} />;\n\n const sections = this.closing\n ? (this.closingSections ?? this.lastRenderedSections)\n : this.activeSections;\n const hasCompositeSections = this.sectionsHaveCompositeControls(sections);\n const emptyMessage = this.closing ? this.lastRenderedEmptyMessage : this.emptyMessage.trim();\n const showEmpty =\n Boolean(emptyMessage) &&\n !hasCompositeSections &&\n sections.every(section => !isMenuPickerSection(section) && section.items.length === 0);\n if (!this.closing) {\n this.lastRenderedEmptyMessage = emptyMessage;\n this.lastRenderedSections = snapshotMenuSections(sections);\n this.closingSections = null;\n }\n let flatIdx = 0;\n const dragLocated =\n this.reorderFromFlat === null ? null : locateMenuItem(sections, this.reorderFromFlat);\n const dragRange = dragLocated\n ? menuReorderableRange(dragLocated.items, dragLocated.itemIndex)\n : null;\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 if (this.availableHeight !== null) {\n popupStyle['max-height'] = `${Math.floor(this.availableHeight)}px`;\n }\n\n return (\n <Host style={{ display: 'contents' }}>\n <div\n popover={this.embedded ? undefined : 'manual'}\n class={{\n 'menu-popup': true,\n 'menu-popup--closing': this.closing,\n 'ds-choice-popup': !this.embedded,\n 'data-preferences-embedded': this.embedded,\n 'ds-choice-popup--closing': this.closing,\n 'ds-focus-ring': showEmpty,\n 'ds-focus-ring--visible': showEmpty && this.focusRingVisible,\n }}\n style={this.embedded ? undefined : popupStyle}\n role={hasCompositeSections || showEmpty ? 'dialog' : 'menu'}\n aria-label={this.menuLabel}\n aria-orientation={hasCompositeSections || showEmpty ? undefined : 'vertical'}\n aria-describedby={showEmpty ? this.emptyMessageId : undefined}\n tabIndex={showEmpty ? -1 : undefined}\n >\n <div class=\"ds-choice-list\">\n {showEmpty ? (\n <ds-text\n textId={this.emptyMessageId}\n class=\"menu-empty\"\n as=\"p\"\n variant={CONTROL_TEXT_VARIANT[this.size]}\n color=\"secondary\"\n >\n {emptyMessage}\n </ds-text>\n ) : (\n sections.map((section, si) => (\n <div\n key={si}\n class={{\n 'menu-section': true,\n 'menu-section--divided':\n si < sections.length - 1 && !this.sectionUsesInsetDivider(sections[si + 1]),\n 'menu-section--swatch-picker': isMenuPickerSection(section),\n 'ds-choice-section': true,\n 'ds-chrome-column': true,\n 'ds-chrome-space--sm': true,\n 'ds-choice-section--divided':\n si < sections.length - 1 && !this.sectionUsesInsetDivider(sections[si + 1]),\n 'ds-choice-section--headed-after-first':\n si > 0 &&\n !isMenuPickerSection(section) &&\n Boolean(section.header || section.subheader),\n }}\n role={\n section.header || (!isMenuPickerSection(section) && section.subheader)\n ? 'group'\n : undefined\n }\n aria-label={\n !isMenuPickerSection(section) && section.subheader\n ? section.subheader\n : section.header\n }\n >\n {!isMenuPickerSection(section) && section.insetDividerBefore && (\n <span class=\"menu-subsection-divider\" aria-hidden=\"true\" />\n )}\n {section.header && (\n <ds-text\n class={`section-header ds-choice-section__header ds-control-section-heading ds-control--${this.size}`}\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n emphasis\n color=\"primary\"\n aria-hidden=\"true\"\n >\n <span class=\"ds-choice-section__header-label\">{section.header}</span>\n </ds-text>\n )}\n {!isMenuSwatchPickerSection(section) && section.subheader && (\n <ds-text\n class={`section-subheader ds-choice-section__header ds-control-section-heading ds-control--${this.size}`}\n as=\"span\"\n variant=\"text-caption\"\n emphasis\n color=\"secondary\"\n aria-hidden=\"true\"\n >\n <span class=\"ds-choice-section__header-label\">{section.subheader}</span>\n </ds-text>\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, itemIndex) => {\n const idx = flatIdx++;\n const isFocused = this.focusedIndex === idx;\n const isSingleSelectionItem =\n !hasCompositeSections &&\n this.selectionMode === 'single' &&\n !item.showSwitch;\n const usesLeading = section.items.some(\n candidate =>\n candidate.reorderable ||\n candidate.reorderHandleInactive ||\n !!candidate.icon\n );\n const locked = !!item.isInactive && !item.reorderable;\n const dragging = this.reorderFromFlat === idx;\n const dropBefore =\n dragLocated?.sectionIndex === si &&\n dragRange !== null &&\n itemIndex <= dragRange.end &&\n this.reorderInsertBefore === itemIndex;\n const dropAfter =\n dragLocated?.sectionIndex === si &&\n dragRange !== null &&\n this.reorderInsertBefore === dragRange.end + 1 &&\n itemIndex === dragRange.end;\n if (item.trailingActions?.length) {\n return (\n <div\n key={item.value ?? `${si}-${item.label}`}\n class={{\n 'menu-item-row': true,\n 'menu-item-row--actions': true,\n 'menu-item': true,\n 'ds-choice-item': true,\n 'ds-control-frame': true,\n [`ds-control--${this.size}`]: true,\n 'menu-item--dragging': dragging,\n }}\n role=\"group\"\n aria-label={item.label}\n onMouseDown={() => {\n this.focusRingVisible = false;\n }}\n onFocusin={() => {\n this.focusedIndex = idx;\n }}\n >\n {(dropBefore || dropAfter) && (\n <span\n class={{\n 'menu-item__drop-rail': true,\n 'menu-item__drop-rail--before': dropBefore,\n 'menu-item__drop-rail--after': dropAfter,\n }}\n data-menu-drop-rail\n aria-hidden=\"true\"\n />\n )}\n {item.reorderable || item.reorderHandleInactive ? (\n <span\n class={{\n 'menu-item__handle': true,\n 'menu-item__handle--action-row': true,\n 'menu-item__handle--inactive': Boolean(\n item.reorderHandleInactive\n ),\n 'ds-interaction-fill': Boolean(item.reorderable),\n 'ds-choice-item__icon': true,\n 'ds-control-icon-box': true,\n }}\n data-menu-handle\n aria-hidden=\"true\"\n onPointerDown={\n item.reorderable\n ? event => this.onReorderPointerDown(event, si, itemIndex, idx)\n : undefined\n }\n >\n <ds-icon\n class=\"ds-interaction-fill__content\"\n name=\"DragDots\"\n size={this.size}\n color=\"inherit\"\n />\n </span>\n ) : usesLeading && item.icon ? (\n <span\n class=\"menu-item__icon ds-choice-item__icon ds-control-icon-box\"\n aria-hidden=\"true\"\n >\n <ds-icon name={item.icon} size={this.size} color=\"inherit\" />\n </span>\n ) : usesLeading ? (\n <span\n class=\"menu-item__icon-spacer ds-choice-item__icon ds-control-icon-box\"\n aria-hidden=\"true\"\n />\n ) : null}\n <div class=\"menu-item__content ds-choice-item__content\">\n <ds-text\n class=\"menu-item__label ds-choice-item__label ds-control-label-box\"\n as=\"span\"\n variant={CONTROL_TEXT_VARIANT[this.size]}\n color=\"secondary\"\n >\n {item.label}\n </ds-text>\n {item.subtext && (\n <ds-text\n class=\"menu-item__subtext ds-choice-item__subtext ds-control-label-box\"\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n color=\"secondary\"\n >\n {item.subtext}\n </ds-text>\n )}\n </div>\n <span class=\"menu-item-row__actions\">\n {item.trailingActions.map((action, actionIndex) => (\n <span class=\"menu-item-row__action\" key={action.id}>\n {actionIndex > 0 && (\n <span class=\"menu-item-row__divider\" aria-hidden=\"true\" />\n )}\n <ds-button-unfilled\n variant=\"icon\"\n icon={action.icon}\n size=\"md\"\n ariaLabel={action.label}\n hasBorder={false}\n pressScale={false}\n isInset\n insetDepth=\"single\"\n isInactive={action.isInactive ?? false}\n focusTabIndex={0}\n onDsClick={event => {\n event.stopPropagation();\n this.dsItemToggle.emit({ item, action, pressed: false });\n }}\n />\n </span>\n ))}\n </span>\n </div>\n );\n }\n return (\n <div\n key={item.value ?? `${si}-${item.label}`}\n class={{\n 'menu-item-row': true,\n 'menu-item-row--trailing-toggle': Boolean(item.trailingToggle),\n }}\n >\n <button\n type=\"button\"\n class={{\n 'menu-item': true,\n 'ds-choice-item': true,\n 'ds-control-frame': true,\n [`ds-control--${this.size}`]: true,\n 'ds-focus-ring-inset': true,\n 'ds-focus-ring--visible': isFocused && this.focusRingVisible,\n 'ds-interaction-fill': !locked,\n 'ds-interaction-fill--selected': !!item.isSelected && !locked,\n 'menu-item--selected': !!item.isSelected,\n 'menu-item--switch': !!item.showSwitch,\n 'ds-control-inactive': locked,\n 'menu-item--destructive': !!item.isDestructive,\n 'menu-item--focused': isFocused,\n 'menu-item--dragging': dragging,\n }}\n role={\n hasCompositeSections\n ? undefined\n : item.showSwitch\n ? 'menuitemcheckbox'\n : isSingleSelectionItem\n ? 'menuitemradio'\n : 'menuitem'\n }\n aria-checked={\n !hasCompositeSections && item.showSwitch\n ? String(!!item.switchValue)\n : isSingleSelectionItem\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 !isSingleSelectionItem &&\n item.isSelected\n ? 'true'\n : undefined\n }\n aria-description={\n item.reorderable\n ? item.isInactive\n ? 'Visibility cannot be changed. Alt + Arrow Up or Alt + Arrow Down moves this row.'\n : 'Drag to reorder. Alt + Arrow Up or Alt + Arrow Down moves this row.'\n : undefined\n }\n aria-disabled={item.isInactive ? 'true' : undefined}\n disabled={locked}\n tabIndex={\n hasCompositeSections || (this.embedded && idx === 0)\n ? 0\n : isFocused\n ? 0\n : -1\n }\n onMouseDown={() => {\n this.focusRingVisible = false;\n }}\n onClick={() => this.handleItemClick(item)}\n onFocus={() => {\n this.focusedIndex = idx;\n }}\n >\n {(dropBefore || dropAfter) && (\n <span\n class={{\n 'menu-item__drop-rail': true,\n 'menu-item__drop-rail--before': dropBefore,\n 'menu-item__drop-rail--after': dropAfter,\n }}\n data-menu-drop-rail\n aria-hidden=\"true\"\n />\n )}\n {item.reorderable || item.reorderHandleInactive ? (\n <span\n class={{\n 'menu-item__handle': true,\n 'menu-item__handle--inactive': Boolean(\n item.reorderHandleInactive\n ),\n 'ds-choice-item__icon': true,\n 'ds-control-icon-box': true,\n 'ds-interaction-fill__content': true,\n }}\n data-menu-handle\n aria-hidden=\"true\"\n onPointerDown={\n item.reorderable\n ? event => this.onReorderPointerDown(event, si, itemIndex, idx)\n : undefined\n }\n onClick={\n item.reorderable\n ? event => {\n event.preventDefault();\n event.stopPropagation();\n }\n : undefined\n }\n >\n <ds-icon name=\"DragDots\" size={this.size} color=\"inherit\" />\n </span>\n ) : usesLeading && item.icon ? (\n <span\n class=\"menu-item__icon ds-choice-item__icon ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-icon name={item.icon} size={this.size} color=\"inherit\" />\n </span>\n ) : usesLeading ? (\n <span\n class=\"menu-item__icon-spacer ds-choice-item__icon ds-control-icon-box\"\n aria-hidden=\"true\"\n />\n ) : null}\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 ds-control-label-box\"\n as=\"span\"\n variant={CONTROL_TEXT_VARIANT[this.size]}\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 ds-control-label-box\"\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n color=\"secondary\"\n >\n {item.subtext}\n </ds-text>\n )}\n </div>\n {item.tag && (\n <ds-tag\n class=\"ds-choice-item__tag ds-interaction-fill__content\"\n label={item.tag.label}\n size={MENU_ITEM_TAG_SIZE[this.size]}\n intent={item.tag.intent ?? 'neutral'}\n contrast={item.tag.contrast ?? 'faint'}\n rounded={item.tag.rounded ?? false}\n isInset\n />\n )}\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={this.size}\n checked={!!item.switchValue}\n isInactive={!!item.isInactive}\n presentation\n />\n )}\n </button>\n {item.trailingToggle && (\n <span class=\"menu-item-row__trailing-toggle\">\n <span class=\"menu-item-row__divider\" aria-hidden=\"true\" />\n <ds-button-unfilled\n variant=\"icon\"\n icon={item.trailingToggle.icon}\n size={this.size}\n ariaLabel={item.trailingToggle.label}\n hasBorder={false}\n pressScale={false}\n pressed={item.trailingToggle.pressed}\n isInactive={item.trailingToggle.isInactive ?? false}\n onDsChange={event => {\n event.stopPropagation();\n this.dsItemToggle.emit({ item, pressed: event.detail });\n }}\n />\n </span>\n )}\n </div>\n );\n })\n )}\n </div>\n ))\n )}\n </div>\n </div>\n <div class=\"ds-visually-hidden\" role=\"status\" aria-live=\"polite\">\n {this.reorderAnnouncement}\n </div>\n </Host>\n );\n }\n}\n",
595
614
  "type": "registry:ui"
596
615
  },
597
616
  {
@@ -611,12 +630,12 @@
611
630
  },
612
631
  {
613
632
  "path": "src/wc/components/Menu/menu-sections.ts",
614
- "content": "import type { MenuSection } from './menu-types';\nimport { isMenuSwatchPickerSection } from './menu-types';\n\n/** Preserve the exact menu context painted during exit motion. */\nexport function snapshotMenuSections(sections: readonly MenuSection[]): MenuSection[] {\n return sections.map(section => {\n if (isMenuSwatchPickerSection(section)) {\n return {\n ...section,\n ...(section.options ? { options: section.options.map(option => ({ ...option })) } : {}),\n ...(section.sections\n ? {\n sections: section.sections.map(swatchSection => ({\n ...swatchSection,\n options: swatchSection.options.map(option => ({ ...option })),\n })),\n }\n : {}),\n };\n }\n return { ...section, items: section.items.map(item => ({ ...item })) };\n });\n}\n",
633
+ "content": "import type { MenuSection } from './menu-types';\nimport { isMenuSwatchPickerSection } from './menu-types';\n\n/** Preserve the exact menu context painted during exit motion. */\nexport function snapshotMenuSections(sections: readonly MenuSection[]): MenuSection[] {\n return sections.map(section => {\n if (isMenuSwatchPickerSection(section)) {\n return {\n ...section,\n ...(section.options ? { options: section.options.map(option => ({ ...option })) } : {}),\n ...(section.sections\n ? {\n sections: section.sections.map(swatchSection => ({\n ...swatchSection,\n options: swatchSection.options.map(option => ({ ...option })),\n })),\n }\n : {}),\n };\n }\n return {\n ...section,\n items: section.items.map(item => ({\n ...item,\n ...(item.trailingToggle ? { trailingToggle: { ...item.trailingToggle } } : {}),\n ...(item.trailingActions\n ? { trailingActions: item.trailingActions.map(action => ({ ...action })) }\n : {}),\n })),\n };\n });\n}\n",
615
634
  "type": "registry:ui"
616
635
  },
617
636
  {
618
637
  "path": "src/wc/components/Menu/menu-types.ts",
619
- "content": "import type { SwatchPickerOption, SwatchPickerSection } from '../SwatchPicker/swatch-picker-types';\nimport type { TagContrast, TagIntent } from '../Tag/Tag';\n\nexport type { MenuAlign, MenuSide } from './menu-position';\nexport {\n PANEL_NAV_USER_MENU_PLACEMENT,\n PANEL_TOOLS_HEADER_MENU_PLACEMENT,\n type MenuPlacement,\n} from './menu-placement';\n\nexport interface MenuItemTagData {\n label: string;\n intent?: TagIntent;\n contrast?: TagContrast;\n rounded?: boolean;\n}\n\nexport interface MenuItemData {\n label: string;\n value?: string;\n subtext?: string;\n /** Leading decorative icon. Ignored when `reorderable` is true. */\n icon?: string;\n /** Show a supplemental brand notification dot. */\n dot?: boolean;\n /** Show a non-interactive tag at the trailing edge of the row. */\n tag?: MenuItemTagData;\n isSelected?: boolean;\n isInactive?: boolean;\n isDestructive?: boolean;\n /** Render a compact switch indicator; the menu row owns interaction. */\n showSwitch?: boolean;\n switchValue?: boolean;\n /** Prefix a drag handle and allow pointer and keyboard reorder within the contiguous reorderable run. */\n reorderable?: boolean;\n}\n\n/** Next section item order after a pointer or keyboard reorder. */\nexport interface MenuReorderDetail {\n item: MenuItemData;\n fromIndex: number;\n toIndex: number;\n sectionIndex: number;\n items: MenuItemData[];\n}\n\nexport interface MenuItemsSection {\n header?: string;\n items: MenuItemData[];\n}\n\nexport interface MenuSwatchPickerSection {\n header?: string;\n variant: 'swatch-picker';\n value: string;\n options?: SwatchPickerOption[];\n sections?: SwatchPickerSection[];\n groupLabel?: string;\n}\n\nexport type MenuSection = MenuItemsSection | MenuSwatchPickerSection;\n\nexport function isMenuSwatchPickerSection(\n section: MenuSection\n): section is MenuSwatchPickerSection {\n return 'variant' in section && section.variant === 'swatch-picker';\n}\n\nexport function isMenuPickerSection(section: MenuSection): section is MenuSwatchPickerSection {\n return isMenuSwatchPickerSection(section);\n}\n\nexport type { SwatchPickerOption, SwatchPickerSection };\n",
638
+ "content": "import type { SwatchPickerOption, SwatchPickerSection } from '../SwatchPicker/swatch-picker-types';\nimport type { TagContrast, TagIntent } from '../Tag/Tag';\n\nexport type { MenuAlign, MenuSide } from './menu-position';\nexport {\n PANEL_NAV_USER_MENU_PLACEMENT,\n PANEL_TOOLS_HEADER_MENU_PLACEMENT,\n type MenuPlacement,\n} from './menu-placement';\n\nexport interface MenuItemTagData {\n label: string;\n intent?: TagIntent;\n contrast?: TagContrast;\n rounded?: boolean;\n}\n\nexport interface MenuItemTrailingToggleData {\n /** Icon-only toggle glyph. */\n icon: string;\n /** Accessible name describing the requested state change. */\n label: string;\n /** Controlled toggle state. */\n pressed: boolean;\n /** Keep the toggle visible but unavailable. */\n isInactive?: boolean;\n}\n\nexport interface MenuItemActionData {\n /** Stable action identity for consumer event handling. */\n id: string;\n /** Icon-only action glyph. */\n icon: string;\n /** Accessible name describing the action. */\n label: string;\n /** Keep the action visible but unavailable. */\n isInactive?: boolean;\n}\n\nexport interface MenuItemData {\n label: string;\n value?: string;\n subtext?: string;\n /** Leading decorative icon. Ignored when `reorderable` is true. */\n icon?: string;\n /** Show a supplemental brand notification dot. */\n dot?: boolean;\n /** Show a non-interactive tag at the trailing edge of the row. */\n tag?: MenuItemTagData;\n isSelected?: boolean;\n isInactive?: boolean;\n isDestructive?: boolean;\n /** Render a compact switch indicator; the menu row owns interaction. */\n showSwitch?: boolean;\n switchValue?: boolean;\n /** Prefix a drag handle and allow pointer and keyboard reorder within the contiguous reorderable run. */\n reorderable?: boolean;\n /** Keep the drag handle visible as an inactive alignment affordance when reordering is unavailable. */\n reorderHandleInactive?: boolean;\n /** Append a separate unfilled icon toggle after a decorative divider. */\n trailingToggle?: MenuItemTrailingToggleData;\n /** Append independent unfilled icon actions while the row owns the shared hover surface. */\n trailingActions?: MenuItemActionData[];\n}\n\nexport interface MenuItemToggleDetail {\n item: MenuItemData;\n pressed: boolean;\n /** Identifies an independent trailing action; omitted for the legacy toggle control. */\n action?: MenuItemActionData;\n}\n\n/** Next section item order after a pointer or keyboard reorder. */\nexport interface MenuReorderDetail {\n item: MenuItemData;\n fromIndex: number;\n toIndex: number;\n sectionIndex: number;\n items: MenuItemData[];\n}\n\nexport interface MenuItemsSection {\n header?: string;\n /** Secondary heading beneath a shared parent section heading. */\n subheader?: string;\n /** Separate this subsection from the previous one with an inset divider. */\n insetDividerBefore?: boolean;\n items: MenuItemData[];\n}\n\nexport interface MenuSwatchPickerSection {\n header?: string;\n variant: 'swatch-picker';\n value: string;\n options?: SwatchPickerOption[];\n sections?: SwatchPickerSection[];\n groupLabel?: string;\n}\n\nexport type MenuSection = MenuItemsSection | MenuSwatchPickerSection;\n\nexport function isMenuSwatchPickerSection(\n section: MenuSection\n): section is MenuSwatchPickerSection {\n return 'variant' in section && section.variant === 'swatch-picker';\n}\n\nexport function isMenuPickerSection(section: MenuSection): section is MenuSwatchPickerSection {\n return isMenuSwatchPickerSection(section);\n}\n\nexport type { SwatchPickerOption, SwatchPickerSection };\n",
620
639
  "type": "registry:ui"
621
640
  }
622
641
  ]
@@ -167,7 +167,7 @@
167
167
  },
168
168
  "peerDependencies": {
169
169
  "required": [
170
- "@ds-mo/tokens ^6.6.1",
170
+ "@ds-mo/tokens ^6.7.0",
171
171
  "@ds-mo/icons ^8.1.1"
172
172
  ],
173
173
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -127,7 +127,7 @@
127
127
  },
128
128
  "peerDependencies": {
129
129
  "required": [
130
- "@ds-mo/tokens ^6.6.1",
130
+ "@ds-mo/tokens ^6.7.0",
131
131
  "@ds-mo/icons ^8.1.1"
132
132
  ],
133
133
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -285,7 +285,7 @@
285
285
  },
286
286
  "peerDependencies": {
287
287
  "required": [
288
- "@ds-mo/tokens ^6.6.1",
288
+ "@ds-mo/tokens ^6.7.0",
289
289
  "@ds-mo/icons ^8.1.1"
290
290
  ],
291
291
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -212,7 +212,7 @@
212
212
  },
213
213
  "peerDependencies": {
214
214
  "required": [
215
- "@ds-mo/tokens ^6.6.1",
215
+ "@ds-mo/tokens ^6.7.0",
216
216
  "@ds-mo/icons ^8.1.1"
217
217
  ],
218
218
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -259,7 +259,7 @@
259
259
  },
260
260
  "peerDependencies": {
261
261
  "required": [
262
- "@ds-mo/tokens ^6.6.1",
262
+ "@ds-mo/tokens ^6.7.0",
263
263
  "@ds-mo/icons ^8.1.1"
264
264
  ],
265
265
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -429,7 +429,7 @@
429
429
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: currentArea.",
430
430
  "peerDependencies": {
431
431
  "required": [
432
- "@ds-mo/tokens ^6.6.1",
432
+ "@ds-mo/tokens ^6.7.0",
433
433
  "@ds-mo/icons ^8.1.1"
434
434
  ],
435
435
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -368,7 +368,7 @@
368
368
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: sections, subsections.",
369
369
  "peerDependencies": {
370
370
  "required": [
371
- "@ds-mo/tokens ^6.6.1",
371
+ "@ds-mo/tokens ^6.7.0",
372
372
  "@ds-mo/icons ^8.1.1"
373
373
  ],
374
374
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -228,7 +228,7 @@
228
228
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: sections.",
229
229
  "peerDependencies": {
230
230
  "required": [
231
- "@ds-mo/tokens ^6.6.1",
231
+ "@ds-mo/tokens ^6.7.0",
232
232
  "@ds-mo/icons ^8.1.1"
233
233
  ],
234
234
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -390,7 +390,7 @@
390
390
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: dashboardGroups, settingsGroups.",
391
391
  "peerDependencies": {
392
392
  "required": [
393
- "@ds-mo/tokens ^6.6.1",
393
+ "@ds-mo/tokens ^6.7.0",
394
394
  "@ds-mo/icons ^8.1.1"
395
395
  ],
396
396
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -298,7 +298,7 @@
298
298
  },
299
299
  "peerDependencies": {
300
300
  "required": [
301
- "@ds-mo/tokens ^6.6.1",
301
+ "@ds-mo/tokens ^6.7.0",
302
302
  "@ds-mo/icons ^8.1.1"
303
303
  ],
304
304
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -410,7 +410,7 @@
410
410
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: pageSizeOptions.",
411
411
  "peerDependencies": {
412
412
  "required": [
413
- "@ds-mo/tokens ^6.6.1",
413
+ "@ds-mo/tokens ^6.7.0",
414
414
  "@ds-mo/icons ^8.1.1"
415
415
  ],
416
416
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."