@ds-mo/ui 12.6.2 → 12.8.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 (360) hide show
  1. package/README.md +44 -115
  2. package/dist/.build-stamp +1 -1
  3. package/dist/agent-patterns.json +26 -11
  4. package/dist/agent.json +285 -49
  5. package/dist/angular/ds-button-filled.d.ts +1 -1
  6. package/dist/angular/ds-button-filled.js +3 -3
  7. package/dist/angular/ds-button-unfilled.d.ts +1 -1
  8. package/dist/angular/ds-button-unfilled.js +3 -3
  9. package/dist/angular/ds-card-overview.d.ts +19 -0
  10. package/dist/angular/ds-card-overview.js +42 -0
  11. package/dist/angular/ds-menu.d.ts +1 -1
  12. package/dist/angular/ds-menu.js +3 -3
  13. package/dist/angular/index.d.ts +1 -1
  14. package/dist/angular/index.js +1 -0
  15. package/dist/angular/proxies.d.ts +1 -0
  16. package/dist/angular/proxies.js +1 -0
  17. package/dist/components/ds-agent-activity.js +1 -1
  18. package/dist/components/ds-agent-response.js +1 -1
  19. package/dist/components/ds-agent-source-list.js +1 -1
  20. package/dist/components/ds-agent-tool-call.js +1 -1
  21. package/dist/components/ds-attachment-list.js +1 -1
  22. package/dist/components/ds-badge.js +1 -1
  23. package/dist/components/ds-bar-nav.js +1 -1
  24. package/dist/components/ds-bar-nav.js.map +1 -1
  25. package/dist/components/ds-bar-title.js +1 -1
  26. package/dist/components/ds-bar-title.js.map +1 -1
  27. package/dist/components/ds-bar-workflow.js +1 -1
  28. package/dist/components/ds-bar-workflow.js.map +1 -1
  29. package/dist/components/ds-breadcrumb.js +1 -1
  30. package/dist/components/ds-button-filled.js +1 -1
  31. package/dist/components/ds-button-unfilled.js +1 -1
  32. package/dist/components/ds-card-data-viz-bar.js +1 -1
  33. package/dist/components/ds-card-data-viz-bar.js.map +1 -1
  34. package/dist/components/ds-card-data-viz-donut.js +1 -1
  35. package/dist/components/ds-card-data-viz-donut.js.map +1 -1
  36. package/dist/components/ds-card-data-viz-line.js +1 -1
  37. package/dist/components/ds-card-data-viz-line.js.map +1 -1
  38. package/dist/components/ds-card-overview.d.ts +11 -0
  39. package/dist/components/ds-card-overview.js +2 -0
  40. package/dist/components/ds-card-overview.js.map +1 -0
  41. package/dist/components/ds-card-setting.js +1 -1
  42. package/dist/components/ds-card-setting.js.map +1 -1
  43. package/dist/components/ds-card-shell-data-viz.js +1 -1
  44. package/dist/components/ds-chart-bar-stacked.js +1 -1
  45. package/dist/components/ds-chart-bar-stacked.js.map +1 -1
  46. package/dist/components/ds-chart-bar.js +1 -1
  47. package/dist/components/ds-chart-bar.js.map +1 -1
  48. package/dist/components/ds-chart-donut.js +1 -1
  49. package/dist/components/ds-chart-donut.js.map +1 -1
  50. package/dist/components/ds-chart-legend.js +1 -1
  51. package/dist/components/ds-chart-line.js +1 -1
  52. package/dist/components/ds-chart-line.js.map +1 -1
  53. package/dist/components/ds-checkbox.js +1 -1
  54. package/dist/components/ds-chip.js +1 -1
  55. package/dist/components/ds-code-block.js +1 -1
  56. package/dist/components/ds-conversation-list-item.js +1 -1
  57. package/dist/components/ds-conversation-list-item.js.map +1 -1
  58. package/dist/components/ds-conversation-list-section.js +1 -1
  59. package/dist/components/ds-conversation-list.js +1 -1
  60. package/dist/components/ds-conversation-list.js.map +1 -1
  61. package/dist/components/ds-divider.js +1 -1
  62. package/dist/components/ds-empty-state.js +1 -1
  63. package/dist/components/ds-field.js +1 -1
  64. package/dist/components/ds-field.js.map +1 -1
  65. package/dist/components/ds-input.js +1 -1
  66. package/dist/components/ds-input.js.map +1 -1
  67. package/dist/components/ds-markdown.js +1 -1
  68. package/dist/components/ds-menu.js +1 -1
  69. package/dist/components/ds-message-bubble.js +1 -1
  70. package/dist/components/ds-message-composer.js +1 -1
  71. package/dist/components/ds-message-composer.js.map +1 -1
  72. package/dist/components/ds-message-scroller.js +1 -1
  73. package/dist/components/ds-message-scroller.js.map +1 -1
  74. package/dist/components/ds-message.js +1 -1
  75. package/dist/components/ds-mobile-bar-nav.js +1 -1
  76. package/dist/components/ds-mobile-bar-nav.js.map +1 -1
  77. package/dist/components/ds-mobile-header.js +1 -1
  78. package/dist/components/ds-mobile-section-switcher.js +1 -1
  79. package/dist/components/ds-mobile-sheet-nav.js +1 -1
  80. package/dist/components/ds-mobile-sheet-nav.js.map +1 -1
  81. package/dist/components/ds-modal.js +1 -1
  82. package/dist/components/ds-modal.js.map +1 -1
  83. package/dist/components/ds-panel-nav.js +1 -1
  84. package/dist/components/ds-panel-nav.js.map +1 -1
  85. package/dist/components/ds-panel-sub-nav.js +1 -1
  86. package/dist/components/ds-panel-tool-header.js +1 -1
  87. package/dist/components/ds-panel-tool-search.js +1 -1
  88. package/dist/components/ds-panel-tool-search.js.map +1 -1
  89. package/dist/components/ds-panel-tools.js +1 -1
  90. package/dist/components/ds-radio.js +1 -1
  91. package/dist/components/ds-radio.js.map +1 -1
  92. package/dist/components/ds-scroll-overlay.js +1 -1
  93. package/dist/components/ds-select-multi.js +1 -1
  94. package/dist/components/ds-select-multi.js.map +1 -1
  95. package/dist/components/ds-select.js +1 -1
  96. package/dist/components/ds-select.js.map +1 -1
  97. package/dist/components/ds-shell-app.js +1 -1
  98. package/dist/components/ds-shell-app.js.map +1 -1
  99. package/dist/components/ds-shell-gradient-picker.js +1 -1
  100. package/dist/components/ds-shell-gradient-swatch.js +1 -1
  101. package/dist/components/ds-shell-page.js +1 -1
  102. package/dist/components/ds-shell-page.js.map +1 -1
  103. package/dist/components/ds-shell-tools.js +1 -1
  104. package/dist/components/ds-shell-tools.js.map +1 -1
  105. package/dist/components/ds-skeleton.js +1 -1
  106. package/dist/components/ds-skeleton.js.map +1 -1
  107. package/dist/components/ds-slider.js +1 -1
  108. package/dist/components/ds-slider.js.map +1 -1
  109. package/dist/components/ds-swatch-picker.js +1 -1
  110. package/dist/components/ds-switch.js +1 -1
  111. package/dist/components/ds-tab-group.js +1 -1
  112. package/dist/components/ds-tag.js +1 -1
  113. package/dist/components/ds-text.js +1 -1
  114. package/dist/components/ds-toast.js +1 -1
  115. package/dist/components/ds-toast.js.map +1 -1
  116. package/dist/components/ds-tooltip-data-viz.js +1 -1
  117. package/dist/components/ds-tooltip.js +1 -1
  118. package/dist/components/ds-typing-indicator.js +1 -1
  119. package/dist/components/p-2foAUsDN.js +2 -0
  120. package/dist/components/p-2foAUsDN.js.map +1 -0
  121. package/dist/components/{p-Ccj5Op6E.js → p-38PfdMkF.js} +2 -2
  122. package/dist/components/p-69lHhJiX.js +2 -0
  123. package/dist/components/p-69lHhJiX.js.map +1 -0
  124. package/dist/components/p-B6NvVdeO.js +2 -0
  125. package/dist/components/p-B6NvVdeO.js.map +1 -0
  126. package/dist/components/p-B6Vuayt6.js +2 -0
  127. package/dist/components/p-B6Vuayt6.js.map +1 -0
  128. package/dist/components/p-B6vE3ujd.js +2 -0
  129. package/dist/components/p-B6vE3ujd.js.map +1 -0
  130. package/dist/components/p-BBePKRHC.js +2 -0
  131. package/dist/components/p-BBePKRHC.js.map +1 -0
  132. package/dist/components/{p-CaRyxTMz.js → p-BFRMwl93.js} +2 -2
  133. package/dist/components/p-BKOlvCGS.js +2 -0
  134. package/dist/components/p-BKOlvCGS.js.map +1 -0
  135. package/dist/components/{p-C7bZ-jiU.js → p-BO5aqZcf.js} +2 -2
  136. package/dist/components/p-BX0BMhgz.js +2 -0
  137. package/dist/components/p-BX0BMhgz.js.map +1 -0
  138. package/dist/components/p-BXaieghV.js +2 -0
  139. package/dist/components/p-BXaieghV.js.map +1 -0
  140. package/dist/components/p-BlHzmszE.js +2 -0
  141. package/dist/components/{p-BkAMkjIR.js.map → p-BlHzmszE.js.map} +1 -1
  142. package/dist/components/p-By_s1wNN.js +2 -0
  143. package/dist/components/p-By_s1wNN.js.map +1 -0
  144. package/dist/components/p-C61yJkfr.js +2 -0
  145. package/dist/components/p-C61yJkfr.js.map +1 -0
  146. package/dist/components/{p-DXLWm4PF.js → p-CNgPwd11.js} +2 -2
  147. package/dist/components/p-CNjOyUQu.js +2 -0
  148. package/dist/components/p-CNjOyUQu.js.map +1 -0
  149. package/dist/components/{p-BwGnhInj.js → p-CPwpgUWT.js} +2 -2
  150. package/dist/components/p-CU2673IJ.js +2 -0
  151. package/dist/components/p-CU2673IJ.js.map +1 -0
  152. package/dist/components/{p-B9bxDfZV.js → p-CsX3ICz7.js} +2 -2
  153. package/dist/components/p-CuwopElg.js +2 -0
  154. package/dist/components/p-CuwopElg.js.map +1 -0
  155. package/dist/components/{p-D-xttdpn.js → p-D0e77tkd.js} +2 -2
  156. package/dist/components/p-D1bNvIpb.js +2 -0
  157. package/dist/components/p-D1bNvIpb.js.map +1 -0
  158. package/dist/components/{p-BwsGYxZx.js → p-D9u1_OEZ.js} +2 -2
  159. package/dist/components/{p-DQDpDYyF.js → p-DB9XJm2c.js} +2 -2
  160. package/dist/components/{p-CnHrJTD5.js → p-DIFUsoM-.js} +2 -2
  161. package/dist/components/p-DhKvqh5G.js +2 -0
  162. package/dist/components/p-DhKvqh5G.js.map +1 -0
  163. package/dist/components/p-DhLmoCe_.js +2 -0
  164. package/dist/components/p-DhLmoCe_.js.map +1 -0
  165. package/dist/components/p-DjbTFy2B.js +2 -0
  166. package/dist/components/p-DjbTFy2B.js.map +1 -0
  167. package/dist/components/{p-CWTih3Gc.js → p-DmAHufd5.js} +2 -2
  168. package/dist/components/p-DzM3SzPz.js +2 -0
  169. package/dist/components/p-DzM3SzPz.js.map +1 -0
  170. package/dist/components/{p-DqubRds2.js → p-G3qIxpQj.js} +2 -2
  171. package/dist/components/p-Re2kPrXm.js +2 -0
  172. package/dist/components/p-Re2kPrXm.js.map +1 -0
  173. package/dist/components/{p-DwX7UmLl.js → p-Utr23NC-.js} +2 -2
  174. package/dist/components/{p-DIoiApa-.js → p-Vdufoj7G.js} +2 -2
  175. package/dist/components/p-huw1oRbt.js +2 -0
  176. package/dist/components/p-huw1oRbt.js.map +1 -0
  177. package/dist/components/p-ukB-dPOK.js +2 -0
  178. package/dist/components/{p-BY0PzkUp.js.map → p-ukB-dPOK.js.map} +1 -1
  179. package/dist/components/p-yhj1lIPx.js +2 -0
  180. package/dist/docs/components.json +760 -87
  181. package/dist/lib/components/Menu/menu-position.d.ts +6 -6
  182. package/dist/lib/toast/toast-types.d.ts +3 -3
  183. package/dist/lib/utils/anchored-position.d.ts +34 -0
  184. package/dist/lib/utils/choice-popup-alignment.d.ts +2 -2
  185. package/dist/lib/utils/control-text.d.ts +3 -1
  186. package/dist/lib/utils/index.d.ts +3 -1
  187. package/dist/lib/utils/index.js +39 -0
  188. package/dist/lib/utils/index.js.map +3 -3
  189. package/dist/lib/utils/metric-change.d.ts +52 -0
  190. package/dist/lib/utils/token-defaults.d.ts +3 -0
  191. package/dist/mcp-data/registry/agent-activity.json +1 -1
  192. package/dist/mcp-data/registry/agent-response.json +1 -1
  193. package/dist/mcp-data/registry/agent-source-list.json +1 -1
  194. package/dist/mcp-data/registry/agent-tool-call.json +1 -1
  195. package/dist/mcp-data/registry/attachment-list.json +1 -1
  196. package/dist/mcp-data/registry/avatar.json +1 -1
  197. package/dist/mcp-data/registry/badge.json +1 -1
  198. package/dist/mcp-data/registry/bar-nav.json +3 -3
  199. package/dist/mcp-data/registry/bar-title.json +3 -3
  200. package/dist/mcp-data/registry/bar-workflow.json +3 -3
  201. package/dist/mcp-data/registry/breadcrumb.json +1 -1
  202. package/dist/mcp-data/registry/button-filled.json +125 -8
  203. package/dist/mcp-data/registry/button-unfilled.json +50 -5
  204. package/dist/mcp-data/registry/card-data-viz-bar.json +5 -4
  205. package/dist/mcp-data/registry/card-data-viz-donut.json +5 -3
  206. package/dist/mcp-data/registry/card-data-viz-line.json +5 -4
  207. package/dist/mcp-data/registry/card-overview.json +317 -0
  208. package/dist/mcp-data/registry/card-setting.json +3 -3
  209. package/dist/mcp-data/registry/card-shell-data-viz.json +3 -3
  210. package/dist/mcp-data/registry/chart-bar-stacked.json +14 -9
  211. package/dist/mcp-data/registry/chart-bar.json +15 -10
  212. package/dist/mcp-data/registry/chart-donut.json +3 -2
  213. package/dist/mcp-data/registry/chart-legend.json +1 -1
  214. package/dist/mcp-data/registry/chart-line.json +15 -10
  215. package/dist/mcp-data/registry/checkbox.json +10 -10
  216. package/dist/mcp-data/registry/chip.json +1 -1
  217. package/dist/mcp-data/registry/code-block.json +1 -1
  218. package/dist/mcp-data/registry/conversation-list-item.json +3 -3
  219. package/dist/mcp-data/registry/conversation-list-section.json +1 -1
  220. package/dist/mcp-data/registry/conversation-list.json +3 -3
  221. package/dist/mcp-data/registry/divider.json +1 -1
  222. package/dist/mcp-data/registry/empty-state.json +6 -2
  223. package/dist/mcp-data/registry/field.json +4 -3
  224. package/dist/mcp-data/registry/icon.json +1 -1
  225. package/dist/mcp-data/registry/input.json +9 -8
  226. package/dist/mcp-data/registry/loader.json +1 -1
  227. package/dist/mcp-data/registry/markdown.json +1 -1
  228. package/dist/mcp-data/registry/menu.json +24 -5
  229. package/dist/mcp-data/registry/message-bubble.json +1 -1
  230. package/dist/mcp-data/registry/message-composer.json +1 -1
  231. package/dist/mcp-data/registry/message-scroller.json +3 -3
  232. package/dist/mcp-data/registry/message.json +1 -1
  233. package/dist/mcp-data/registry/mobile-bar-nav.json +4 -4
  234. package/dist/mcp-data/registry/mobile-header.json +3 -3
  235. package/dist/mcp-data/registry/mobile-section-switcher.json +1 -1
  236. package/dist/mcp-data/registry/mobile-sheet-nav.json +6 -4
  237. package/dist/mcp-data/registry/modal.json +3 -3
  238. package/dist/mcp-data/registry/panel-nav.json +3 -3
  239. package/dist/mcp-data/registry/panel-sub-nav.json +1 -1
  240. package/dist/mcp-data/registry/panel-tool-header.json +3 -3
  241. package/dist/mcp-data/registry/panel-tool-search.json +3 -3
  242. package/dist/mcp-data/registry/panel-tools.json +4 -3
  243. package/dist/mcp-data/registry/radio.json +10 -10
  244. package/dist/mcp-data/registry/registry.json +288 -49
  245. package/dist/mcp-data/registry/scroll-overlay.json +1 -1
  246. package/dist/mcp-data/registry/select-multi.json +10 -8
  247. package/dist/mcp-data/registry/select.json +10 -8
  248. package/dist/mcp-data/registry/shell-app.json +3 -2
  249. package/dist/mcp-data/registry/shell-gradient-picker.json +1 -1
  250. package/dist/mcp-data/registry/shell-gradient-swatch.json +1 -1
  251. package/dist/mcp-data/registry/shell-page.json +5 -4
  252. package/dist/mcp-data/registry/shell-tools.json +2 -2
  253. package/dist/mcp-data/registry/skeleton.json +3 -3
  254. package/dist/mcp-data/registry/slider.json +4 -3
  255. package/dist/mcp-data/registry/swatch-picker.json +1 -1
  256. package/dist/mcp-data/registry/switch.json +8 -8
  257. package/dist/mcp-data/registry/tab-group.json +1 -1
  258. package/dist/mcp-data/registry/tag.json +1 -1
  259. package/dist/mcp-data/registry/text.json +1 -1
  260. package/dist/mcp-data/registry/toast.json +2 -2
  261. package/dist/mcp-data/registry/tooltip-data-viz.json +3 -1
  262. package/dist/mcp-data/registry/tooltip.json +3 -8
  263. package/dist/mcp-data/registry/typing-indicator.json +1 -1
  264. package/dist/react/components.d.ts +1 -0
  265. package/dist/react/components.js +1 -0
  266. package/dist/react/ds-card-overview.d.ts +12 -0
  267. package/dist/react/ds-card-overview.js +12 -0
  268. package/dist/styles/control-elevation.css +38 -2
  269. package/dist/types/components/ButtonFilled/ButtonFilled.d.ts +33 -1
  270. package/dist/types/components/ButtonUnfilled/ButtonUnfilled.d.ts +20 -0
  271. package/dist/types/components/CardOverview/CardOverview.d.ts +59 -0
  272. package/dist/types/components/CardOverview/card-overview-types.d.ts +32 -0
  273. package/dist/types/components/CardOverview/index.d.ts +2 -0
  274. package/dist/types/components/ChartBar/ChartBar.d.ts +11 -0
  275. package/dist/types/components/ChartBarStacked/ChartBarStacked.d.ts +11 -0
  276. package/dist/types/components/ChartLine/ChartLine.d.ts +11 -0
  277. package/dist/types/components/Checkbox/Checkbox.d.ts +1 -1
  278. package/dist/types/components/Input/Input.d.ts +1 -1
  279. package/dist/types/components/Menu/Menu.d.ts +11 -5
  280. package/dist/types/components/Menu/menu-position.d.ts +6 -6
  281. package/dist/types/components/MobileSheetNav/MobileSheetNav.d.ts +0 -1
  282. package/dist/types/components/Radio/Radio.d.ts +1 -1
  283. package/dist/types/components/Select/Select.d.ts +6 -23
  284. package/dist/types/components/SelectMulti/SelectMulti.d.ts +5 -20
  285. package/dist/types/components/ShellPage/ShellPage.d.ts +1 -0
  286. package/dist/types/components/ShellTools/ShellTools.d.ts +1 -0
  287. package/dist/types/components/Switch/Switch.d.ts +1 -1
  288. package/dist/types/components/Tooltip/Tooltip.d.ts +1 -4
  289. package/dist/types/components.d.ts +225 -4
  290. package/dist/types/toast/toast-types.d.ts +3 -3
  291. package/dist/types/utils/anchored-position-controller.d.ts +96 -0
  292. package/dist/types/utils/anchored-position.d.ts +35 -0
  293. package/dist/types/utils/cartesian-chart-layout.d.ts +13 -0
  294. package/dist/types/utils/chart-viewport-controller.d.ts +21 -0
  295. package/dist/types/utils/choice-list-parts.d.ts +4 -0
  296. package/dist/types/utils/choice-popup-alignment.d.ts +2 -2
  297. package/dist/types/utils/control-press.d.ts +9 -0
  298. package/dist/types/utils/control-text.d.ts +3 -1
  299. package/dist/types/utils/index.d.ts +3 -1
  300. package/dist/types/utils/metric-change.d.ts +53 -0
  301. package/dist/types/utils/select-controller.d.ts +67 -0
  302. package/dist/types/utils/token-defaults.d.ts +3 -0
  303. package/package.json +3 -3
  304. package/dist/components/p-B8C_yfLQ.js +0 -2
  305. package/dist/components/p-B8C_yfLQ.js.map +0 -1
  306. package/dist/components/p-B9xVWER6.js +0 -2
  307. package/dist/components/p-B9xVWER6.js.map +0 -1
  308. package/dist/components/p-BLea0TrP.js +0 -2
  309. package/dist/components/p-BLea0TrP.js.map +0 -1
  310. package/dist/components/p-BY0PzkUp.js +0 -2
  311. package/dist/components/p-BbgPGF2a.js +0 -2
  312. package/dist/components/p-BbgPGF2a.js.map +0 -1
  313. package/dist/components/p-BhC1YyId.js +0 -2
  314. package/dist/components/p-BhC1YyId.js.map +0 -1
  315. package/dist/components/p-BkAMkjIR.js +0 -2
  316. package/dist/components/p-C2ym_Ich.js +0 -2
  317. package/dist/components/p-C_ewSXTT.js +0 -2
  318. package/dist/components/p-C_ewSXTT.js.map +0 -1
  319. package/dist/components/p-CiqnkENu.js +0 -2
  320. package/dist/components/p-CiqnkENu.js.map +0 -1
  321. package/dist/components/p-Cx-ywr1e.js +0 -2
  322. package/dist/components/p-Cx-ywr1e.js.map +0 -1
  323. package/dist/components/p-CydY-v7X.js +0 -2
  324. package/dist/components/p-CydY-v7X.js.map +0 -1
  325. package/dist/components/p-DPME1wUJ.js +0 -2
  326. package/dist/components/p-DPME1wUJ.js.map +0 -1
  327. package/dist/components/p-DSa0Hnd3.js +0 -2
  328. package/dist/components/p-DSa0Hnd3.js.map +0 -1
  329. package/dist/components/p-DUlqUNbE.js +0 -2
  330. package/dist/components/p-DUlqUNbE.js.map +0 -1
  331. package/dist/components/p-DWdRSOBV.js +0 -2
  332. package/dist/components/p-DWdRSOBV.js.map +0 -1
  333. package/dist/components/p-DYcC5-R9.js +0 -2
  334. package/dist/components/p-DYcC5-R9.js.map +0 -1
  335. package/dist/components/p-DbpfpeQT.js +0 -2
  336. package/dist/components/p-DbpfpeQT.js.map +0 -1
  337. package/dist/components/p-Dg3xLna0.js +0 -2
  338. package/dist/components/p-Dg3xLna0.js.map +0 -1
  339. package/dist/components/p-DuFm-ieS.js +0 -2
  340. package/dist/components/p-DuFm-ieS.js.map +0 -1
  341. package/dist/components/p-uQhuDEVt.js +0 -2
  342. package/dist/components/p-uQhuDEVt.js.map +0 -1
  343. package/dist/lib/utils/anchored-popup.d.ts +0 -23
  344. package/dist/types/components/Tooltip/tooltip-position.d.ts +0 -21
  345. package/dist/types/utils/anchored-popup.d.ts +0 -24
  346. /package/dist/components/{p-Ccj5Op6E.js.map → p-38PfdMkF.js.map} +0 -0
  347. /package/dist/components/{p-CaRyxTMz.js.map → p-BFRMwl93.js.map} +0 -0
  348. /package/dist/components/{p-C7bZ-jiU.js.map → p-BO5aqZcf.js.map} +0 -0
  349. /package/dist/components/{p-DXLWm4PF.js.map → p-CNgPwd11.js.map} +0 -0
  350. /package/dist/components/{p-BwGnhInj.js.map → p-CPwpgUWT.js.map} +0 -0
  351. /package/dist/components/{p-B9bxDfZV.js.map → p-CsX3ICz7.js.map} +0 -0
  352. /package/dist/components/{p-D-xttdpn.js.map → p-D0e77tkd.js.map} +0 -0
  353. /package/dist/components/{p-BwsGYxZx.js.map → p-D9u1_OEZ.js.map} +0 -0
  354. /package/dist/components/{p-DQDpDYyF.js.map → p-DB9XJm2c.js.map} +0 -0
  355. /package/dist/components/{p-CnHrJTD5.js.map → p-DIFUsoM-.js.map} +0 -0
  356. /package/dist/components/{p-CWTih3Gc.js.map → p-DmAHufd5.js.map} +0 -0
  357. /package/dist/components/{p-DqubRds2.js.map → p-G3qIxpQj.js.map} +0 -0
  358. /package/dist/components/{p-DwX7UmLl.js.map → p-Utr23NC-.js.map} +0 -0
  359. /package/dist/components/{p-DIoiApa-.js.map → p-Vdufoj7G.js.map} +0 -0
  360. /package/dist/components/{p-C2ym_Ich.js.map → p-yhj1lIPx.js.map} +0 -0
@@ -38,7 +38,7 @@
38
38
  "actionLayout footer stretches a long action between 8px side and bottom insets.",
39
39
  "Both action layouts share the always-active scroll-overlay fade controller, reserve measured end clearance, and begin fading 8px before the visible action.",
40
40
  "ConversationList paints its measured fade on a separate surface-aware layer so contextual row actions retain a real backdrop for blur; hovered, focused, and menu-open rows rise above that decorative fade.",
41
- "The persistent action wrapper splits its md outer shadow from a top inset-highlight overlay so opaque filled actions keep the complete TokoMo elevation; slotted controls must disable optional resting borders."
41
+ "The persistent action wrapper splits its md outer shadow from a top inset-highlight overlay so opaque filled actions keep the complete TokoMo elevation; slotted buttons disable optional resting borders and the press-scale modifier moves the button and elevated surface as one unit."
42
42
  ],
43
43
  "responsiveBehavior": [
44
44
  "The list fills its owner, scrolls independently, and keeps its persistent action above scrolling rows."
@@ -102,7 +102,7 @@
102
102
  },
103
103
  "peerDependencies": {
104
104
  "required": [
105
- "@ds-mo/tokens ^3.1.1",
105
+ "@ds-mo/tokens ^5.0.0",
106
106
  "@ds-mo/icons ^6.4.0"
107
107
  ],
108
108
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -122,7 +122,7 @@
122
122
  },
123
123
  {
124
124
  "path": "src/wc/components/ConversationList/ConversationList.tsx",
125
- "content": "import { Component, Element, h, Host, Prop } from '@stencil/core';\nimport { ScrollOverlayController } from '../../utils/scroll-overlay-controller';\n\nexport type ConversationListActionLayout = 'floating' | 'footer';\n\n@Component({\n tag: 'ds-conversation-list',\n styleUrl: 'ConversationList.css',\n styleUrls: ['../../utils/scroll-edge-fade.css'],\n scoped: true,\n})\nexport class ConversationList {\n @Element() el!: HTMLElement;\n\n /** Corner icon action or full-width persistent footer action. */\n @Prop() actionLayout: ConversationListActionLayout = 'floating';\n\n private viewport?: HTMLElement;\n private content?: HTMLElement;\n private overlay?: HTMLElement;\n private scrollOverlayController?: ScrollOverlayController;\n private emptySlotObserver?: MutationObserver;\n\n componentDidLoad(): void {\n if (!this.viewport || !this.content || !this.overlay) return;\n this.scrollOverlayController = new ScrollOverlayController({\n host: this.el,\n viewport: this.viewport,\n content: this.content,\n overlay: this.overlay,\n overlayContentSelector: '[slot=\"actions\"]',\n });\n this.scrollOverlayController.connect();\n this.syncEmptyStateLayout();\n this.emptySlotObserver = new MutationObserver(this.syncEmptyStateLayout);\n this.emptySlotObserver.observe(this.el, { childList: true, subtree: true });\n }\n\n disconnectedCallback(): void {\n this.scrollOverlayController?.disconnect();\n this.emptySlotObserver?.disconnect();\n }\n\n private syncEmptyStateLayout = (): void => {\n const emptyState = this.el.querySelector<HTMLElement>('[slot=\"empty\"]');\n if (emptyState) {\n emptyState.style.setProperty('position', 'absolute');\n emptyState.style.setProperty('inset-block-start', '0');\n emptyState.style.setProperty('inset-inline', '0');\n emptyState.style.setProperty(\n 'inset-block-end',\n 'var(--ds-scroll-overlay-block-size, 0)',\n );\n emptyState.style.setProperty('height', 'auto');\n emptyState.style.setProperty('box-sizing', 'border-box');\n }\n this.scrollOverlayController?.sync();\n };\n\n render() {\n return (\n <Host>\n <div class=\"conversation-list\">\n <div\n class=\"conversation-list__viewport\"\n ref={element => {\n this.viewport = element;\n }}\n onScroll={() => this.scrollOverlayController?.sync()}\n >\n <div\n class=\"conversation-list__content\"\n ref={element => {\n this.content = element;\n }}\n >\n <slot />\n <div class=\"conversation-list__empty\">\n <slot name=\"empty\" />\n </div>\n </div>\n </div>\n <div\n class=\"conversation-list__fade scroll-edge-fade scroll-edge-fade--top\"\n aria-hidden=\"true\"\n />\n <div\n class={{\n 'conversation-list__actions': true,\n 'conversation-list__actions--footer': this.actionLayout === 'footer',\n }}\n ref={element => {\n this.overlay = element;\n }}\n >\n <div class=\"conversation-list__actions-surface ds-control-elevation ds-control-elevation--md\">\n <slot name=\"actions\" />\n </div>\n </div>\n </div>\n </Host>\n );\n }\n}\n",
125
+ "content": "import { Component, Element, h, Host, Prop } from '@stencil/core';\nimport { ScrollOverlayController } from '../../utils/scroll-overlay-controller';\n\nexport type ConversationListActionLayout = 'floating' | 'footer';\n\n@Component({\n tag: 'ds-conversation-list',\n styleUrl: 'ConversationList.css',\n styleUrls: ['../../utils/scroll-edge-fade.css'],\n scoped: true,\n})\nexport class ConversationList {\n @Element() el!: HTMLElement;\n\n /** Corner icon action or full-width persistent footer action. */\n @Prop() actionLayout: ConversationListActionLayout = 'floating';\n\n private viewport?: HTMLElement;\n private content?: HTMLElement;\n private overlay?: HTMLElement;\n private scrollOverlayController?: ScrollOverlayController;\n private emptySlotObserver?: MutationObserver;\n\n componentDidLoad(): void {\n if (!this.viewport || !this.content || !this.overlay) return;\n this.scrollOverlayController = new ScrollOverlayController({\n host: this.el,\n viewport: this.viewport,\n content: this.content,\n overlay: this.overlay,\n overlayContentSelector: '[slot=\"actions\"]',\n });\n this.scrollOverlayController.connect();\n this.syncEmptyStateLayout();\n this.emptySlotObserver = new MutationObserver(this.syncEmptyStateLayout);\n this.emptySlotObserver.observe(this.el, { childList: true, subtree: true });\n }\n\n disconnectedCallback(): void {\n this.scrollOverlayController?.disconnect();\n this.emptySlotObserver?.disconnect();\n }\n\n private syncEmptyStateLayout = (): void => {\n const emptyState = this.el.querySelector<HTMLElement>('[slot=\"empty\"]');\n if (emptyState) {\n emptyState.style.setProperty('position', 'absolute');\n emptyState.style.setProperty('inset-block-start', '0');\n emptyState.style.setProperty('inset-inline', '0');\n emptyState.style.setProperty(\n 'inset-block-end',\n 'var(--ds-scroll-overlay-block-size, 0)',\n );\n emptyState.style.setProperty('height', 'auto');\n emptyState.style.setProperty('box-sizing', 'border-box');\n }\n this.scrollOverlayController?.sync();\n };\n\n render() {\n return (\n <Host>\n <div class=\"conversation-list\">\n <div\n class=\"conversation-list__viewport\"\n ref={element => {\n this.viewport = element;\n }}\n onScroll={() => this.scrollOverlayController?.sync()}\n >\n <div\n class=\"conversation-list__content\"\n ref={element => {\n this.content = element;\n }}\n >\n <slot />\n <div class=\"conversation-list__empty\">\n <slot name=\"empty\" />\n </div>\n </div>\n </div>\n <div\n class=\"conversation-list__fade scroll-edge-fade scroll-edge-fade--top\"\n aria-hidden=\"true\"\n />\n <div\n class={{\n 'conversation-list__actions': true,\n 'conversation-list__actions--footer': this.actionLayout === 'footer',\n }}\n ref={element => {\n this.overlay = element;\n }}\n >\n <div class=\"conversation-list__actions-surface ds-control-elevation ds-control-elevation--md ds-control-elevation--press-scale\">\n <slot name=\"actions\" />\n </div>\n </div>\n </div>\n </Host>\n );\n }\n}\n",
126
126
  "type": "registry:ui"
127
127
  }
128
128
  ]
@@ -198,7 +198,7 @@
198
198
  },
199
199
  "peerDependencies": {
200
200
  "required": [
201
- "@ds-mo/tokens ^3.1.1",
201
+ "@ds-mo/tokens ^5.0.0",
202
202
  "@ds-mo/icons ^6.4.0"
203
203
  ],
204
204
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -57,7 +57,7 @@
57
57
  "Supported compositions are icon plus title plus body, title plus body, and body only; body copy is present in every supported composition.",
58
58
  "The decorative icon renders at xl in the primary foreground with an 8px gap before the text group and only when a title is also provided.",
59
59
  "The visual title uses title-small primary text; body uses body-medium secondary text with a 4px title-to-body gap.",
60
- "All content is centered. The owner supplies container size and surrounding padding; do not compose actions into or immediately alongside the current EmptyState pattern.",
60
+ "All content is centered. The owner supplies container size and surrounding padding; repeated panel and master-detail owners should share one empty-region geometry instead of wrapping each EmptyState differently. Do not compose actions into or immediately alongside the current EmptyState pattern.",
61
61
  "First-use, no-content, no-results, and no-access states reuse this primitive with cause-specific localized messages rather than visual variants."
62
62
  ],
63
63
  "responsiveBehavior": [
@@ -79,6 +79,10 @@
79
79
  {
80
80
  "label": "Storybook examples",
81
81
  "path": "src/wc/components/EmptyState/EmptyState.stories.ts"
82
+ },
83
+ {
84
+ "label": "Shared layout recipe foundation",
85
+ "path": "docs/layout-recipe-foundation.md"
82
86
  }
83
87
  ]
84
88
  },
@@ -166,7 +170,7 @@
166
170
  },
167
171
  "peerDependencies": {
168
172
  "required": [
169
- "@ds-mo/tokens ^3.1.1",
173
+ "@ds-mo/tokens ^5.0.0",
170
174
  "@ds-mo/icons ^6.4.0"
171
175
  ],
172
176
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -45,6 +45,7 @@
45
45
  "Do not use placeholder text as the only label. Keep label text concise and put format or consequence guidance in description."
46
46
  ],
47
47
  "states": [
48
+ "Label, control, description, and error content follow the shared 4px vertical field flow without changing the slotted control's width or semantics.",
48
49
  "Field exposes data-focused, data-filled, data-dirty, data-touched, data-invalid, data-disabled, and data-required hooks while the slotted control remains the source of value and interaction behavior.",
49
50
  "A rendered error replaces the description; preserve essential guidance in the error message so users retain what they need to correct the field.",
50
51
  "Field owns validation presentation but not validation timing; the form decides when to set error and errorMessage.",
@@ -193,7 +194,7 @@
193
194
  },
194
195
  "peerDependencies": {
195
196
  "required": [
196
- "@ds-mo/tokens ^3.1.1",
197
+ "@ds-mo/tokens ^5.0.0",
197
198
  "@ds-mo/icons ^6.4.0"
198
199
  ],
199
200
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -210,12 +211,12 @@
210
211
  "files": [
211
212
  {
212
213
  "path": "src/wc/components/Field/Field.css",
213
- "content": ":host {\n display: block;\n}\n\n.field {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050);\n}\n\n.field__control {\n display: flex;\n min-width: 0;\n}\n\n.field__control > * {\n width: 100%;\n}\n",
214
+ "content": "@import '../../utils/field-stack.css';\n\n:host {\n display: block;\n}\n\n.field__control {\n display: flex;\n min-width: 0;\n}\n\n.field__control > * {\n width: 100%;\n}\n",
214
215
  "type": "registry:ui"
215
216
  },
216
217
  {
217
218
  "path": "src/wc/components/Field/Field.tsx",
218
- "content": "import { Component, Element, Prop, State, Watch, h, Host } from '@stencil/core';\n\ninterface FieldControl extends HTMLElement {\n value?: string | string[];\n inputId?: string;\n error?: boolean;\n disabled?: boolean;\n isInactive?: boolean;\n required?: boolean;\n setFocus?: () => Promise<void> | void;\n}\n\ninterface AuthoredAria {\n labelledby: string[];\n describedby: string[];\n}\n\nlet fieldCounter = 0;\n\nconst tokens = (value: string | null | undefined): string[] =>\n value?.split(/\\s+/).filter(Boolean) ?? [];\n\nconst uniqueTokens = (...groups: string[][]): string[] => [...new Set(groups.flat())];\n\n@Component({\n tag: 'ds-field',\n styleUrl: 'Field.css',\n scoped: true,\n})\nexport class Field {\n @Element() el!: HTMLElement;\n\n /** Persistent visible label for the single slotted control. */\n @Prop() label!: string;\n /** Explicit ID for the slotted control; generated when omitted. */\n @Prop() fieldId: string | undefined;\n /** Optional guidance associated with the slotted control while no visible error is shown. */\n @Prop() description: string | undefined;\n /** Invalid visual and accessible state forwarded to supported ds controls. */\n @Prop() error: boolean = false;\n /** Visible error associated with the slotted control while error is true. */\n @Prop() errorMessage: string | undefined;\n\n @State() private focused = false;\n @State() private filled = false;\n @State() private dirty = false;\n @State() private touched = false;\n @State() private controlDisabled = false;\n @State() private controlRequired = false;\n\n private readonly generatedId = `ds-field-${++fieldCounter}`;\n private controlContainer?: HTMLDivElement;\n private control?: FieldControl;\n private initialValue = '';\n private authoredAria = new WeakMap<FieldControl, AuthoredAria>();\n\n componentDidLoad() {\n this.syncControl();\n requestAnimationFrame(() => this.syncControl());\n }\n\n componentDidRender() {\n this.syncControl();\n }\n\n @Watch('fieldId')\n @Watch('description')\n @Watch('error')\n @Watch('errorMessage')\n onFieldContractChange() {\n this.syncControl();\n }\n\n private get controlId(): string {\n return this.fieldId || this.generatedId;\n }\n\n private get labelId(): string {\n return `${this.controlId}-label`;\n }\n\n private get descriptionId(): string {\n return `${this.controlId}-description`;\n }\n\n private get errorId(): string {\n return `${this.controlId}-error`;\n }\n\n private get renderedDescription(): boolean {\n return Boolean(this.description) && !this.renderedError;\n }\n\n private get renderedError(): boolean {\n return this.error && Boolean(this.errorMessage);\n }\n\n private readValue(control: FieldControl): string {\n const value = control.value;\n return Array.isArray(value) ? value.join('\\u001f') : String(value ?? '');\n }\n\n private updateControlAttribute(control: FieldControl, name: string, value: string | undefined) {\n if (value === undefined) {\n if (control.hasAttribute(name)) control.removeAttribute(name);\n return;\n }\n if (control.getAttribute(name) !== value) control.setAttribute(name, value);\n }\n\n private findControl(): FieldControl | undefined {\n return this.controlContainer?.querySelector<FieldControl>(':scope > :not(slot)') ?? undefined;\n }\n\n private syncControl = () => {\n const control = this.findControl();\n if (!control) return;\n\n if (control !== this.control) {\n this.control = control;\n this.initialValue = this.readValue(control);\n this.authoredAria.set(control, {\n labelledby: tokens(control.getAttribute('aria-labelledby')),\n describedby: tokens(control.getAttribute('aria-describedby')),\n });\n }\n\n const isDsControl = control.tagName.startsWith('DS-');\n if (isDsControl && 'inputId' in control) control.inputId = this.controlId;\n else this.updateControlAttribute(control, 'id', this.controlId);\n\n const authored = this.authoredAria.get(control) ?? { labelledby: [], describedby: [] };\n const labelledby = uniqueTokens(authored.labelledby, [this.labelId]).join(' ');\n const describedby = uniqueTokens(\n authored.describedby,\n this.renderedDescription ? [this.descriptionId] : [],\n this.renderedError ? [this.errorId] : [],\n ).join(' ');\n\n this.updateControlAttribute(control, 'aria-labelledby', labelledby);\n this.updateControlAttribute(control, 'aria-describedby', describedby || undefined);\n this.updateControlAttribute(control, 'aria-invalid', this.error ? 'true' : undefined);\n\n if (isDsControl && 'error' in control && control.error !== this.error) control.error = this.error;\n\n const value = this.readValue(control);\n this.filled = value.length > 0;\n this.dirty = value !== this.initialValue;\n this.controlDisabled = Boolean(control.disabled || control.isInactive || control.hasAttribute('disabled'));\n this.controlRequired = Boolean(control.required || control.hasAttribute('required'));\n };\n\n private handleLabelClick = (event: MouseEvent) => {\n if (!this.control?.tagName.startsWith('DS-')) return;\n event.preventDefault();\n this.control.setFocus?.();\n };\n\n private handleFocusIn = () => {\n this.focused = true;\n };\n\n private handleFocusOut = (event: FocusEvent) => {\n if (event.relatedTarget instanceof Node && this.el.contains(event.relatedTarget)) return;\n this.focused = false;\n this.touched = true;\n };\n\n private handleValueChange = () => {\n this.syncControl();\n };\n\n render() {\n return (\n <Host\n data-focused={this.focused ? '' : undefined}\n data-filled={this.filled ? '' : undefined}\n data-dirty={this.dirty ? '' : undefined}\n data-touched={this.touched ? '' : undefined}\n data-invalid={this.error ? '' : undefined}\n data-disabled={this.controlDisabled ? '' : undefined}\n data-required={this.controlRequired ? '' : undefined}\n onFocusin={this.handleFocusIn}\n onFocusout={this.handleFocusOut}\n onInput={this.handleValueChange}\n onChange={this.handleValueChange}\n onDsChange={this.handleValueChange}\n >\n <div class=\"field\">\n <ds-text\n class=\"field__label\"\n as=\"label\"\n variant=\"text-body-small\"\n color=\"primary\"\n emphasis\n for={this.controlId}\n textId={this.labelId}\n onClick={this.handleLabelClick}\n >\n {this.label}\n </ds-text>\n <div class=\"field__control\" ref={el => (this.controlContainer = el)}>\n <slot onSlotchange={this.syncControl} />\n </div>\n {this.renderedDescription && (\n <ds-text\n class=\"field__description\"\n as=\"div\"\n variant=\"text-body-small\"\n color=\"secondary\"\n textId={this.descriptionId}\n >\n {this.description}\n </ds-text>\n )}\n {this.renderedError && (\n <ds-text\n class=\"field__error\"\n as=\"div\"\n variant=\"text-body-small\"\n color=\"negative\"\n textId={this.errorId}\n role=\"alert\"\n >\n {this.errorMessage}\n </ds-text>\n )}\n </div>\n </Host>\n );\n }\n}\n",
219
+ "content": "import { Component, Element, Prop, State, Watch, h, Host } from '@stencil/core';\n\ninterface FieldControl extends HTMLElement {\n value?: string | string[];\n inputId?: string;\n error?: boolean;\n disabled?: boolean;\n isInactive?: boolean;\n required?: boolean;\n setFocus?: () => Promise<void> | void;\n}\n\ninterface AuthoredAria {\n labelledby: string[];\n describedby: string[];\n}\n\nlet fieldCounter = 0;\n\nconst tokens = (value: string | null | undefined): string[] =>\n value?.split(/\\s+/).filter(Boolean) ?? [];\n\nconst uniqueTokens = (...groups: string[][]): string[] => [...new Set(groups.flat())];\n\n@Component({\n tag: 'ds-field',\n styleUrl: 'Field.css',\n scoped: true,\n})\nexport class Field {\n @Element() el!: HTMLElement;\n\n /** Persistent visible label for the single slotted control. */\n @Prop() label!: string;\n /** Explicit ID for the slotted control; generated when omitted. */\n @Prop() fieldId: string | undefined;\n /** Optional guidance associated with the slotted control while no visible error is shown. */\n @Prop() description: string | undefined;\n /** Invalid visual and accessible state forwarded to supported ds controls. */\n @Prop() error: boolean = false;\n /** Visible error associated with the slotted control while error is true. */\n @Prop() errorMessage: string | undefined;\n\n @State() private focused = false;\n @State() private filled = false;\n @State() private dirty = false;\n @State() private touched = false;\n @State() private controlDisabled = false;\n @State() private controlRequired = false;\n\n private readonly generatedId = `ds-field-${++fieldCounter}`;\n private controlContainer?: HTMLDivElement;\n private control?: FieldControl;\n private initialValue = '';\n private authoredAria = new WeakMap<FieldControl, AuthoredAria>();\n\n componentDidLoad() {\n this.syncControl();\n requestAnimationFrame(() => this.syncControl());\n }\n\n componentDidRender() {\n this.syncControl();\n }\n\n @Watch('fieldId')\n @Watch('description')\n @Watch('error')\n @Watch('errorMessage')\n onFieldContractChange() {\n this.syncControl();\n }\n\n private get controlId(): string {\n return this.fieldId || this.generatedId;\n }\n\n private get labelId(): string {\n return `${this.controlId}-label`;\n }\n\n private get descriptionId(): string {\n return `${this.controlId}-description`;\n }\n\n private get errorId(): string {\n return `${this.controlId}-error`;\n }\n\n private get renderedDescription(): boolean {\n return Boolean(this.description) && !this.renderedError;\n }\n\n private get renderedError(): boolean {\n return this.error && Boolean(this.errorMessage);\n }\n\n private readValue(control: FieldControl): string {\n const value = control.value;\n return Array.isArray(value) ? value.join('\\u001f') : String(value ?? '');\n }\n\n private updateControlAttribute(control: FieldControl, name: string, value: string | undefined) {\n if (value === undefined) {\n if (control.hasAttribute(name)) control.removeAttribute(name);\n return;\n }\n if (control.getAttribute(name) !== value) control.setAttribute(name, value);\n }\n\n private findControl(): FieldControl | undefined {\n return this.controlContainer?.querySelector<FieldControl>(':scope > :not(slot)') ?? undefined;\n }\n\n private syncControl = () => {\n const control = this.findControl();\n if (!control) return;\n\n if (control !== this.control) {\n this.control = control;\n this.initialValue = this.readValue(control);\n this.authoredAria.set(control, {\n labelledby: tokens(control.getAttribute('aria-labelledby')),\n describedby: tokens(control.getAttribute('aria-describedby')),\n });\n }\n\n const isDsControl = control.tagName.startsWith('DS-');\n if (isDsControl && 'inputId' in control) control.inputId = this.controlId;\n else this.updateControlAttribute(control, 'id', this.controlId);\n\n const authored = this.authoredAria.get(control) ?? { labelledby: [], describedby: [] };\n const labelledby = uniqueTokens(authored.labelledby, [this.labelId]).join(' ');\n const describedby = uniqueTokens(\n authored.describedby,\n this.renderedDescription ? [this.descriptionId] : [],\n this.renderedError ? [this.errorId] : [],\n ).join(' ');\n\n this.updateControlAttribute(control, 'aria-labelledby', labelledby);\n this.updateControlAttribute(control, 'aria-describedby', describedby || undefined);\n this.updateControlAttribute(control, 'aria-invalid', this.error ? 'true' : undefined);\n\n if (isDsControl && 'error' in control && control.error !== this.error) control.error = this.error;\n\n const value = this.readValue(control);\n this.filled = value.length > 0;\n this.dirty = value !== this.initialValue;\n this.controlDisabled = Boolean(control.disabled || control.isInactive || control.hasAttribute('disabled'));\n this.controlRequired = Boolean(control.required || control.hasAttribute('required'));\n };\n\n private handleLabelClick = (event: MouseEvent) => {\n if (!this.control?.tagName.startsWith('DS-')) return;\n event.preventDefault();\n this.control.setFocus?.();\n };\n\n private handleFocusIn = () => {\n this.focused = true;\n };\n\n private handleFocusOut = (event: FocusEvent) => {\n if (event.relatedTarget instanceof Node && this.el.contains(event.relatedTarget)) return;\n this.focused = false;\n this.touched = true;\n };\n\n private handleValueChange = () => {\n this.syncControl();\n };\n\n render() {\n return (\n <Host\n data-focused={this.focused ? '' : undefined}\n data-filled={this.filled ? '' : undefined}\n data-dirty={this.dirty ? '' : undefined}\n data-touched={this.touched ? '' : undefined}\n data-invalid={this.error ? '' : undefined}\n data-disabled={this.controlDisabled ? '' : undefined}\n data-required={this.controlRequired ? '' : undefined}\n onFocusin={this.handleFocusIn}\n onFocusout={this.handleFocusOut}\n onInput={this.handleValueChange}\n onChange={this.handleValueChange}\n onDsChange={this.handleValueChange}\n >\n <div class=\"field ds-field-stack\">\n <ds-text\n class=\"field__label\"\n as=\"label\"\n variant=\"text-body-small\"\n color=\"primary\"\n emphasis\n for={this.controlId}\n textId={this.labelId}\n onClick={this.handleLabelClick}\n >\n {this.label}\n </ds-text>\n <div class=\"field__control\" ref={el => (this.controlContainer = el)}>\n <slot onSlotchange={this.syncControl} />\n </div>\n {this.renderedDescription && (\n <ds-text\n class=\"field__description\"\n as=\"div\"\n variant=\"text-body-small\"\n color=\"secondary\"\n textId={this.descriptionId}\n >\n {this.description}\n </ds-text>\n )}\n {this.renderedError && (\n <ds-text\n class=\"field__error\"\n as=\"div\"\n variant=\"text-body-small\"\n color=\"negative\"\n textId={this.errorId}\n role=\"alert\"\n >\n {this.errorMessage}\n </ds-text>\n )}\n </div>\n </Host>\n );\n }\n}\n",
219
220
  "type": "registry:ui"
220
221
  }
221
222
  ]
@@ -194,7 +194,7 @@
194
194
  },
195
195
  "peerDependencies": {
196
196
  "required": [
197
- "@ds-mo/tokens ^3.1.1",
197
+ "@ds-mo/tokens ^5.0.0",
198
198
  "@ds-mo/icons ^6.4.0"
199
199
  ],
200
200
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
3
  "name": "input",
4
4
  "title": "Input",
5
- "description": "Form-associated single-line text field for primary and secondary application backgrounds, with native text input behavior, three control densities, optional leading and trailing content, search clearing, and validation.",
5
+ "description": "Form-associated single-line text field for primary and secondary application backgrounds, with native text input behavior, four control densities, optional leading and trailing content, search clearing, and validation.",
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": "Form-associated single-line text field for primary and secondary application backgrounds, with native text input behavior, three control densities, optional leading and trailing content, search clearing, and validation.",
16
+ "summary": "Form-associated single-line text field for primary and secondary application backgrounds, with native text input behavior, four control densities, optional leading and trailing content, search clearing, and validation.",
17
17
  "useWhen": [
18
18
  "A user must enter or edit one line of text, an email address, telephone number, URL, search query, or password.",
19
19
  "The value must participate in native, Angular, or React form state.",
@@ -54,6 +54,7 @@
54
54
  "Suffix content must not carry information that is otherwise absent from the field's visible label, value, or description."
55
55
  ],
56
56
  "states": [
57
+ "Every size applies one complete control-density recipe across frame height, inline padding, icon zones, text-container inset, gap, and radius; supporting error content follows the shared 4px field flow.",
57
58
  "The normal field boundary uses the shared secondary inset border. Focus promotes it to the thicker bold-brand inset stroke, while error uses the thicker bold-negative inset stroke without changing geometry.",
58
59
  "Placeholder text is secondary at rest and quaternary while the editable field is focused because it becomes decorative once typing begins.",
59
60
  "Entered values use the parent surface's primary foreground; prefix icons, suffix content, and resting placeholder text use its secondary foreground.",
@@ -65,7 +66,7 @@
65
66
  ],
66
67
  "responsiveBehavior": [
67
68
  "Form fields normally fill their parent; use hug width only in a compact toolbar or filter layout with an explicit width constraint.",
68
- "Choose density from the containing layout: md for ordinary forms, sm or xs only where the surrounding controls use the same compact recipe.",
69
+ "Choose density from the containing layout: md for ordinary forms, lg for a touch-primary layout, and sm or xs only where surrounding controls use the same compact recipe.",
69
70
  "The owner supplies layout constraints and decides when a compact field moves to its own row."
70
71
  ],
71
72
  "frameworkCaveats": {
@@ -261,7 +262,7 @@
261
262
  },
262
263
  "size": {
263
264
  "type": "InputSize",
264
- "resolvedType": "\"md\" | \"sm\" | \"xs\"",
265
+ "resolvedType": "\"lg\" | \"md\" | \"sm\" | \"xs\"",
265
266
  "attribute": "size",
266
267
  "default": "'md'",
267
268
  "required": false,
@@ -487,7 +488,7 @@
487
488
  },
488
489
  "size": {
489
490
  "type": "InputSize",
490
- "resolvedType": "\"md\" | \"sm\" | \"xs\"",
491
+ "resolvedType": "\"lg\" | \"md\" | \"sm\" | \"xs\"",
491
492
  "attribute": "size",
492
493
  "default": "'md'",
493
494
  "required": false,
@@ -565,7 +566,7 @@
565
566
  },
566
567
  "peerDependencies": {
567
568
  "required": [
568
- "@ds-mo/tokens ^3.1.1",
569
+ "@ds-mo/tokens ^5.0.0",
569
570
  "@ds-mo/icons ^6.4.0"
570
571
  ],
571
572
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -585,12 +586,12 @@
585
586
  "files": [
586
587
  {
587
588
  "path": "src/wc/components/Input/Input.css",
588
- "content": "@import '../../utils/interaction-fill.css';\n@import '../../utils/control-inactive.css';\n@import '../../utils/control-density.css';\n@import '../../utils/control-width.css';\n@import '../../utils/typography.css';\n\n:host(.input-host) {\n display: inline-flex;\n flex-direction: column;\n gap: var(--dimension-space-050);\n --ds-input-fg: var(--color-foreground-primary);\n --ds-input-placeholder: var(--color-foreground-secondary);\n --ds-input-placeholder-active: var(--color-foreground-quaternary);\n --ds-input-adornment-fg: var(--color-foreground-secondary);\n --ds-input-border: var(--color-border-secondary);\n}\n\n:host(.ds-control-width--fill),\n:host(.ds-control-width--fill) .input-control {\n width: 100%;\n}\n\n.input-control {\n display: inline-flex;\n align-items: center;\n box-sizing: border-box;\n height: var(--ds-control-height, var(--dimension-size-400));\n padding: 0 var(--ds-control-padding-inline, var(--dimension-space-075));\n gap: var(--ds-control-gap, var(--dimension-space-050));\n overflow: hidden;\n background: transparent;\n border: none;\n border-radius: var(--ds-control-radius, var(--dimension-radius-025));\n color: var(--ds-input-fg);\n}\n\n.input-control--bordered {\n --ds-interaction-border-width: var(--dimension-stroke-width-012);\n --ds-interaction-border-color: var(--ds-input-border);\n}\n\n.input-control--error {\n --ds-interaction-border-width: var(--dimension-stroke-width-018);\n --ds-interaction-border-color: var(--color-border-bold-negative);\n}\n\n.input-control:focus-within:not(.input-control--error) {\n --ds-interaction-border-width: var(--dimension-stroke-width-018);\n --ds-interaction-border-color: var(--color-border-bold-brand);\n}\n\n:host(.ds-control-inactive) .input-control::after {\n background: transparent;\n}\n\n.native-input {\n position: relative;\n z-index: 2;\n flex: 1 1 auto;\n min-width: 0;\n height: 100%;\n padding: 0 var(--ds-control-label-inset, var(--dimension-space-025));\n background: transparent;\n border: none;\n border-radius: inherit;\n outline: none;\n color: var(--ds-input-fg);\n font-family: inherit;\n -webkit-appearance: none;\n appearance: none;\n}\n\n.native-input:disabled {\n cursor: not-allowed;\n}\n\n.native-input:read-only:not(:disabled) {\n cursor: default;\n}\n\n.native-input::placeholder {\n color: var(--ds-input-placeholder);\n}\n\n.native-input:focus::placeholder {\n color: var(--ds-input-placeholder-active);\n}\n\n.native-input[type='search']::-webkit-search-cancel-button,\n.native-input[type='search']::-webkit-search-decoration {\n display: none;\n}\n\n.input-control__prefix,\n.input-control__suffix {\n position: relative;\n z-index: 2;\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 display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--ds-input-adornment-fg);\n}\n\n.input-control__suffix--empty {\n display: none;\n}\n\n.input-control__clear {\n position: relative;\n z-index: 2;\n flex-shrink: 0;\n align-self: center;\n}\n\n.input-control.ds-control--md:has(.input-control__clear),\n.input-control.ds-control--sm:has(.input-control__clear) {\n padding-right: var(--dimension-space-050);\n}\n\n.input-control.ds-control--xs:has(.input-control__clear) {\n padding-right: 0;\n}\n\n.error-text {\n width: 100%;\n}\n",
589
+ "content": "@import '../../utils/interaction-fill.css';\n@import '../../utils/control-inactive.css';\n@import '../../utils/control-density.css';\n@import '../../utils/control-parts.css';\n@import '../../utils/control-width.css';\n@import '../../utils/field-stack.css';\n@import '../../utils/typography.css';\n\n:host(.input-host) {\n display: inline-flex;\n --ds-input-fg: var(--color-foreground-primary);\n --ds-input-placeholder: var(--color-foreground-secondary);\n --ds-input-placeholder-active: var(--color-foreground-quaternary);\n --ds-input-adornment-fg: var(--color-foreground-secondary);\n --ds-input-border: var(--color-border-secondary);\n}\n\n:host(.ds-control-width--fill),\n:host(.ds-control-width--fill) .input-control {\n width: 100%;\n}\n\n.input-control {\n overflow: hidden;\n background: transparent;\n border: none;\n border-radius: var(--ds-control-radius, var(--dimension-radius-025));\n color: var(--ds-input-fg);\n}\n\n.input-control--bordered {\n --ds-interaction-border-width: var(--dimension-stroke-width-012);\n --ds-interaction-border-color: var(--ds-input-border);\n}\n\n.input-control--error {\n --ds-interaction-border-width: var(--dimension-stroke-width-018);\n --ds-interaction-border-color: var(--color-border-bold-negative);\n}\n\n.input-control:focus-within:not(.input-control--error) {\n --ds-interaction-border-width: var(--dimension-stroke-width-018);\n --ds-interaction-border-color: var(--color-border-bold-brand);\n}\n\n:host(.ds-control-inactive) .input-control::after {\n background: transparent;\n}\n\n.native-input {\n position: relative;\n z-index: 2;\n flex: 1 1 auto;\n min-width: 0;\n height: 100%;\n background: transparent;\n border: none;\n border-radius: inherit;\n outline: none;\n color: var(--ds-input-fg);\n font-family: inherit;\n -webkit-appearance: none;\n appearance: none;\n}\n\n.native-input:disabled {\n cursor: not-allowed;\n}\n\n.native-input:read-only:not(:disabled) {\n cursor: default;\n}\n\n.native-input::placeholder {\n color: var(--ds-input-placeholder);\n}\n\n.native-input:focus::placeholder {\n color: var(--ds-input-placeholder-active);\n}\n\n.native-input[type='search']::-webkit-search-cancel-button,\n.native-input[type='search']::-webkit-search-decoration {\n display: none;\n}\n\n.input-control__prefix,\n.input-control__suffix {\n position: relative;\n z-index: 2;\n color: var(--ds-input-adornment-fg);\n}\n\n.input-control__suffix--empty {\n display: none;\n}\n\n.input-control__clear {\n position: relative;\n z-index: 2;\n flex-shrink: 0;\n align-self: center;\n}\n\n.input-control.ds-control--lg:has(.input-control__clear),\n.input-control.ds-control--md:has(.input-control__clear),\n.input-control.ds-control--sm:has(.input-control__clear) {\n padding-right: var(--dimension-space-050);\n}\n\n.input-control.ds-control--xs:has(.input-control__clear) {\n padding-right: 0;\n}\n\n.error-text {\n width: 100%;\n}\n",
589
590
  "type": "registry:ui"
590
591
  },
591
592
  {
592
593
  "path": "src/wc/components/Input/Input.tsx",
593
- "content": "import { AttachInternals, Component, Prop, State, Event, EventEmitter, Element, Method, Watch, h, Host } from '@stencil/core';\nimport {\n controlWidthClass,\n CONTROL_TEXT_VARIANT,\n DEFAULT_REQUIRED_MESSAGE,\n setRequiredValidity,\n type ControlWidth,\n} from '../../utils';\n\nexport type InputType = 'text' | 'email' | 'tel' | 'url' | 'search' | 'password';\nexport type InputSize = 'md' | 'sm' | 'xs';\nexport type InputWidth = ControlWidth;\n\nconst ICON_SIZE: Record<InputSize, 'md' | 'sm' | 'xs'> = {\n md: 'md',\n sm: 'sm',\n xs: 'xs',\n};\n\nconst CLEAR_BUTTON_SIZE: Record<InputSize, 'sm' | 'xs'> = {\n md: 'sm',\n sm: 'xs',\n xs: 'xs',\n};\n\nlet idCounter = 0;\n\n@Component({\n tag: 'ds-input',\n styleUrl: 'Input.css',\n scoped: true,\n formAssociated: true,\n})\nexport class Input {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n private generatedId = `ds-input-${++idCounter}`;\n private errorId = `${this.generatedId}-error`;\n\n @Prop({ mutable: true }) value: string = '';\n @Prop({ reflect: true }) name: string | undefined;\n @Prop({ reflect: true }) form: string | undefined;\n @Prop({ reflect: true }) disabled: boolean = false;\n /** Keeps the value focusable and submittable while preventing edits. */\n @Prop({ reflect: true }) readOnly: boolean = false;\n @Prop({ reflect: true }) required: boolean = false;\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n @Prop() clearLabel: string = 'Clear';\n @Prop() placeholder: string | undefined;\n @Prop() type: InputType = 'text';\n /** Native browser autofill hint. */\n @Prop({ attribute: 'autocomplete' }) autoComplete: string | undefined;\n /** Preferred virtual keyboard without changing the value semantics. */\n @Prop({ attribute: 'inputmode' }) inputMode: string = '';\n /** Preferred virtual-keyboard action label. */\n @Prop({ attribute: 'enterkeyhint' }) enterKeyHint: string = '';\n /** Control density. */\n @Prop() size: InputSize = 'md';\n /** Width fit — fill the parent (default) or hug the available content. */\n @Prop() width: InputWidth = 'fill';\n /** Show the standard inset border. */\n @Prop() hasBorder: boolean = true;\n /** Optional leading icon name. */\n @Prop() icon: string | undefined;\n @Prop() isInactive: boolean = false;\n @Prop() autoFocus: boolean = false;\n @Prop() error: boolean = false;\n @Prop() errorMessage: string | undefined;\n /** Associates the internal input with an external <label>. */\n @Prop() inputId: string | undefined;\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;\n @Prop({ attribute: 'aria-describedby' }) ariaDescribedby: string | undefined;\n\n @Event() dsChange!: EventEmitter<string>;\n @Event() dsClear!: EventEmitter<void>;\n\n private initialValue = '';\n @State() private formDisabled = false;\n @State() private hasSuffix = false;\n @State() private focused = false;\n @State() private touched = false;\n\n componentWillLoad() {\n this.initialValue = this.value;\n this.hasSuffix = Boolean(this.el.querySelector('[slot=\"suffix\"]'));\n this.syncFormValue();\n }\n\n componentDidRender() {\n const hasSuffix = Boolean(this.el.querySelector('[slot=\"suffix\"]'));\n if (hasSuffix !== this.hasSuffix) this.hasSuffix = hasSuffix;\n }\n\n @Watch('value')\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('required')\n syncFormValue() {\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n this.internals.setFormValue(inactive ? null : this.value);\n const missing = this.required && !inactive && this.value.length === 0;\n setRequiredValidity(this.internals, missing, this.requiredMessage);\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.value = this.initialValue;\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n this.value = typeof state === 'string' ? state : '';\n }\n\n @Method()\n async setFocus() {\n this.el.querySelector('input')?.focus();\n }\n\n private handleInput = (e: Event) => {\n this.value = (e.target as HTMLInputElement).value;\n this.dsChange.emit(this.value);\n };\n\n private handleFocus = () => {\n this.focused = true;\n };\n\n private handleBlur = () => {\n this.focused = false;\n this.touched = true;\n };\n\n private handleClear = () => {\n this.value = '';\n this.dsChange.emit('');\n this.dsClear.emit();\n this.el.querySelector('input')?.focus();\n };\n\n render() {\n const inputId = this.inputId ?? this.generatedId;\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n const filled = this.value.length > 0;\n const dirty = this.value !== this.initialValue;\n const showClear = this.type === 'search' && filled && !inactive && !this.readOnly;\n const showError = this.error && Boolean(this.errorMessage);\n const textVariant = CONTROL_TEXT_VARIANT[this.size];\n const iconSize = ICON_SIZE[this.size];\n\n const describedBy = [\n this.ariaDescribedby,\n showError ? this.errorId : undefined,\n ].filter(Boolean).join(' ') || undefined;\n\n return (\n <Host\n class={{\n 'input-host': true,\n 'ds-control-inactive': inactive,\n [`ds-control--${this.size}`]: true,\n ...controlWidthClass(this.width),\n }}\n data-disabled={inactive ? '' : undefined}\n data-readonly={this.readOnly ? '' : undefined}\n data-required={this.required ? '' : undefined}\n data-invalid={this.error ? '' : undefined}\n data-filled={filled ? '' : undefined}\n data-focused={this.focused ? '' : undefined}\n data-dirty={dirty ? '' : undefined}\n data-touched={this.touched ? '' : undefined}\n >\n <div\n class={{\n 'input-control': true,\n 'input-control--bordered': this.hasBorder || this.error,\n 'input-control--error': this.error,\n 'ds-interaction-fill': true,\n [`ds-control--${this.size}`]: true,\n }}\n >\n {this.icon && (\n <span class=\"input-control__prefix ds-interaction-fill__content\" aria-hidden=\"true\">\n <ds-icon name={this.icon} size={iconSize} color=\"inherit\" />\n </span>\n )}\n <input\n type={this.type}\n id={inputId}\n value={this.value}\n placeholder={this.placeholder}\n disabled={inactive}\n readOnly={this.readOnly}\n required={this.required}\n autoFocus={this.autoFocus}\n autoComplete={this.autoComplete}\n inputMode={this.inputMode || undefined}\n enterKeyHint={this.enterKeyHint || undefined}\n class={`native-input ds-text--${textVariant.replace('text-', '')} ds-text--regular ds-interaction-fill__content`}\n aria-label={this.ariaLabel}\n aria-labelledby={this.ariaLabelledby}\n aria-describedby={describedBy}\n aria-invalid={this.error ? 'true' : undefined}\n onInput={this.handleInput}\n onFocus={this.handleFocus}\n onBlur={this.handleBlur}\n />\n <span class={{ 'input-control__suffix': true, 'input-control__suffix--empty': !this.hasSuffix }}>\n <slot name=\"suffix\" />\n </span>\n {showClear && (\n <ds-button-unfilled\n class=\"input-control__clear\"\n variant=\"icon\"\n size={CLEAR_BUTTON_SIZE[this.size]}\n icon=\"CrossCircle\"\n hasBorder={false}\n rounded\n ariaLabel={this.clearLabel}\n onDsClick={this.handleClear}\n />\n )}\n </div>\n {showError && (\n <ds-text\n class=\"error-text\"\n as=\"div\"\n variant=\"text-body-small\"\n color=\"negative\"\n textId={this.errorId}\n role=\"alert\"\n >\n {this.errorMessage}\n </ds-text>\n )}\n </Host>\n );\n }\n}\n",
594
+ "content": "import { AttachInternals, Component, Prop, State, Event, EventEmitter, Element, Method, Watch, h, Host } from '@stencil/core';\nimport {\n controlWidthClass,\n CONTROL_TEXT_VARIANT,\n DEFAULT_REQUIRED_MESSAGE,\n setRequiredValidity,\n type ControlWidth,\n} from '../../utils';\n\nexport type InputType = 'text' | 'email' | 'tel' | 'url' | 'search' | 'password';\nexport type InputSize = 'lg' | 'md' | 'sm' | 'xs';\nexport type InputWidth = ControlWidth;\n\nconst ICON_SIZE: Record<InputSize, 'lg' | 'md' | 'sm' | 'xs'> = {\n lg: 'lg',\n md: 'md',\n sm: 'sm',\n xs: 'xs',\n};\n\nconst CLEAR_BUTTON_SIZE: Record<InputSize, 'md' | 'sm' | 'xs'> = {\n lg: 'md',\n md: 'sm',\n sm: 'xs',\n xs: 'xs',\n};\n\nlet idCounter = 0;\n\n@Component({\n tag: 'ds-input',\n styleUrl: 'Input.css',\n scoped: true,\n formAssociated: true,\n})\nexport class Input {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n private generatedId = `ds-input-${++idCounter}`;\n private errorId = `${this.generatedId}-error`;\n\n @Prop({ mutable: true }) value: string = '';\n @Prop({ reflect: true }) name: string | undefined;\n @Prop({ reflect: true }) form: string | undefined;\n @Prop({ reflect: true }) disabled: boolean = false;\n /** Keeps the value focusable and submittable while preventing edits. */\n @Prop({ reflect: true }) readOnly: boolean = false;\n @Prop({ reflect: true }) required: boolean = false;\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n @Prop() clearLabel: string = 'Clear';\n @Prop() placeholder: string | undefined;\n @Prop() type: InputType = 'text';\n /** Native browser autofill hint. */\n @Prop({ attribute: 'autocomplete' }) autoComplete: string | undefined;\n /** Preferred virtual keyboard without changing the value semantics. */\n @Prop({ attribute: 'inputmode' }) inputMode: string = '';\n /** Preferred virtual-keyboard action label. */\n @Prop({ attribute: 'enterkeyhint' }) enterKeyHint: string = '';\n /** Control density. */\n @Prop() size: InputSize = 'md';\n /** Width fit — fill the parent (default) or hug the available content. */\n @Prop() width: InputWidth = 'fill';\n /** Show the standard inset border. */\n @Prop() hasBorder: boolean = true;\n /** Optional leading icon name. */\n @Prop() icon: string | undefined;\n @Prop() isInactive: boolean = false;\n @Prop() autoFocus: boolean = false;\n @Prop() error: boolean = false;\n @Prop() errorMessage: string | undefined;\n /** Associates the internal input with an external <label>. */\n @Prop() inputId: string | undefined;\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;\n @Prop({ attribute: 'aria-describedby' }) ariaDescribedby: string | undefined;\n\n @Event() dsChange!: EventEmitter<string>;\n @Event() dsClear!: EventEmitter<void>;\n\n private initialValue = '';\n @State() private formDisabled = false;\n @State() private hasSuffix = false;\n @State() private focused = false;\n @State() private touched = false;\n\n componentWillLoad() {\n this.initialValue = this.value;\n this.hasSuffix = Boolean(this.el.querySelector('[slot=\"suffix\"]'));\n this.syncFormValue();\n }\n\n componentDidRender() {\n const hasSuffix = Boolean(this.el.querySelector('[slot=\"suffix\"]'));\n if (hasSuffix !== this.hasSuffix) this.hasSuffix = hasSuffix;\n }\n\n @Watch('value')\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('required')\n syncFormValue() {\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n this.internals.setFormValue(inactive ? null : this.value);\n const missing = this.required && !inactive && this.value.length === 0;\n setRequiredValidity(this.internals, missing, this.requiredMessage);\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.value = this.initialValue;\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n this.value = typeof state === 'string' ? state : '';\n }\n\n @Method()\n async setFocus() {\n this.el.querySelector('input')?.focus();\n }\n\n private handleInput = (e: Event) => {\n this.value = (e.target as HTMLInputElement).value;\n this.dsChange.emit(this.value);\n };\n\n private handleFocus = () => {\n this.focused = true;\n };\n\n private handleBlur = () => {\n this.focused = false;\n this.touched = true;\n };\n\n private handleClear = () => {\n this.value = '';\n this.dsChange.emit('');\n this.dsClear.emit();\n this.el.querySelector('input')?.focus();\n };\n\n render() {\n const inputId = this.inputId ?? this.generatedId;\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n const filled = this.value.length > 0;\n const dirty = this.value !== this.initialValue;\n const showClear = this.type === 'search' && filled && !inactive && !this.readOnly;\n const showError = this.error && Boolean(this.errorMessage);\n const textVariant = CONTROL_TEXT_VARIANT[this.size];\n const iconSize = ICON_SIZE[this.size];\n\n const describedBy = [\n this.ariaDescribedby,\n showError ? this.errorId : undefined,\n ].filter(Boolean).join(' ') || undefined;\n\n return (\n <Host\n class={{\n 'input-host': true,\n 'ds-field-stack': true,\n 'ds-control-inactive': inactive,\n [`ds-control--${this.size}`]: true,\n ...controlWidthClass(this.width),\n }}\n data-disabled={inactive ? '' : undefined}\n data-readonly={this.readOnly ? '' : undefined}\n data-required={this.required ? '' : undefined}\n data-invalid={this.error ? '' : undefined}\n data-filled={filled ? '' : undefined}\n data-focused={this.focused ? '' : undefined}\n data-dirty={dirty ? '' : undefined}\n data-touched={this.touched ? '' : undefined}\n >\n <div\n class={{\n 'input-control': true,\n 'ds-control-frame': true,\n 'input-control--bordered': this.hasBorder || this.error,\n 'input-control--error': this.error,\n 'ds-interaction-fill': true,\n [`ds-control--${this.size}`]: true,\n }}\n >\n {this.icon && (\n <span class=\"input-control__prefix ds-control-icon-box ds-interaction-fill__content\" aria-hidden=\"true\">\n <ds-icon name={this.icon} size={iconSize} color=\"inherit\" />\n </span>\n )}\n <input\n type={this.type}\n id={inputId}\n value={this.value}\n placeholder={this.placeholder}\n disabled={inactive}\n readOnly={this.readOnly}\n required={this.required}\n autoFocus={this.autoFocus}\n autoComplete={this.autoComplete}\n inputMode={this.inputMode || undefined}\n enterKeyHint={this.enterKeyHint || undefined}\n class={`native-input ds-control-label-box ds-text--${textVariant.replace('text-', '')} ds-text--regular ds-interaction-fill__content`}\n aria-label={this.ariaLabel}\n aria-labelledby={this.ariaLabelledby}\n aria-describedby={describedBy}\n aria-invalid={this.error ? 'true' : undefined}\n onInput={this.handleInput}\n onFocus={this.handleFocus}\n onBlur={this.handleBlur}\n />\n <span class={{ 'input-control__suffix': true, 'ds-control-icon-box': true, 'input-control__suffix--empty': !this.hasSuffix }}>\n <slot name=\"suffix\" />\n </span>\n {showClear && (\n <ds-button-unfilled\n class=\"input-control__clear\"\n variant=\"icon\"\n size={CLEAR_BUTTON_SIZE[this.size]}\n icon=\"CrossCircle\"\n hasBorder={false}\n rounded\n ariaLabel={this.clearLabel}\n onDsClick={this.handleClear}\n />\n )}\n </div>\n {showError && (\n <ds-text\n class=\"error-text\"\n as=\"div\"\n variant=\"text-body-small\"\n color=\"negative\"\n textId={this.errorId}\n role=\"alert\"\n >\n {this.errorMessage}\n </ds-text>\n )}\n </Host>\n );\n }\n}\n",
594
595
  "type": "registry:ui"
595
596
  }
596
597
  ]
@@ -165,7 +165,7 @@
165
165
  },
166
166
  "peerDependencies": {
167
167
  "required": [
168
- "@ds-mo/tokens ^3.1.1",
168
+ "@ds-mo/tokens ^5.0.0",
169
169
  "@ds-mo/icons ^6.4.0"
170
170
  ],
171
171
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -117,7 +117,7 @@
117
117
  },
118
118
  "peerDependencies": {
119
119
  "required": [
120
- "@ds-mo/tokens ^3.1.1",
120
+ "@ds-mo/tokens ^5.0.0",
121
121
  "@ds-mo/icons ^6.4.0"
122
122
  ],
123
123
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -53,7 +53,7 @@
53
53
  "Anchor the menu to the control that opens it and return focus to that control when the menu closes.",
54
54
  "Open with visible initial focus only when the trigger was activated from the keyboard.",
55
55
  "Keep disabled items discoverable only when their unavailable state is meaningful.",
56
- "Switch rows use menuitemcheckbox semantics on the row; the nested ds-switch is an aria-hidden md presentation indicator.",
56
+ "Switch rows use menuitemcheckbox semantics on the row; the nested ds-switch is an aria-hidden presentation indicator at the same density as the menu.",
57
57
  "Single-selection menus use menuitemradio semantics and aria-checked on each row, but render no nested radio control; selected interaction fill and primary foreground provide the visual state.",
58
58
  "A swatch-picker section changes the popup to a non-modal dialog so its radio group and ordinary preference buttons keep valid nested semantics; Tab moves between its controls, then leaves and closes at the popup boundary.",
59
59
  "Use notification dots only as supplemental status and not as the sole indication of important information."
@@ -64,7 +64,8 @@
64
64
  "After dsSelect from a switch row, replace the items or sections array with updated switchValue data so the controlled indicator reflects the new state.",
65
65
  "For selectionMode single, keep exactly one item isSelected and replace the items or sections array after dsSelect; Menu never mutates consumer-owned selection data.",
66
66
  "Arrays, objects, and element anchors must be assigned as JavaScript properties.",
67
- "Use a swatch-picker section for a small curated visual preference and apply dsSwatchSelect immediately without closing the popup; the legacy gradient-picker section and dsGradientSelect remain compatibility-only."
67
+ "Use a swatch-picker section for a small curated visual preference and apply dsSwatchSelect immediately without closing the popup; the legacy gradient-picker section and dsGradientSelect remain compatibility-only.",
68
+ "Ordinary rows, section headings, and supporting text consume the Menu size. At xs, both the primary label and supporting text use caption metrics."
68
69
  ],
69
70
  "responsiveBehavior": [
70
71
  "Keep the default choice-cell anchor alignment so the first or last interactive row edge aligns with its trigger, matching Select; use popup-frame only for deliberate custom frame geometry.",
@@ -216,6 +217,15 @@
216
217
  "required": false,
217
218
  "mutable": false,
218
219
  "description": "Gap between anchor and menu — number (px) or TokoMo length (`var(--dimension-space-050)`, etc.)."
220
+ },
221
+ "size": {
222
+ "type": "MenuSize",
223
+ "resolvedType": "\"lg\" | \"md\" | \"sm\" | \"xs\"",
224
+ "attribute": "size",
225
+ "default": "'md'",
226
+ "required": false,
227
+ "mutable": false,
228
+ "description": "Choice-row density."
219
229
  }
220
230
  },
221
231
  "events": [
@@ -383,6 +393,15 @@
383
393
  "required": false,
384
394
  "mutable": false,
385
395
  "description": "Gap between anchor and menu — number (px) or TokoMo length (`var(--dimension-space-050)`, etc.)."
396
+ },
397
+ "size": {
398
+ "type": "MenuSize",
399
+ "resolvedType": "\"lg\" | \"md\" | \"sm\" | \"xs\"",
400
+ "attribute": "size",
401
+ "default": "'md'",
402
+ "required": false,
403
+ "mutable": false,
404
+ "description": "Choice-row density."
386
405
  }
387
406
  },
388
407
  "events": [
@@ -450,7 +469,7 @@
450
469
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: anchor, items, sections.",
451
470
  "peerDependencies": {
452
471
  "required": [
453
- "@ds-mo/tokens ^3.1.1",
472
+ "@ds-mo/tokens ^5.0.0",
454
473
  "@ds-mo/icons ^6.4.0"
455
474
  ],
456
475
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -475,7 +494,7 @@
475
494
  },
476
495
  {
477
496
  "path": "src/wc/components/Menu/Menu.tsx",
478
- "content": "import {\n Component,\n Prop,\n State,\n Event,\n EventEmitter,\n Element,\n Watch,\n Listen,\n h,\n Host,\n} from '@stencil/core';\nimport {\n choicePopupMinWidth,\n resolveChoicePopupAlignOffset,\n resolveCssLengthPx,\n resolveMotionTimeMs,\n TOKEN_CSS_LENGTHS,\n TOKEN_DEFAULTS,\n type ChoicePopupAnchorAlignment,\n} from '../../utils';\nimport { computeMenuPosition, type MenuAlign, type MenuSide } from './menu-position';\nimport type { MenuItemData, MenuSection } from './menu-types';\n\nexport type MenuSelectionMode = 'none' | 'single';\nimport {\n isMenuGradientPickerSection,\n isMenuPickerSection,\n isMenuSwatchPickerSection,\n} from './menu-types';\nimport {\n shellGradientPickerSections,\n type ShellGradientPreset,\n} from '../../shell/shell-gradient-presets';\n\n/** rAF retries while the popup mounts or the anchor resolves. */\nconst POSITION_RETRY_BUDGET = 8;\nconst MENU_FOCUSABLE_SELECTOR = [\n 'a[href]',\n 'button:not([disabled])',\n 'input:not([disabled]):not([type=\"hidden\"])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(',');\n\nfunction snapshotMenuSections(sections: MenuSection[]): MenuSection[] {\n return sections.map(section => {\n if (isMenuGradientPickerSection(section)) return { ...section };\n if (isMenuSwatchPickerSection(section)) {\n return {\n ...section,\n options: section.options?.map(option => ({ ...option })),\n sections: section.sections?.map(swatchSection => ({\n ...swatchSection,\n options: swatchSection.options.map(option => ({ ...option })),\n })),\n };\n }\n return { ...section, items: section.items.map(item => ({ ...item })) };\n });\n}\n\n@Component({\n tag: 'ds-menu',\n styleUrl: 'Menu.css',\n scoped: true,\n})\nexport class Menu {\n @Element() el!: HTMLElement;\n\n @Prop({ mutable: true }) open: boolean = false;\n @Prop() items: MenuItemData[] = [];\n /** Give ordinary rows mutually-exclusive menu semantics using isSelected. */\n @Prop() selectionMode: MenuSelectionMode = 'none';\n @Prop() sections: MenuSection[] = [];\n /** Preferred side; placement flips to the opposite side when that offers a better fit. */\n @Prop() side: MenuSide = 'bottom';\n @Prop() align: MenuAlign = 'start';\n /** Align choice-row edges to the anchor by default; use `popup-frame` only for custom frame geometry. */\n @Prop() anchorAlignment: ChoicePopupAnchorAlignment = 'choice-cell';\n /** Gap between anchor and menu — number (px) or TokoMo length (`var(--dimension-space-050)`, etc.). */\n @Prop() sideOffset: number | string = TOKEN_CSS_LENGTHS.space050;\n /** Cross-axis offset — number (px) or TokoMo length. */\n @Prop() alignOffset: number | string = 0;\n @Prop() menuWidth: string | undefined;\n @Prop() minWidth: string | undefined;\n /** External trigger element to position against. Set via JS: menuEl.anchor = buttonEl */\n @Prop() anchor: HTMLElement | undefined;\n /** ID of the external trigger element for positioning */\n @Prop() anchorId: string | undefined;\n /** Show a visible ring on the initially focused menu item. Use only when the opener was keyboard-driven. */\n @Prop() initialFocusVisible: boolean = false;\n /** Accessible name for the popup menu. */\n @Prop() menuLabel: string = 'Menu';\n\n @State() private shouldRender: boolean = false;\n @State() private closing: boolean = false;\n @State() private pos: { x: number; y: number } = { x: 0, y: 0 };\n @State() private focusedIndex: number = 0;\n @State() private positionReady: boolean = false;\n @State() private focusRingVisible: boolean = false;\n\n @Event() dsClose!: EventEmitter<void>;\n /** Emitted after the popup's exit motion is complete and its rendered content is removed. */\n @Event() dsAfterClose!: EventEmitter<void>;\n @Event() dsSelect!: EventEmitter<MenuItemData>;\n /** Emitted when a `gradient-picker` section swatch is chosen. */\n @Event() dsGradientSelect!: EventEmitter<ShellGradientPreset>;\n /** Emitted when a generic `swatch-picker` section option is chosen. */\n @Event() dsSwatchSelect!: EventEmitter<string>;\n\n private clickOutsideHandler: ((e: MouseEvent) => void) | null = null;\n private scrollResizeHandler: (() => void) | null = null;\n private closeTimer: ReturnType<typeof setTimeout> | null = null;\n private positionRetryRaf: number | null = null;\n private livePositionRaf: number | null = null;\n /** Last content actually painted while open; retained unchanged through exit motion. */\n private lastRenderedSections: MenuSection[] = [];\n private closingSections: MenuSection[] | null = null;\n\n componentDidLoad() {\n if (this.open) this.onOpenChange(true);\n }\n\n componentDidRender() {\n if (!this.shouldRender) return;\n const popup = this.el.querySelector<HTMLElement>('.menu-popup');\n if (!popup || typeof popup.showPopover !== 'function') return;\n if (!popup.matches(':popover-open')) popup.showPopover();\n }\n\n disconnectedCallback() {\n this.cancelPositionRetry();\n this.cancelLivePositionUpdate();\n this.teardownListeners();\n }\n\n @Watch('open')\n onOpenChange(isOpen: boolean) {\n if (isOpen) {\n this.teardownListeners();\n this.closingSections = null;\n this.shouldRender = true;\n this.closing = false;\n this.positionReady = false;\n this.focusRingVisible = this.initialFocusVisible;\n this.setupListeners();\n this.schedulePositionUpdate(() => {\n this.focusInitialItem();\n });\n } else if (this.shouldRender) {\n this.cancelPositionRetry();\n this.captureClosingSections();\n this.closing = true;\n this.teardownListeners();\n const closeAnimationMs = this.closeAnimationMs;\n if (closeAnimationMs <= 0) {\n this.finishClose();\n return;\n }\n this.closeTimer = setTimeout(() => {\n this.closeTimer = null;\n this.finishClose();\n }, closeAnimationMs);\n }\n }\n\n @Watch('anchor')\n @Watch('anchorId')\n onAnchorChange() {\n if (this.open) this.schedulePositionUpdate();\n }\n\n @Watch('side')\n @Watch('align')\n @Watch('anchorAlignment')\n @Watch('sideOffset')\n @Watch('alignOffset')\n onPositionPropsChange() {\n if (this.open) this.schedulePositionUpdate();\n }\n\n private get viewportPadPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n }\n\n private get sideOffsetPx(): number {\n return resolveCssLengthPx(this.sideOffset, TOKEN_DEFAULTS.space050);\n }\n\n private get alignOffsetPx(): number {\n return resolveCssLengthPx(this.alignOffset, 0);\n }\n\n private get popupFallbackWidthPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuWidthXs, TOKEN_DEFAULTS.menuWidthXs);\n }\n\n private get popupFallbackHeightPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuFallbackHeight, TOKEN_DEFAULTS.menuFallbackHeight);\n }\n\n private get closeAnimationMs(): number {\n return resolveMotionTimeMs(TOKEN_DEFAULTS.motionShort2, TOKEN_DEFAULTS.animationDurationShort3);\n }\n\n private finishClose() {\n const popup = this.el.querySelector<HTMLElement>('.menu-popup');\n if (popup?.matches(':popover-open')) popup.hidePopover();\n this.shouldRender = false;\n this.closing = false;\n this.lastRenderedSections = [];\n this.closingSections = null;\n this.dsAfterClose.emit();\n }\n\n private captureClosingSections() {\n if (this.closingSections) return;\n const visibleSections = this.lastRenderedSections.length\n ? this.lastRenderedSections\n : this.activeSections;\n this.closingSections = snapshotMenuSections(visibleSections);\n }\n\n private get resolvedAnchor(): HTMLElement | null {\n if (this.anchor) return this.anchor;\n if (this.anchorId) return document.getElementById(this.anchorId);\n return null;\n }\n\n private get anchorFocusTarget(): HTMLElement | null {\n const anchor = this.resolvedAnchor;\n if (!anchor) return null;\n if (anchor.matches(MENU_FOCUSABLE_SELECTOR)) return anchor;\n return anchor.querySelector<HTMLElement>(MENU_FOCUSABLE_SELECTOR) ?? anchor;\n }\n\n private focusAnchor() {\n const anchor = this.resolvedAnchor as\n | (HTMLElement & { setFocus?: () => Promise<void> | void })\n | null;\n if (anchor?.setFocus) {\n anchor.setFocus();\n return;\n }\n this.anchorFocusTarget?.focus();\n }\n\n private moveFocusAfterTab(backwards: boolean) {\n const anchor = this.anchorFocusTarget;\n if (!anchor) return;\n if (backwards) {\n anchor.focus();\n return;\n }\n\n const candidates = Array.from(\n document.querySelectorAll<HTMLElement>(MENU_FOCUSABLE_SELECTOR)\n ).filter(\n element =>\n !element.closest('.menu-popup') &&\n !element.closest('[inert]') &&\n element.getClientRects().length > 0\n );\n const index = candidates.indexOf(anchor);\n (candidates[index + 1] ?? anchor).focus();\n }\n\n private compositeTabLeavesPopup(event: KeyboardEvent): boolean {\n const popup = this.el.querySelector<HTMLElement>('.menu-popup');\n if (!popup) return true;\n const focusables = Array.from(\n popup.querySelectorAll<HTMLElement>(MENU_FOCUSABLE_SELECTOR)\n ).filter(element => element.getClientRects().length > 0);\n const currentIndex = focusables.indexOf(event.target as HTMLElement);\n if (currentIndex < 0) return true;\n return event.shiftKey ? currentIndex === 0 : currentIndex === focusables.length - 1;\n }\n\n private get activeSections(): MenuSection[] {\n if (this.sections.length > 0) return this.sections;\n if (this.items.length > 0) return [{ items: this.items }];\n return [];\n }\n\n private get flatItems(): MenuItemData[] {\n return this.activeSections.flatMap(section =>\n isMenuPickerSection(section) ? [] : section.items\n );\n }\n\n /** Rich preference content is a non-modal dialog, not an ARIA action menu. */\n private get hasCompositeSections(): boolean {\n return this.activeSections.some(isMenuPickerSection);\n }\n\n private cancelPositionRetry() {\n if (this.positionRetryRaf !== null) {\n cancelAnimationFrame(this.positionRetryRaf);\n this.positionRetryRaf = null;\n }\n }\n\n private cancelLivePositionUpdate() {\n if (this.livePositionRaf !== null) {\n cancelAnimationFrame(this.livePositionRaf);\n this.livePositionRaf = null;\n }\n }\n\n /** Reposition after the current scroll/layout frame without hiding the open popup. */\n private scheduleLivePositionUpdate() {\n if (this.livePositionRaf !== null) return;\n this.livePositionRaf = requestAnimationFrame(() => {\n if (this.shouldRender && !this.closing) this.calculatePosition();\n // Scroll-driven owners such as ShellPage may commit their compact header\n // render in this frame. Re-read once after that render, then stop tracking.\n this.livePositionRaf = requestAnimationFrame(() => {\n this.livePositionRaf = null;\n if (this.shouldRender && !this.closing) this.calculatePosition();\n });\n });\n }\n\n /** Retry until anchor + popup exist — do not reveal at 0,0 on a failed first pass. */\n private schedulePositionUpdate(onReady?: () => void) {\n if (!this.open) return;\n\n this.cancelPositionRetry();\n this.positionReady = false;\n\n let remaining = POSITION_RETRY_BUDGET;\n\n const attempt = () => {\n this.positionRetryRaf = null;\n if (!this.open) return;\n\n if (this.calculatePosition()) {\n this.positionReady = true;\n onReady?.();\n return;\n }\n\n if (remaining > 0) {\n remaining -= 1;\n this.positionRetryRaf = requestAnimationFrame(attempt);\n }\n };\n\n this.positionRetryRaf = requestAnimationFrame(attempt);\n }\n\n /** @returns `true` when anchor and popup were found and `pos` was updated. */\n private calculatePosition(): boolean {\n const anchorEl = this.resolvedAnchor;\n if (!anchorEl) return false;\n const popup = this.el.querySelector('.menu-popup') as HTMLElement | null;\n if (!popup) return false;\n\n const anchorRect = anchorEl.getBoundingClientRect();\n const sectionInsetPx = this.viewportPadPx;\n if (!this.minWidth) {\n if (\n this.anchorAlignment === 'choice-cell' &&\n (this.side === 'top' || this.side === 'bottom')\n ) {\n popup.style.minWidth = `max(var(--dimension-menu-width-xs), ${choicePopupMinWidth(\n anchorRect.width,\n sectionInsetPx\n )}px)`;\n } else {\n popup.style.removeProperty('min-width');\n }\n }\n\n const next = computeMenuPosition({\n anchorRect,\n popupWidth: popup.offsetWidth || this.popupFallbackWidthPx,\n popupHeight: popup.offsetHeight || this.popupFallbackHeightPx,\n side: this.side,\n align: this.align,\n sideOffsetPx: this.sideOffsetPx,\n alignOffsetPx: resolveChoicePopupAlignOffset({\n align: this.align,\n alignOffsetPx: this.alignOffsetPx,\n sectionInsetPx,\n anchorAlignment: this.anchorAlignment,\n }),\n viewportPadPx: this.viewportPadPx,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n });\n if (next.x !== this.pos.x || next.y !== this.pos.y) this.pos = next;\n return true;\n }\n\n /** Focus the selected item when present, otherwise the first enabled item. */\n private focusInitialItem() {\n if (this.hasCompositeSections) {\n requestAnimationFrame(() => {\n this.el.querySelector<HTMLElement>('.menu-popup [tabindex=\"0\"]')?.focus();\n });\n return;\n }\n\n const flat = this.flatItems;\n const selectedIdx = flat.findIndex(it => it.isSelected && !it.isInactive);\n const firstEnabledIdx = flat.findIndex(it => !it.isInactive);\n this.focusedIndex = selectedIdx >= 0 ? selectedIdx : firstEnabledIdx >= 0 ? firstEnabledIdx : 0;\n\n requestAnimationFrame(() => {\n const btns = this.el.querySelectorAll<HTMLElement>('.menu-item');\n btns[this.focusedIndex]?.focus();\n });\n }\n\n private setupListeners() {\n this.clickOutsideHandler = (e: MouseEvent) => {\n const t = e.target as Node;\n const popup = this.el.querySelector('.menu-popup');\n const anchorEl = this.resolvedAnchor;\n if (popup?.contains(t) || anchorEl?.contains(t)) return;\n this.close();\n };\n\n this.scrollResizeHandler = () => this.scheduleLivePositionUpdate();\n\n document.addEventListener('mousedown', this.clickOutsideHandler, true);\n window.addEventListener('scroll', this.scrollResizeHandler, true);\n window.addEventListener('resize', this.scrollResizeHandler);\n }\n\n private teardownListeners() {\n this.cancelLivePositionUpdate();\n if (this.clickOutsideHandler) {\n document.removeEventListener('mousedown', this.clickOutsideHandler, true);\n this.clickOutsideHandler = null;\n }\n if (this.scrollResizeHandler) {\n window.removeEventListener('scroll', this.scrollResizeHandler, true);\n window.removeEventListener('resize', this.scrollResizeHandler);\n this.scrollResizeHandler = null;\n }\n if (this.closeTimer) {\n clearTimeout(this.closeTimer);\n this.closeTimer = null;\n }\n }\n\n private close(restoreFocus = true) {\n this.captureClosingSections();\n if (restoreFocus) this.focusAnchor();\n this.dsClose.emit();\n this.open = false;\n this.onOpenChange(false);\n }\n\n @Listen('keydown')\n handleKeyDown(e: KeyboardEvent) {\n if (!this.shouldRender || this.closing) return;\n\n if (e.key === 'Escape') {\n e.preventDefault();\n this.close();\n return;\n }\n\n if (e.key === 'Tab') {\n if (this.hasCompositeSections && !this.compositeTabLeavesPopup(e)) return;\n e.preventDefault();\n this.close(false);\n this.moveFocusAfterTab(e.shiftKey);\n return;\n }\n\n if (this.hasCompositeSections) return;\n\n const flat = this.flatItems;\n const enabled = flat\n .map((it, i) => ({ it, i }))\n .filter(({ it }) => !it.isInactive)\n .map(({ i }) => i);\n if (!enabled.length) return;\n\n const cur = enabled.indexOf(this.focusedIndex);\n const safe = cur < 0 ? 0 : cur;\n\n switch (e.key) {\n case 'ArrowDown':\n e.preventDefault();\n e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[(safe + 1) % enabled.length];\n this.focusItem(this.focusedIndex);\n break;\n case 'ArrowUp':\n e.preventDefault();\n e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[(safe - 1 + enabled.length) % enabled.length];\n this.focusItem(this.focusedIndex);\n break;\n case 'Home':\n e.preventDefault();\n e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[0];\n this.focusItem(this.focusedIndex);\n break;\n case 'End':\n e.preventDefault();\n e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[enabled.length - 1];\n this.focusItem(this.focusedIndex);\n break;\n }\n }\n\n private focusItem(idx: number) {\n const btns = this.el.querySelectorAll<HTMLElement>('.menu-item');\n btns[idx]?.focus();\n }\n\n private handleItemClick(item: MenuItemData) {\n if (item.isInactive) return;\n // Consumers commonly change the item source while handling selection.\n // Capture first so the exit animation never paints the next menu context.\n this.captureClosingSections();\n this.dsSelect.emit(item);\n }\n\n private handleGradientSelect(preset: ShellGradientPreset) {\n this.dsGradientSelect.emit(preset);\n }\n\n private handleSwatchSelect(value: string) {\n this.dsSwatchSelect.emit(value);\n }\n\n render() {\n if (!this.shouldRender) return <Host style={{ display: 'contents' }} />;\n\n const sections = this.closing\n ? (this.closingSections ?? this.lastRenderedSections)\n : this.activeSections;\n const hasCompositeSections = sections.some(isMenuPickerSection);\n if (!this.closing) {\n this.lastRenderedSections = snapshotMenuSections(sections);\n this.closingSections = null;\n }\n let flatIdx = 0;\n\n const popupStyle: Record<string, string> = {\n position: 'fixed',\n left: '0',\n top: '0',\n transform: `translate(${Math.round(this.pos.x)}px, ${Math.round(this.pos.y)}px)`,\n zIndex: '9998',\n visibility: this.positionReady ? 'visible' : 'hidden',\n };\n\n if (this.menuWidth) popupStyle['width'] = this.menuWidth;\n if (this.minWidth) popupStyle['min-width'] = this.minWidth;\n\n return (\n <Host style={{ display: 'contents' }}>\n <div\n popover=\"manual\"\n class={{\n 'menu-popup': true,\n 'menu-popup--closing': this.closing,\n 'ds-choice-popup': true,\n 'ds-choice-popup--closing': this.closing,\n }}\n style={popupStyle}\n role={hasCompositeSections ? 'dialog' : 'menu'}\n aria-label={this.menuLabel}\n aria-orientation={hasCompositeSections ? undefined : 'vertical'}\n >\n <div class=\"ds-choice-list\">\n {sections.map((section, si) => (\n <div\n key={si}\n class={{\n 'menu-section': true,\n 'menu-section--divided': si < sections.length - 1,\n 'menu-section--gradient-picker': isMenuPickerSection(section),\n 'ds-choice-section': true,\n 'ds-choice-section--divided': si < sections.length - 1,\n }}\n role={section.header ? 'group' : undefined}\n aria-label={section.header}\n >\n {section.header && (\n <ds-text\n class=\"section-header ds-choice-section__header ds-control--md\"\n as=\"span\"\n variant=\"text-body-small\"\n emphasis\n color=\"primary\"\n aria-hidden=\"true\"\n >\n {section.header}\n </ds-text>\n )}\n {isMenuGradientPickerSection(section) ? (\n <ds-swatch-picker\n value={section.value}\n groupLabel={section.header ?? 'Shell gradient theme'}\n sections={shellGradientPickerSections()}\n onDsChange={(e: CustomEvent<string>) => {\n e.stopPropagation();\n this.handleGradientSelect(e.detail as ShellGradientPreset);\n }}\n />\n ) : isMenuSwatchPickerSection(section) ? (\n <ds-swatch-picker\n value={section.value}\n groupLabel={section.groupLabel ?? section.header ?? 'Swatch options'}\n options={section.options ?? []}\n sections={section.sections ?? []}\n onDsChange={(e: CustomEvent<string>) => {\n e.stopPropagation();\n this.handleSwatchSelect(e.detail);\n }}\n />\n ) : (\n section.items.map(item => {\n const idx = flatIdx++;\n const isFocused = this.focusedIndex === idx;\n const isSingleSelectionItem =\n !hasCompositeSections &&\n this.selectionMode === 'single' &&\n !item.showSwitch;\n return (\n <button\n key={idx}\n type=\"button\"\n class={{\n 'menu-item': true,\n 'ds-choice-item': true,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-focus-ring--visible': isFocused && this.focusRingVisible,\n 'ds-interaction-fill': !item.isInactive,\n 'ds-interaction-fill--selected': !!item.isSelected && !item.isInactive,\n 'menu-item--selected': !!item.isSelected,\n 'ds-control-inactive': !!item.isInactive,\n 'menu-item--destructive': !!item.isDestructive,\n 'menu-item--focused': isFocused,\n }}\n role={\n hasCompositeSections\n ? undefined\n : item.showSwitch\n ? 'menuitemcheckbox'\n : 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 disabled={item.isInactive}\n tabIndex={hasCompositeSections ? 0 : isFocused ? 0 : -1}\n onMouseDown={() => {\n this.focusRingVisible = false;\n }}\n onClick={() => this.handleItemClick(item)}\n onFocus={() => {\n this.focusedIndex = idx;\n }}\n >\n <div class=\"menu-item__content ds-choice-item__content ds-interaction-fill__content\">\n <ds-text\n class=\"menu-item__label ds-choice-item__label\"\n as=\"span\"\n variant=\"text-body-medium\"\n color={item.isSelected ? 'primary' : 'secondary'}\n >\n {item.label}\n </ds-text>\n {item.subtext && (\n <ds-text\n class=\"menu-item__subtext ds-choice-item__subtext\"\n as=\"span\"\n variant=\"text-body-small\"\n color=\"secondary\"\n >\n {item.subtext}\n </ds-text>\n )}\n </div>\n {item.dot && (\n <span\n class=\"menu-item__dot-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-badge\n class=\"menu-item__dot\"\n variant=\"dot\"\n hasRing={false}\n label=\"\"\n />\n </span>\n )}\n {item.showSwitch && (\n <ds-switch\n class=\"menu-item__switch ds-interaction-fill__content\"\n size=\"md\"\n checked={!!item.switchValue}\n presentation\n />\n )}\n </button>\n );\n })\n )}\n </div>\n ))}\n </div>\n </div>\n </Host>\n );\n }\n}\n",
497
+ "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 type { MenuAlign, MenuSide } from './menu-position';\nimport type { MenuItemData, MenuSection } from './menu-types';\n\nexport type MenuSelectionMode = 'none' | 'single';\nexport type MenuSize = ControlSize;\nimport {\n isMenuGradientPickerSection,\n isMenuPickerSection,\n isMenuSwatchPickerSection,\n} from './menu-types';\nimport {\n shellGradientPickerSections,\n type ShellGradientPreset,\n} from '../../shell/shell-gradient-presets';\n\nconst MENU_FOCUSABLE_SELECTOR = [\n 'a[href]',\n 'button:not([disabled])',\n 'input:not([disabled]):not([type=\"hidden\"])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(',');\n\nfunction snapshotMenuSections(sections: MenuSection[]): MenuSection[] {\n return sections.map(section => {\n if (isMenuGradientPickerSection(section)) return { ...section };\n if (isMenuSwatchPickerSection(section)) {\n return {\n ...section,\n options: section.options?.map(option => ({ ...option })),\n sections: section.sections?.map(swatchSection => ({\n ...swatchSection,\n options: swatchSection.options.map(option => ({ ...option })),\n })),\n };\n }\n return { ...section, items: section.items.map(item => ({ ...item })) };\n });\n}\n\n@Component({\n tag: 'ds-menu',\n styleUrl: 'Menu.css',\n scoped: true,\n})\nexport class Menu {\n @Element() el!: HTMLElement;\n\n @Prop({ mutable: true }) open: boolean = false;\n @Prop() items: MenuItemData[] = [];\n /** 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 /** Show a visible ring on the initially focused menu item. Use only when the opener was keyboard-driven. */\n @Prop() initialFocusVisible: boolean = false;\n /** Accessible name for the popup menu. */\n @Prop() menuLabel: string = 'Menu';\n\n @State() private shouldRender: boolean = false;\n @State() private closing: boolean = false;\n @State() private pos: { x: number; y: number } = { x: 0, y: 0 };\n @State() private focusedIndex: number = 0;\n @State() private positionReady: boolean = false;\n @State() private focusRingVisible: boolean = false;\n\n @Event() dsClose!: EventEmitter<void>;\n /** Emitted after the popup's exit motion is complete and its rendered content is removed. */\n @Event() dsAfterClose!: EventEmitter<void>;\n @Event() dsSelect!: EventEmitter<MenuItemData>;\n /** Emitted when a `gradient-picker` section swatch is chosen. */\n @Event() dsGradientSelect!: EventEmitter<ShellGradientPreset>;\n /** Emitted when a generic `swatch-picker` section option is chosen. */\n @Event() dsSwatchSelect!: EventEmitter<string>;\n\n private clickOutsideHandler: ((e: MouseEvent) => void) | null = null;\n private closeTimer: ReturnType<typeof setTimeout> | 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 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 return {\n anchorRect,\n popupWidth: popup.offsetWidth || this.popupFallbackWidthPx,\n popupHeight: popup.offsetHeight || this.popupFallbackHeightPx,\n side: this.side,\n align: this.align,\n sideOffsetPx: this.sideOffsetPx,\n alignOffsetPx: resolveChoicePopupAlignOffset({\n align: this.align,\n alignOffsetPx: this.alignOffsetPx,\n sectionInsetPx,\n anchorAlignment: this.anchorAlignment,\n }),\n viewportPadPx: this.viewportPadPx,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n };\n },\n apply: ({ x, y }) => {\n this.pos = { x, y };\n },\n onReady: () => {\n this.positionReady = true;\n },\n liveUpdate: 'double-frame',\n });\n /** Last content actually painted while open; retained unchanged through exit motion. */\n private lastRenderedSections: MenuSection[] = [];\n private closingSections: MenuSection[] | null = null;\n\n componentDidLoad() {\n if (this.open) this.onOpenChange(true);\n }\n\n componentDidRender() {\n if (!this.shouldRender) return;\n const popup = this.el.querySelector<HTMLElement>('.menu-popup');\n if (!popup || typeof popup.showPopover !== 'function') return;\n if (!popup.matches(':popover-open')) popup.showPopover();\n }\n\n disconnectedCallback() {\n this.cancelPositionRetry();\n this.cancelLivePositionUpdate();\n this.teardownListeners();\n }\n\n @Watch('open')\n onOpenChange(isOpen: boolean) {\n if (isOpen) {\n this.teardownListeners();\n this.closingSections = null;\n this.shouldRender = true;\n this.closing = false;\n this.positionReady = false;\n this.focusRingVisible = this.initialFocusVisible;\n this.setupListeners();\n this.schedulePositionUpdate(() => {\n this.focusInitialItem();\n });\n } else if (this.shouldRender) {\n this.cancelPositionRetry();\n this.captureClosingSections();\n this.closing = true;\n this.teardownListeners();\n const closeAnimationMs = this.closeAnimationMs;\n if (closeAnimationMs <= 0) {\n this.finishClose();\n return;\n }\n this.closeTimer = setTimeout(() => {\n this.closeTimer = null;\n this.finishClose();\n }, closeAnimationMs);\n }\n }\n\n @Watch('anchor')\n @Watch('anchorId')\n onAnchorChange() {\n if (this.open) this.schedulePositionUpdate();\n }\n\n @Watch('side')\n @Watch('align')\n @Watch('anchorAlignment')\n @Watch('sideOffset')\n @Watch('alignOffset')\n onPositionPropsChange() {\n if (this.open) this.schedulePositionUpdate();\n }\n\n private get viewportPadPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n }\n\n private get sideOffsetPx(): number {\n return resolveCssLengthPx(this.sideOffset, TOKEN_DEFAULTS.space050);\n }\n\n private get alignOffsetPx(): number {\n return resolveCssLengthPx(this.alignOffset, 0);\n }\n\n private get popupFallbackWidthPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuWidthXs, TOKEN_DEFAULTS.menuWidthXs);\n }\n\n private get popupFallbackHeightPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuFallbackHeight, TOKEN_DEFAULTS.menuFallbackHeight);\n }\n\n private get closeAnimationMs(): number {\n return resolveMotionTimeMs(TOKEN_DEFAULTS.motionShort2, TOKEN_DEFAULTS.animationDurationShort3);\n }\n\n private finishClose() {\n const popup = this.el.querySelector<HTMLElement>('.menu-popup');\n if (popup?.matches(':popover-open')) popup.hidePopover();\n this.shouldRender = false;\n this.closing = false;\n this.lastRenderedSections = [];\n this.closingSections = null;\n this.dsAfterClose.emit();\n }\n\n private captureClosingSections() {\n if (this.closingSections) return;\n const visibleSections = this.lastRenderedSections.length\n ? this.lastRenderedSections\n : this.activeSections;\n this.closingSections = snapshotMenuSections(visibleSections);\n }\n\n private get resolvedAnchor(): HTMLElement | null {\n if (this.anchor) return this.anchor;\n if (this.anchorId) return document.getElementById(this.anchorId);\n return null;\n }\n\n private get anchorFocusTarget(): HTMLElement | null {\n const anchor = this.resolvedAnchor;\n if (!anchor) return null;\n if (anchor.matches(MENU_FOCUSABLE_SELECTOR)) return anchor;\n return anchor.querySelector<HTMLElement>(MENU_FOCUSABLE_SELECTOR) ?? anchor;\n }\n\n private focusAnchor() {\n const anchor = this.resolvedAnchor as\n | (HTMLElement & { setFocus?: () => Promise<void> | void })\n | null;\n if (anchor?.setFocus) {\n anchor.setFocus();\n return;\n }\n this.anchorFocusTarget?.focus();\n }\n\n private moveFocusAfterTab(backwards: boolean) {\n const anchor = this.anchorFocusTarget;\n if (!anchor) return;\n if (backwards) {\n anchor.focus();\n return;\n }\n\n const candidates = Array.from(\n document.querySelectorAll<HTMLElement>(MENU_FOCUSABLE_SELECTOR)\n ).filter(\n element =>\n !element.closest('.menu-popup') &&\n !element.closest('[inert]') &&\n element.getClientRects().length > 0\n );\n const index = candidates.indexOf(anchor);\n (candidates[index + 1] ?? anchor).focus();\n }\n\n private compositeTabLeavesPopup(event: KeyboardEvent): boolean {\n const popup = this.el.querySelector<HTMLElement>('.menu-popup');\n if (!popup) return true;\n const focusables = Array.from(\n popup.querySelectorAll<HTMLElement>(MENU_FOCUSABLE_SELECTOR)\n ).filter(element => element.getClientRects().length > 0);\n const currentIndex = focusables.indexOf(event.target as HTMLElement);\n if (currentIndex < 0) return true;\n return event.shiftKey ? currentIndex === 0 : currentIndex === focusables.length - 1;\n }\n\n private get activeSections(): MenuSection[] {\n if (this.sections.length > 0) return this.sections;\n if (this.items.length > 0) return [{ items: this.items }];\n return [];\n }\n\n private get flatItems(): MenuItemData[] {\n return this.activeSections.flatMap(section =>\n isMenuPickerSection(section) ? [] : section.items\n );\n }\n\n /** Rich preference content is a non-modal dialog, not an ARIA action menu. */\n private get hasCompositeSections(): boolean {\n return this.activeSections.some(isMenuPickerSection);\n }\n\n private cancelPositionRetry() {\n 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) return;\n this.positionReady = false;\n this.position.schedule(onReady);\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) {\n requestAnimationFrame(() => {\n this.el.querySelector<HTMLElement>('.menu-popup [tabindex=\"0\"]')?.focus();\n });\n return;\n }\n\n const flat = this.flatItems;\n const selectedIdx = flat.findIndex(it => it.isSelected && !it.isInactive);\n const firstEnabledIdx = flat.findIndex(it => !it.isInactive);\n this.focusedIndex = selectedIdx >= 0 ? selectedIdx : firstEnabledIdx >= 0 ? firstEnabledIdx : 0;\n\n requestAnimationFrame(() => {\n const btns = this.el.querySelectorAll<HTMLElement>('.menu-item');\n btns[this.focusedIndex]?.focus();\n });\n }\n\n private setupListeners() {\n this.clickOutsideHandler = (e: MouseEvent) => {\n const t = e.target as Node;\n const popup = this.el.querySelector('.menu-popup');\n const anchorEl = this.resolvedAnchor;\n if (popup?.contains(t) || anchorEl?.contains(t)) return;\n this.close();\n };\n\n document.addEventListener('mousedown', this.clickOutsideHandler, true);\n this.position.observe();\n }\n\n private teardownListeners() {\n this.position.unobserve();\n if (this.clickOutsideHandler) {\n document.removeEventListener('mousedown', this.clickOutsideHandler, true);\n this.clickOutsideHandler = null;\n }\n if (this.closeTimer) {\n clearTimeout(this.closeTimer);\n this.closeTimer = null;\n }\n }\n\n private close(restoreFocus = true) {\n this.captureClosingSections();\n if (restoreFocus) this.focusAnchor();\n this.dsClose.emit();\n this.open = false;\n this.onOpenChange(false);\n }\n\n @Listen('keydown')\n handleKeyDown(e: KeyboardEvent) {\n if (!this.shouldRender || this.closing) return;\n\n if (e.key === 'Escape') {\n e.preventDefault();\n this.close();\n return;\n }\n\n if (e.key === 'Tab') {\n if (this.hasCompositeSections && !this.compositeTabLeavesPopup(e)) return;\n e.preventDefault();\n this.close(false);\n this.moveFocusAfterTab(e.shiftKey);\n return;\n }\n\n if (this.hasCompositeSections) return;\n\n const flat = this.flatItems;\n const enabled = flat\n .map((it, i) => ({ it, i }))\n .filter(({ it }) => !it.isInactive)\n .map(({ i }) => i);\n if (!enabled.length) return;\n\n const cur = enabled.indexOf(this.focusedIndex);\n const safe = cur < 0 ? 0 : cur;\n\n switch (e.key) {\n case 'ArrowDown':\n e.preventDefault();\n e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[(safe + 1) % enabled.length];\n this.focusItem(this.focusedIndex);\n break;\n case 'ArrowUp':\n e.preventDefault();\n e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[(safe - 1 + enabled.length) % enabled.length];\n this.focusItem(this.focusedIndex);\n break;\n case 'Home':\n e.preventDefault();\n e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[0];\n this.focusItem(this.focusedIndex);\n break;\n case 'End':\n e.preventDefault();\n e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[enabled.length - 1];\n this.focusItem(this.focusedIndex);\n break;\n }\n }\n\n private focusItem(idx: number) {\n const btns = this.el.querySelectorAll<HTMLElement>('.menu-item');\n btns[idx]?.focus();\n }\n\n private handleItemClick(item: MenuItemData) {\n if (item.isInactive) return;\n // Consumers commonly change the item source while handling selection.\n // Capture first so the exit animation never paints the next menu context.\n this.captureClosingSections();\n this.dsSelect.emit(item);\n }\n\n private handleGradientSelect(preset: ShellGradientPreset) {\n this.dsGradientSelect.emit(preset);\n }\n\n private handleSwatchSelect(value: string) {\n this.dsSwatchSelect.emit(value);\n }\n\n render() {\n if (!this.shouldRender) return <Host style={{ display: 'contents' }} />;\n\n const sections = this.closing\n ? (this.closingSections ?? this.lastRenderedSections)\n : this.activeSections;\n const hasCompositeSections = sections.some(isMenuPickerSection);\n if (!this.closing) {\n this.lastRenderedSections = snapshotMenuSections(sections);\n this.closingSections = null;\n }\n let flatIdx = 0;\n\n const popupStyle: Record<string, string> = {\n position: 'fixed',\n left: '0',\n top: '0',\n transform: `translate(${Math.round(this.pos.x)}px, ${Math.round(this.pos.y)}px)`,\n zIndex: '9998',\n visibility: this.positionReady ? 'visible' : 'hidden',\n };\n\n if (this.menuWidth) popupStyle['width'] = this.menuWidth;\n if (this.minWidth) popupStyle['min-width'] = this.minWidth;\n\n return (\n <Host style={{ display: 'contents' }}>\n <div\n popover=\"manual\"\n class={{\n 'menu-popup': true,\n 'menu-popup--closing': this.closing,\n 'ds-choice-popup': true,\n 'ds-choice-popup--closing': this.closing,\n }}\n style={popupStyle}\n role={hasCompositeSections ? 'dialog' : 'menu'}\n aria-label={this.menuLabel}\n aria-orientation={hasCompositeSections ? undefined : 'vertical'}\n >\n <div class=\"ds-choice-list\">\n {sections.map((section, si) => (\n <div\n key={si}\n class={{\n 'menu-section': true,\n 'menu-section--divided': si < sections.length - 1,\n 'menu-section--gradient-picker': isMenuPickerSection(section),\n 'ds-choice-section': true,\n 'ds-chrome-column': true,\n 'ds-chrome-space--sm': true,\n 'ds-choice-section--divided': si < sections.length - 1,\n }}\n role={section.header ? 'group' : undefined}\n aria-label={section.header}\n >\n {section.header && (\n <ds-text\n class={`section-header ds-choice-section__header ds-control--${this.size}`}\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n emphasis\n color=\"primary\"\n aria-hidden=\"true\"\n >\n {section.header}\n </ds-text>\n )}\n {isMenuGradientPickerSection(section) ? (\n <ds-swatch-picker\n value={section.value}\n groupLabel={section.header ?? 'Shell gradient theme'}\n sections={shellGradientPickerSections()}\n onDsChange={(e: CustomEvent<string>) => {\n e.stopPropagation();\n this.handleGradientSelect(e.detail as ShellGradientPreset);\n }}\n />\n ) : isMenuSwatchPickerSection(section) ? (\n <ds-swatch-picker\n value={section.value}\n groupLabel={section.groupLabel ?? section.header ?? 'Swatch options'}\n options={section.options ?? []}\n sections={section.sections ?? []}\n onDsChange={(e: CustomEvent<string>) => {\n e.stopPropagation();\n this.handleSwatchSelect(e.detail);\n }}\n />\n ) : (\n section.items.map(item => {\n const idx = flatIdx++;\n const isFocused = this.focusedIndex === idx;\n const isSingleSelectionItem =\n !hasCompositeSections &&\n this.selectionMode === 'single' &&\n !item.showSwitch;\n return (\n <button\n key={idx}\n type=\"button\"\n class={{\n 'menu-item': true,\n 'ds-choice-item': true,\n 'ds-control-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': !item.isInactive,\n 'ds-interaction-fill--selected': !!item.isSelected && !item.isInactive,\n 'menu-item--selected': !!item.isSelected,\n 'ds-control-inactive': !!item.isInactive,\n 'menu-item--destructive': !!item.isDestructive,\n 'menu-item--focused': isFocused,\n }}\n role={\n hasCompositeSections\n ? undefined\n : item.showSwitch\n ? 'menuitemcheckbox'\n : 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 disabled={item.isInactive}\n tabIndex={hasCompositeSections ? 0 : isFocused ? 0 : -1}\n onMouseDown={() => {\n this.focusRingVisible = false;\n }}\n onClick={() => this.handleItemClick(item)}\n onFocus={() => {\n this.focusedIndex = idx;\n }}\n >\n <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.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 presentation\n />\n )}\n </button>\n );\n })\n )}\n </div>\n ))}\n </div>\n </div>\n </Host>\n );\n }\n}\n",
479
498
  "type": "registry:ui"
480
499
  },
481
500
  {
@@ -485,7 +504,7 @@
485
504
  },
486
505
  {
487
506
  "path": "src/wc/components/Menu/menu-position.ts",
488
- "content": "import {\n computeAnchoredPopupPosition,\n type AnchoredPopupAlign,\n type AnchoredPopupPositionInput,\n type AnchoredPopupSide,\n} from '../../utils/anchored-popup';\n\nexport type MenuSide = AnchoredPopupSide;\nexport type MenuAlign = AnchoredPopupAlign;\nexport type MenuPositionInput = AnchoredPopupPositionInput;\n\n/** Backward-compatible menu alias for the shared anchored-popup layout math. */\nexport const computeMenuPosition = computeAnchoredPopupPosition;\n",
507
+ "content": "import {\n computeAnchoredPosition,\n type AnchoredAlign,\n type AnchoredPositionInput,\n type AnchoredSide,\n} from '../../utils/anchored-position';\n\nexport type MenuSide = AnchoredSide;\nexport type MenuAlign = AnchoredAlign;\nexport type MenuPositionInput = AnchoredPositionInput;\n\n/** Backward-compatible menu alias for the shared anchored-position layout math. */\nexport const computeMenuPosition = computeAnchoredPosition;\n",
489
508
  "type": "registry:ui"
490
509
  },
491
510
  {
@@ -97,7 +97,7 @@
97
97
  },
98
98
  "peerDependencies": {
99
99
  "required": [
100
- "@ds-mo/tokens ^3.1.1",
100
+ "@ds-mo/tokens ^5.0.0",
101
101
  "@ds-mo/icons ^6.4.0"
102
102
  ],
103
103
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -238,7 +238,7 @@
238
238
  },
239
239
  "peerDependencies": {
240
240
  "required": [
241
- "@ds-mo/tokens ^3.1.1",
241
+ "@ds-mo/tokens ^5.0.0",
242
242
  "@ds-mo/icons ^6.4.0"
243
243
  ],
244
244
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."