@ds-mo/ui 13.14.0 → 13.15.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 (300) hide show
  1. package/dist/.package-ready.json +1 -1
  2. package/dist/agent-patterns.json +71 -21
  3. package/dist/agent.json +242 -58
  4. package/dist/angular/ds-agent-questionnaire.d.ts +19 -0
  5. package/dist/angular/ds-agent-questionnaire.js +46 -0
  6. package/dist/angular/ds-agent-response.d.ts +1 -1
  7. package/dist/angular/ds-agent-response.js +3 -3
  8. package/dist/angular/ds-agent-source-list.d.ts +9 -2
  9. package/dist/angular/ds-agent-source-list.js +7 -3
  10. package/dist/angular/ds-agent-tool-call.d.ts +9 -2
  11. package/dist/angular/ds-agent-tool-call.js +9 -5
  12. package/dist/angular/ds-checkbox.d.ts +1 -1
  13. package/dist/angular/ds-checkbox.js +3 -3
  14. package/dist/angular/ds-message-composer.d.ts +1 -1
  15. package/dist/angular/ds-message-composer.js +3 -3
  16. package/dist/angular/ds-radio.js +2 -1
  17. package/dist/angular/index.d.ts +1 -1
  18. package/dist/angular/index.js +1 -0
  19. package/dist/angular/proxies.d.ts +1 -0
  20. package/dist/angular/proxies.js +1 -0
  21. package/dist/components/ds-agent-activity.js +1 -1
  22. package/dist/components/ds-agent-questionnaire.d.ts +24 -0
  23. package/dist/components/ds-agent-questionnaire.js +2 -0
  24. package/dist/components/ds-agent-questionnaire.js.map +1 -0
  25. package/dist/components/ds-agent-response.js +1 -1
  26. package/dist/components/ds-agent-response.js.map +1 -1
  27. package/dist/components/ds-agent-source-list.js +1 -1
  28. package/dist/components/ds-agent-tool-call.js +1 -1
  29. package/dist/components/ds-attachment-list.js +1 -1
  30. package/dist/components/ds-badge.js +1 -1
  31. package/dist/components/ds-banner.js +1 -1
  32. package/dist/components/ds-bar-nav.js +1 -1
  33. package/dist/components/ds-bar-title.js +1 -1
  34. package/dist/components/ds-bar-workflow.js +1 -1
  35. package/dist/components/ds-breadcrumb.js +1 -1
  36. package/dist/components/ds-button-filled.js +1 -1
  37. package/dist/components/ds-button-unfilled.js +1 -1
  38. package/dist/components/ds-card-chart.js +1 -1
  39. package/dist/components/ds-card-overview.js +1 -1
  40. package/dist/components/ds-card-overview.js.map +1 -1
  41. package/dist/components/ds-card-setting.js +1 -1
  42. package/dist/components/ds-chart-legend.js +1 -1
  43. package/dist/components/ds-chart-legend.js.map +1 -1
  44. package/dist/components/ds-chart.js +1 -1
  45. package/dist/components/ds-chart.js.map +1 -1
  46. package/dist/components/ds-checkbox.js +1 -1
  47. package/dist/components/ds-chip.js +1 -1
  48. package/dist/components/ds-chip.js.map +1 -1
  49. package/dist/components/ds-code-block.js +1 -1
  50. package/dist/components/ds-conversation-list-item.js +1 -1
  51. package/dist/components/ds-conversation-list-item.js.map +1 -1
  52. package/dist/components/ds-conversation-list-section.js +1 -1
  53. package/dist/components/ds-conversation-list.js +1 -1
  54. package/dist/components/ds-divider.js +1 -1
  55. package/dist/components/ds-empty-state.js +1 -1
  56. package/dist/components/ds-field.js +1 -1
  57. package/dist/components/ds-input.js +1 -1
  58. package/dist/components/ds-input.js.map +1 -1
  59. package/dist/components/ds-markdown.js +1 -1
  60. package/dist/components/ds-menu.js +1 -1
  61. package/dist/components/ds-message-actions.js +1 -1
  62. package/dist/components/ds-message-bubble.js +1 -1
  63. package/dist/components/ds-message-bubble.js.map +1 -1
  64. package/dist/components/ds-message-composer.js +1 -1
  65. package/dist/components/ds-message-composer.js.map +1 -1
  66. package/dist/components/ds-message-scroller.js +1 -1
  67. package/dist/components/ds-message-scroller.js.map +1 -1
  68. package/dist/components/ds-message.js +1 -1
  69. package/dist/components/ds-mobile-bar-nav.js +1 -1
  70. package/dist/components/ds-mobile-header.js +1 -1
  71. package/dist/components/ds-mobile-section-switcher.js +1 -1
  72. package/dist/components/ds-mobile-sheet-nav.js +1 -1
  73. package/dist/components/ds-modal.js +1 -1
  74. package/dist/components/ds-panel-nav.js +1 -1
  75. package/dist/components/ds-panel-sub-nav.js +1 -1
  76. package/dist/components/ds-panel-sub-nav.js.map +1 -1
  77. package/dist/components/ds-panel-tool-header.js +1 -1
  78. package/dist/components/ds-panel-tool-search.js +1 -1
  79. package/dist/components/ds-panel-tools.js +1 -1
  80. package/dist/components/ds-radio.js +1 -1
  81. package/dist/components/ds-radio.js.map +1 -1
  82. package/dist/components/ds-scroll-overlay.js +1 -1
  83. package/dist/components/ds-select.js +1 -1
  84. package/dist/components/ds-select.js.map +1 -1
  85. package/dist/components/ds-shell-app.js +1 -1
  86. package/dist/components/ds-shell-app.js.map +1 -1
  87. package/dist/components/ds-shell-page.js +1 -1
  88. package/dist/components/ds-shell-tools.js +1 -1
  89. package/dist/components/ds-skeleton.js +1 -1
  90. package/dist/components/ds-slider.js +1 -1
  91. package/dist/components/ds-slider.js.map +1 -1
  92. package/dist/components/ds-swatch-picker.js +1 -1
  93. package/dist/components/ds-switch.js +1 -1
  94. package/dist/components/ds-tab-group.js +1 -1
  95. package/dist/components/ds-table.js +1 -1
  96. package/dist/components/ds-table.js.map +1 -1
  97. package/dist/components/ds-tag.js +1 -1
  98. package/dist/components/ds-text.js +1 -1
  99. package/dist/components/ds-toast.js +1 -1
  100. package/dist/components/ds-tooltip-chart.js +1 -1
  101. package/dist/components/ds-tooltip.js +1 -1
  102. package/dist/components/ds-typing-indicator.js +1 -1
  103. package/dist/components/index.d.ts +10 -0
  104. package/dist/components/{p-PQgDzzn9.js → p-00or0yGw.js} +2 -2
  105. package/dist/components/p-B2SoinWf.js +2 -0
  106. package/dist/components/p-B2SoinWf.js.map +1 -0
  107. package/dist/components/{p-BULWzaBs.js → p-B69Re7HW.js} +2 -2
  108. package/dist/components/p-BH_lVRgp.js +2 -0
  109. package/dist/components/{p-CCYgBUUS.js.map → p-BH_lVRgp.js.map} +1 -1
  110. package/dist/components/{p-C-aCs6ID.js → p-BLaqjUWu.js} +2 -2
  111. package/dist/components/{p-B6z51yBf.js → p-BNkU9dcc.js} +2 -2
  112. package/dist/components/{p-B6linhwY.js → p-BRze4tRe.js} +2 -2
  113. package/dist/components/p-BtUgncSJ.js +2 -0
  114. package/dist/components/p-BtUgncSJ.js.map +1 -0
  115. package/dist/components/p-C891ueIx.js +2 -0
  116. package/dist/components/p-C891ueIx.js.map +1 -0
  117. package/dist/components/p-CCkptWPj.js +2 -0
  118. package/dist/components/p-CCkptWPj.js.map +1 -0
  119. package/dist/components/p-CKo3OFG1.js +2 -0
  120. package/dist/components/p-CKo3OFG1.js.map +1 -0
  121. package/dist/components/p-CXduurfd.js.map +1 -1
  122. package/dist/components/{p-DLwM1RNL.js → p-CdKz-QT2.js} +2 -2
  123. package/dist/components/p-CdKz-QT2.js.map +1 -0
  124. package/dist/components/p-CekGwihQ.js +2 -0
  125. package/dist/components/{p-BSdAFIcG.js.map → p-CekGwihQ.js.map} +1 -1
  126. package/dist/components/p-ClHpIZg0.js +2 -0
  127. package/dist/components/p-ClHpIZg0.js.map +1 -0
  128. package/dist/components/p-CnUq7E8S.js +2 -0
  129. package/dist/components/{p-DyDkQ8H9.js.map → p-CnUq7E8S.js.map} +1 -1
  130. package/dist/components/{p-C9StMCWg.js → p-Cohb8T7G.js} +2 -2
  131. package/dist/components/p-CqDJRVT8.js +2 -0
  132. package/dist/components/p-CqDJRVT8.js.map +1 -0
  133. package/dist/components/p-CsXC1cHI.js +2 -0
  134. package/dist/components/{p-DYAQVvOu.js.map → p-CsXC1cHI.js.map} +1 -1
  135. package/dist/components/p-D4j0deyd.js +2 -0
  136. package/dist/components/{p-DAM4CDXT.js.map → p-D4j0deyd.js.map} +1 -1
  137. package/dist/components/p-D5aUkdTw.js +2 -0
  138. package/dist/components/p-D5aUkdTw.js.map +1 -0
  139. package/dist/components/p-DQXzTV8N.js +2 -0
  140. package/dist/components/p-DQXzTV8N.js.map +1 -0
  141. package/dist/components/p-DStd98dM.js +2 -0
  142. package/dist/components/{p-Azjx2SsL.js.map → p-DStd98dM.js.map} +1 -1
  143. package/dist/components/p-DWNhyLp2.js +2 -0
  144. package/dist/components/{p-B1JB0Zn-.js.map → p-DWNhyLp2.js.map} +1 -1
  145. package/dist/components/p-DWjE0VNn.js +2 -0
  146. package/dist/components/p-DWjE0VNn.js.map +1 -0
  147. package/dist/components/{p-C5ecLNZA.js → p-DgIRyJoc.js} +2 -2
  148. package/dist/components/{p-BNiIyZCn.js → p-Dhx_3_tf.js} +2 -2
  149. package/dist/components/p-Dhx_3_tf.js.map +1 -0
  150. package/dist/components/{p-CE82aQZr.js → p-DyVAWGL9.js} +2 -2
  151. package/dist/components/p-GSnB-sa7.js +2 -0
  152. package/dist/components/p-GSnB-sa7.js.map +1 -0
  153. package/dist/components/p-GWUTrIlk.js +2 -0
  154. package/dist/components/p-GWUTrIlk.js.map +1 -0
  155. package/dist/components/p-IhToA4m4.js +2 -0
  156. package/dist/components/{p-cPh32CFB.js.map → p-IhToA4m4.js.map} +1 -1
  157. package/dist/components/p-NQOI5gN1.js +2 -0
  158. package/dist/components/p-NQOI5gN1.js.map +1 -0
  159. package/dist/components/p-QDcUAz9m.js +2 -0
  160. package/dist/components/{p-BPS17WvI.js.map → p-QDcUAz9m.js.map} +1 -1
  161. package/dist/components/p-Qf-os3lG.js +2 -0
  162. package/dist/components/{p-B8HG9OFh.js.map → p-Qf-os3lG.js.map} +1 -1
  163. package/dist/components/{p-DMJEOAwP.js → p-chA2WA5x.js} +2 -2
  164. package/dist/components/{p-DwKC_9u2.js → p-dEouiXJ8.js} +2 -2
  165. package/dist/components/{p-CeyoHpPn.js → p-fKwbYrhi.js} +2 -2
  166. package/dist/docs/components.json +664 -10
  167. package/dist/mcp-data/registry/agent-activity.json +1 -1
  168. package/dist/mcp-data/registry/agent-questionnaire.json +273 -0
  169. package/dist/mcp-data/registry/agent-response.json +32 -11
  170. package/dist/mcp-data/registry/agent-source-list.json +33 -13
  171. package/dist/mcp-data/registry/agent-tool-call.json +59 -18
  172. package/dist/mcp-data/registry/attachment-list.json +1 -1
  173. package/dist/mcp-data/registry/avatar.json +1 -1
  174. package/dist/mcp-data/registry/badge.json +1 -1
  175. package/dist/mcp-data/registry/banner.json +1 -1
  176. package/dist/mcp-data/registry/bar-nav.json +2 -2
  177. package/dist/mcp-data/registry/bar-title.json +2 -2
  178. package/dist/mcp-data/registry/bar-workflow.json +1 -1
  179. package/dist/mcp-data/registry/breadcrumb.json +2 -2
  180. package/dist/mcp-data/registry/button-filled.json +1 -1
  181. package/dist/mcp-data/registry/button-unfilled.json +1 -1
  182. package/dist/mcp-data/registry/card-chart.json +1 -1
  183. package/dist/mcp-data/registry/card-overview.json +1 -1
  184. package/dist/mcp-data/registry/card-setting.json +1 -1
  185. package/dist/mcp-data/registry/chart-legend.json +2 -2
  186. package/dist/mcp-data/registry/chart.json +2 -2
  187. package/dist/mcp-data/registry/checkbox.json +26 -9
  188. package/dist/mcp-data/registry/chip.json +2 -2
  189. package/dist/mcp-data/registry/code-block.json +14 -6
  190. package/dist/mcp-data/registry/conversation-list-item.json +2 -2
  191. package/dist/mcp-data/registry/conversation-list-section.json +1 -1
  192. package/dist/mcp-data/registry/conversation-list.json +1 -1
  193. package/dist/mcp-data/registry/divider.json +1 -1
  194. package/dist/mcp-data/registry/empty-state.json +1 -1
  195. package/dist/mcp-data/registry/field.json +1 -1
  196. package/dist/mcp-data/registry/icon.json +1 -1
  197. package/dist/mcp-data/registry/input.json +1 -1
  198. package/dist/mcp-data/registry/loader.json +1 -1
  199. package/dist/mcp-data/registry/markdown.json +1 -1
  200. package/dist/mcp-data/registry/menu.json +2 -2
  201. package/dist/mcp-data/registry/message-actions.json +1 -1
  202. package/dist/mcp-data/registry/message-bubble.json +2 -2
  203. package/dist/mcp-data/registry/message-composer.json +30 -8
  204. package/dist/mcp-data/registry/message-scroller.json +17 -9
  205. package/dist/mcp-data/registry/message.json +1 -1
  206. package/dist/mcp-data/registry/mobile-bar-nav.json +1 -1
  207. package/dist/mcp-data/registry/mobile-header.json +1 -1
  208. package/dist/mcp-data/registry/mobile-section-switcher.json +1 -1
  209. package/dist/mcp-data/registry/mobile-sheet-nav.json +1 -1
  210. package/dist/mcp-data/registry/modal.json +1 -1
  211. package/dist/mcp-data/registry/panel-nav.json +2 -2
  212. package/dist/mcp-data/registry/panel-sub-nav.json +1 -1
  213. package/dist/mcp-data/registry/panel-tool-header.json +1 -1
  214. package/dist/mcp-data/registry/panel-tool-search.json +1 -1
  215. package/dist/mcp-data/registry/panel-tools.json +2 -2
  216. package/dist/mcp-data/registry/radio.json +24 -11
  217. package/dist/mcp-data/registry/registry.json +245 -58
  218. package/dist/mcp-data/registry/scroll-overlay.json +1 -1
  219. package/dist/mcp-data/registry/select.json +2 -2
  220. package/dist/mcp-data/registry/shell-app.json +2 -2
  221. package/dist/mcp-data/registry/shell-page.json +1 -1
  222. package/dist/mcp-data/registry/shell-tools.json +1 -1
  223. package/dist/mcp-data/registry/skeleton.json +1 -1
  224. package/dist/mcp-data/registry/slider.json +2 -2
  225. package/dist/mcp-data/registry/swatch-picker.json +2 -2
  226. package/dist/mcp-data/registry/switch.json +2 -2
  227. package/dist/mcp-data/registry/tab-group.json +1 -1
  228. package/dist/mcp-data/registry/table.json +1 -1
  229. package/dist/mcp-data/registry/tag.json +1 -1
  230. package/dist/mcp-data/registry/text.json +2 -2
  231. package/dist/mcp-data/registry/toast.json +1 -1
  232. package/dist/mcp-data/registry/tooltip-chart.json +5 -3
  233. package/dist/mcp-data/registry/tooltip.json +1 -1
  234. package/dist/mcp-data/registry/typing-indicator.json +1 -1
  235. package/dist/react/components.d.ts +1 -0
  236. package/dist/react/components.js +1 -0
  237. package/dist/react/ds-agent-questionnaire.d.ts +13 -0
  238. package/dist/react/ds-agent-questionnaire.js +15 -0
  239. package/dist/react/ds-agent-source-list.d.ts +7 -2
  240. package/dist/react/ds-agent-source-list.js +1 -1
  241. package/dist/react/ds-agent-tool-call.d.ts +7 -2
  242. package/dist/react/ds-agent-tool-call.js +1 -1
  243. package/dist/styles/prose.css +21 -4
  244. package/dist/types/components/AgentQuestionnaire/AgentQuestionnaire.d.ts +49 -0
  245. package/dist/types/components/AgentQuestionnaire/agent-questionnaire-model.d.ts +13 -0
  246. package/dist/types/components/AgentResponse/AgentResponse.d.ts +3 -1
  247. package/dist/types/components/AgentSourceList/AgentSourceList.d.ts +5 -0
  248. package/dist/types/components/AgentToolCall/AgentToolCall.d.ts +21 -0
  249. package/dist/types/components/Checkbox/Checkbox.d.ts +3 -0
  250. package/dist/types/components/MessageComposer/MessageComposer.d.ts +3 -0
  251. package/dist/types/components/MessageScroller/MessageScroller.d.ts +21 -1
  252. package/dist/types/components/Radio/Radio.d.ts +4 -0
  253. package/dist/types/components/conversation-types.d.ts +51 -2
  254. package/dist/types/components.d.ts +147 -2
  255. package/package.json +5 -3
  256. package/dist/components/p-Azjx2SsL.js +0 -2
  257. package/dist/components/p-B1JB0Zn-.js +0 -2
  258. package/dist/components/p-B2t8alvH.js +0 -2
  259. package/dist/components/p-B2t8alvH.js.map +0 -1
  260. package/dist/components/p-B8HG9OFh.js +0 -2
  261. package/dist/components/p-BJVzrR_P.js +0 -2
  262. package/dist/components/p-BJVzrR_P.js.map +0 -1
  263. package/dist/components/p-BL0JHLJp.js +0 -2
  264. package/dist/components/p-BL0JHLJp.js.map +0 -1
  265. package/dist/components/p-BNiIyZCn.js.map +0 -1
  266. package/dist/components/p-BPS17WvI.js +0 -2
  267. package/dist/components/p-BSdAFIcG.js +0 -2
  268. package/dist/components/p-CCYgBUUS.js +0 -2
  269. package/dist/components/p-COmqMfQo.js +0 -2
  270. package/dist/components/p-COmqMfQo.js.map +0 -1
  271. package/dist/components/p-Ch5yT3UA.js +0 -2
  272. package/dist/components/p-Ch5yT3UA.js.map +0 -1
  273. package/dist/components/p-CiUciD5Z.js +0 -2
  274. package/dist/components/p-CiUciD5Z.js.map +0 -1
  275. package/dist/components/p-D6g070eY.js +0 -2
  276. package/dist/components/p-D6g070eY.js.map +0 -1
  277. package/dist/components/p-DAM4CDXT.js +0 -2
  278. package/dist/components/p-DLwM1RNL.js.map +0 -1
  279. package/dist/components/p-DYAQVvOu.js +0 -2
  280. package/dist/components/p-DmblUQ4D.js +0 -2
  281. package/dist/components/p-DmblUQ4D.js.map +0 -1
  282. package/dist/components/p-DyDkQ8H9.js +0 -2
  283. package/dist/components/p-Dz3Ufmr4.js +0 -2
  284. package/dist/components/p-Dz3Ufmr4.js.map +0 -1
  285. package/dist/components/p-cPh32CFB.js +0 -2
  286. package/dist/components/p-fkQhr5s9.js +0 -2
  287. package/dist/components/p-fkQhr5s9.js.map +0 -1
  288. package/dist/components/p-oOKTL-yx.js +0 -2
  289. package/dist/components/p-oOKTL-yx.js.map +0 -1
  290. /package/dist/components/{p-PQgDzzn9.js.map → p-00or0yGw.js.map} +0 -0
  291. /package/dist/components/{p-BULWzaBs.js.map → p-B69Re7HW.js.map} +0 -0
  292. /package/dist/components/{p-C-aCs6ID.js.map → p-BLaqjUWu.js.map} +0 -0
  293. /package/dist/components/{p-B6z51yBf.js.map → p-BNkU9dcc.js.map} +0 -0
  294. /package/dist/components/{p-B6linhwY.js.map → p-BRze4tRe.js.map} +0 -0
  295. /package/dist/components/{p-C9StMCWg.js.map → p-Cohb8T7G.js.map} +0 -0
  296. /package/dist/components/{p-C5ecLNZA.js.map → p-DgIRyJoc.js.map} +0 -0
  297. /package/dist/components/{p-CE82aQZr.js.map → p-DyVAWGL9.js.map} +0 -0
  298. /package/dist/components/{p-DMJEOAwP.js.map → p-chA2WA5x.js.map} +0 -0
  299. /package/dist/components/{p-DwKC_9u2.js.map → p-dEouiXJ8.js.map} +0 -0
  300. /package/dist/components/{p-CeyoHpPn.js.map → p-fKwbYrhi.js.map} +0 -0
@@ -245,7 +245,7 @@
245
245
  },
246
246
  "peerDependencies": {
247
247
  "required": [
248
- "@ds-mo/tokens ^6.0.0",
248
+ "@ds-mo/tokens ^6.2.1",
249
249
  "@ds-mo/icons ^6.6.0"
250
250
  ],
251
251
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -264,7 +264,7 @@
264
264
  "files": [
265
265
  {
266
266
  "path": "src/wc/components/Chip/Chip.css",
267
- "content": "@import '../../utils/tag-chip.css';\n\n/*\n * Semantic state colors — Chip’s differentiator from Tag.\n * All states use the faint contrast weight; hue comes from the state.\n */\n:host(.chip--default) {\n background-color: var(--color-background-faint-neutral);\n color: var(--color-foreground-bold-neutral);\n}\n\n:host(.chip--active) {\n background-color: var(--color-background-faint-brand);\n color: var(--color-foreground-bold-brand);\n}\n\n:host(.chip--error) {\n background-color: var(--color-background-faint-negative);\n color: var(--color-foreground-bold-negative);\n}\n\n:host(.chip--caution) {\n background-color: var(--color-background-faint-caution);\n color: var(--color-foreground-bold-caution);\n}\n\n/*\n * Label↔dismiss uses host gap, mirroring Tag icon↔label spacing:\n * [inset][text][inset][gap][icon].\n */\n.tag__remove {\n padding: 0;\n margin: 0;\n border: none;\n background: none;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: default;\n color: inherit;\n flex-shrink: 0;\n position: relative;\n z-index: 2;\n border-radius: var(--dimension-radius-half);\n}\n\n.tag__remove::before {\n content: '';\n position: absolute;\n inset: var(--dimension-space-n025);\n border-radius: var(--dimension-radius-half);\n background-color: transparent;\n transition: var(--effect-transition-interaction-background-short-2);\n pointer-events: none;\n}\n\n.tag__remove:hover::before {\n background-color: var(--color-interaction-hover);\n}\n\n.tag__remove:active::before {\n background-color: var(--color-interaction-pressed);\n}\n\n:host(.tag--size-sm) .tag__remove::before {\n inset: var(--dimension-space-n012);\n}\n\n:host(.tag--size-xs) .tag__remove::before {\n inset: 0;\n}\n\n.tag__remove-x {\n display: inline-flex;\n flex-shrink: 0;\n width: var(--ds-control-icon);\n height: var(--ds-control-icon);\n}\n",
267
+ "content": "@import '../../utils/tag-chip.css';\n\n/*\n * Semantic state colors — Chip’s differentiator from Tag.\n * All states use the faint contrast weight; hue comes from the state.\n */\n:host(.chip--default) {\n background-color: var(--color-background-faint-neutral);\n color: var(--color-foreground-bold-neutral);\n}\n\n:host(.chip--active) {\n background-color: var(--color-background-faint-brand);\n color: var(--color-foreground-bold-brand);\n}\n\n:host(.chip--error) {\n background-color: var(--color-background-faint-negative);\n color: var(--color-foreground-bold-negative);\n}\n\n:host(.chip--caution) {\n background-color: var(--color-background-faint-caution);\n color: var(--color-foreground-bold-caution);\n}\n\n/*\n * Label↔dismiss uses host gap, mirroring Tag icon↔label spacing:\n * [inset][text][inset][gap][icon].\n */\n.tag__remove {\n padding: 0;\n margin: 0;\n border: none;\n background: none;\n display: flex;\n align-items: center;\n justify-content: center;\n cursor: default;\n color: inherit;\n flex-shrink: 0;\n position: relative;\n z-index: 2;\n border-radius: var(--dimension-radius-half);\n}\n\n.tag__remove::before {\n content: '';\n position: absolute;\n inset: var(--dimension-space-n025);\n border-radius: var(--dimension-radius-half);\n background-color: transparent;\n transition: var(--effect-transition-interaction-background-short-2);\n pointer-events: none;\n}\n\n@media (hover: hover) and (pointer: fine) {\n .tag__remove:hover::before {\n background-color: var(--color-interaction-hover);\n }\n}\n\n.tag__remove:active::before {\n background-color: var(--color-interaction-pressed);\n}\n\n:host(.tag--size-sm) .tag__remove::before {\n inset: var(--dimension-space-n012);\n}\n\n:host(.tag--size-xs) .tag__remove::before {\n inset: 0;\n}\n\n.tag__remove-x {\n display: inline-flex;\n flex-shrink: 0;\n width: var(--ds-control-icon);\n height: var(--ds-control-icon);\n}\n",
268
268
  "type": "registry:ui"
269
269
  },
270
270
  {
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
3
  "name": "code-block",
4
4
  "title": "CodeBlock",
5
- "description": "Scrollable monospace code surface with optional language or filename context and copy action.",
5
+ "description": "Scrollable code surface with optional language or filename context and copy action.",
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": "experimental",
16
- "summary": "Scrollable monospace code surface with optional language or filename context and copy action.",
16
+ "summary": "Scrollable code surface with optional language or filename context and copy action.",
17
17
  "useWhen": [
18
18
  "A message or document displays fenced code that readers may copy."
19
19
  ],
@@ -26,7 +26,11 @@
26
26
  ],
27
27
  "accessibility": [
28
28
  "The copy action has matching accessible-name and visible-tooltip status wording, and the code remains selectable text.",
29
- "Language and filename labels supplement rather than replace the code content."
29
+ "Language and filename labels supplement rather than replace the code content.",
30
+ "The semantic code-family token retains a system-monospace fallback when an application does not load Fira Code; applications may disable programming ligatures through the shared code-surface custom property."
31
+ ],
32
+ "states": [
33
+ "Applications load font assets once at their root. The component consumes --typography-font-family-code and never fetches or packages a font."
30
34
  ],
31
35
  "responsiveBehavior": [
32
36
  "Long lines scroll horizontally and do not widen the owning transcript."
@@ -34,7 +38,11 @@
34
38
  "references": [
35
39
  {
36
40
  "label": "Storybook examples",
37
- "path": "src/wc/components/MessageScroller/MessageScroller.stories.ts"
41
+ "path": "src/wc/components/CodeBlock/CodeBlock.stories.ts"
42
+ },
43
+ {
44
+ "label": "Font ownership and loading",
45
+ "path": "docs/font-ownership.md"
38
46
  }
39
47
  ]
40
48
  },
@@ -120,7 +128,7 @@
120
128
  },
121
129
  "peerDependencies": {
122
130
  "required": [
123
- "@ds-mo/tokens ^6.0.0",
131
+ "@ds-mo/tokens ^6.2.1",
124
132
  "@ds-mo/icons ^6.6.0"
125
133
  ],
126
134
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -139,7 +147,7 @@
139
147
  "files": [
140
148
  {
141
149
  "path": "src/wc/components/CodeBlock/CodeBlock.css",
142
- "content": ":host {\n display: block;\n width: 100%;\n min-width: 0;\n box-sizing: border-box;\n -webkit-user-select: none;\n user-select: none;\n}\n\n.code-block {\n width: 100%;\n min-width: 0;\n margin: 0;\n overflow: hidden;\n border-radius: var(--dimension-radius-050);\n background: var(--color-background-strong-neutral);\n color: var(--color-foreground-faint-neutral);\n box-sizing: border-box;\n}\n\n.code-block__header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--dimension-space-100);\n min-height: var(--dimension-size-400);\n padding: var(--dimension-space-050) var(--dimension-space-100);\n background: var(--color-background-strong-neutral);\n box-sizing: border-box;\n}\n\npre {\n margin: 0;\n padding: var(--dimension-space-150);\n overflow: auto;\n font-family: var(--typography-fontfamily-mono);\n font-size: var(--typography-fontsize-sm);\n line-height: var(--typography-lineheight-sm);\n tab-size: 2;\n -webkit-user-select: text;\n user-select: text;\n}\n\ncode {\n font: inherit;\n white-space: pre;\n}\n",
150
+ "content": ":host {\n display: block;\n width: 100%;\n min-width: 0;\n box-sizing: border-box;\n -webkit-user-select: none;\n user-select: none;\n}\n\n.code-block {\n width: 100%;\n min-width: 0;\n margin: 0;\n overflow: hidden;\n border-radius: var(--dimension-radius-050);\n background: var(--color-background-strong-neutral);\n color: var(--color-foreground-faint-neutral);\n box-sizing: border-box;\n}\n\n.code-block__header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--dimension-space-100);\n min-height: var(--dimension-size-400);\n padding: var(--dimension-space-050) var(--dimension-space-100);\n background: var(--color-background-strong-neutral);\n box-sizing: border-box;\n}\n\npre {\n margin: 0;\n padding: var(--dimension-space-150);\n overflow: auto;\n font-family: var(\n --typography-font-family-code,\n 'Fira Code', ui-monospace, 'SFMono-Regular', 'Cascadia Code', 'Roboto Mono',\n Menlo, Monaco, Consolas, 'Liberation Mono', monospace\n );\n font-size: var(--typography-fontsize-sm);\n font-variant-ligatures: var(--ds-code-font-variant-ligatures, normal);\n font-weight: var(--typography-weight-regular);\n line-height: var(--typography-lineheight-sm);\n letter-spacing: var(--typography-letterspacing-none);\n tab-size: 2;\n -webkit-user-select: text;\n user-select: text;\n}\n\ncode {\n font: inherit;\n white-space: pre;\n}\n",
143
151
  "type": "registry:ui"
144
152
  },
145
153
  {
@@ -253,7 +253,7 @@
253
253
  },
254
254
  "peerDependencies": {
255
255
  "required": [
256
- "@ds-mo/tokens ^6.0.0",
256
+ "@ds-mo/tokens ^6.2.1",
257
257
  "@ds-mo/icons ^6.6.0"
258
258
  ],
259
259
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -272,7 +272,7 @@
272
272
  "files": [
273
273
  {
274
274
  "path": "src/wc/components/ConversationListItem/ConversationListItem.css",
275
- "content": "@import '../../utils/choice-list.css';\n@import '../../utils/visually-hidden.css';\n@import '../../styles/control-elevation.css';\n\n:host {\n display: block;\n box-sizing: border-box;\n -webkit-user-select: none;\n user-select: none;\n}\n\n:host(:hover),\n:host(:focus-within),\n:host(.conversation-list-item--actions-open) {\n position: relative;\n z-index: var(--dimension-z-index-floating);\n}\n\n.conversation-list-item__row {\n position: relative;\n isolation: isolate;\n}\n\n.conversation-list-item {\n color: var(--color-foreground-primary);\n}\n\n.conversation-list-item--error {\n --ds-interaction-border-width: var(--dimension-stroke-width-012);\n --ds-interaction-border-color: var(--color-border-bold-negative);\n}\n\n.conversation-list-item__leading:empty,\n.conversation-list-item__trailing:empty {\n display: none;\n}\n\n.conversation-list-item__leading,\n.conversation-list-item__trailing,\n.conversation-list-item__preview-row {\n display: flex;\n align-items: center;\n}\n\n.conversation-list-item__leading {\n align-self: center;\n width: auto;\n height: auto;\n margin-inline-start: var(--ds-control-label-inset, var(--dimension-space-025));\n margin-inline-end: var(--dimension-space-050);\n}\n\n.conversation-list-item__content {\n min-width: 0;\n}\n\n.conversation-list-item__title-row {\n display: flex;\n align-items: center;\n min-width: 0;\n}\n\n.conversation-list-item__preview-row {\n min-width: 0;\n gap: var(--dimension-space-050);\n}\n\nds-text.conversation-list-item__title,\nds-text.conversation-list-item__preview {\n display: block;\n min-width: 0;\n}\n\nds-text.conversation-list-item__title {\n flex: 1;\n}\n\n.conversation-list-item__unread {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--dimension-size-200);\n margin-inline-end: var(--dimension-space-025);\n}\n\nds-text.conversation-list-item__preview.ds-choice-item__subtext {\n -webkit-line-clamp: 1;\n}\n\n.conversation-list-item__actions {\n position: absolute;\n top: 50%;\n right: var(--dimension-space-100);\n z-index: var(--dimension-z-index-floating);\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--dimension-radius-half);\n background: var(--color-background-transparent);\n -webkit-backdrop-filter: blur(var(--effect-blur-sm));\n backdrop-filter: blur(var(--effect-blur-sm));\n opacity: 0;\n pointer-events: none;\n transform: translateY(-50%);\n}\n\n.conversation-list-item__row:hover .conversation-list-item__actions,\n.conversation-list-item__row:has(:focus-visible) .conversation-list-item__actions,\n:host(.conversation-list-item--actions-open) .conversation-list-item__actions {\n opacity: 1;\n pointer-events: auto;\n}\n\n/* Keep the row wash continuous while the pointer moves onto its sibling action. */\n.conversation-list-item__row:hover .conversation-list-item::after {\n background: var(--ds-interaction-hover);\n}\n\n:host(.conversation-list-item--actions-open) .conversation-list-item::after {\n background: var(--ds-interaction-hover);\n}\n\n.conversation-list-item__row:active .conversation-list-item::after {\n background: var(--ds-interaction-pressed);\n}\n\n@media (hover: none) {\n .conversation-list-item__actions {\n opacity: 1;\n pointer-events: auto;\n }\n\n :host(.conversation-list-item--has-actions) .conversation-list-item__title-row {\n padding-inline-end: var(--dimension-size-400);\n }\n}\n\n",
275
+ "content": "@import '../../utils/choice-list.css';\n@import '../../utils/visually-hidden.css';\n@import '../../styles/control-elevation.css';\n\n:host {\n display: block;\n box-sizing: border-box;\n -webkit-user-select: none;\n user-select: none;\n}\n\n:host(:focus-within),\n:host(.conversation-list-item--actions-open) {\n position: relative;\n z-index: var(--dimension-z-index-floating);\n}\n\n.conversation-list-item__row {\n position: relative;\n isolation: isolate;\n}\n\n.conversation-list-item {\n color: var(--color-foreground-primary);\n}\n\n.conversation-list-item--error {\n --ds-interaction-border-width: var(--dimension-stroke-width-012);\n --ds-interaction-border-color: var(--color-border-bold-negative);\n}\n\n.conversation-list-item__leading:empty,\n.conversation-list-item__trailing:empty {\n display: none;\n}\n\n.conversation-list-item__leading,\n.conversation-list-item__trailing,\n.conversation-list-item__preview-row {\n display: flex;\n align-items: center;\n}\n\n.conversation-list-item__leading {\n align-self: center;\n width: auto;\n height: auto;\n margin-inline-start: var(--ds-control-label-inset, var(--dimension-space-025));\n margin-inline-end: var(--dimension-space-050);\n}\n\n.conversation-list-item__content {\n min-width: 0;\n}\n\n.conversation-list-item__title-row {\n display: flex;\n align-items: center;\n min-width: 0;\n}\n\n.conversation-list-item__preview-row {\n min-width: 0;\n gap: var(--dimension-space-050);\n}\n\nds-text.conversation-list-item__title,\nds-text.conversation-list-item__preview {\n display: block;\n min-width: 0;\n}\n\nds-text.conversation-list-item__title {\n flex: 1;\n}\n\n.conversation-list-item__unread {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--dimension-size-200);\n margin-inline-end: var(--dimension-space-025);\n}\n\nds-text.conversation-list-item__preview.ds-choice-item__subtext {\n -webkit-line-clamp: 1;\n}\n\n.conversation-list-item__actions {\n position: absolute;\n top: 50%;\n right: var(--dimension-space-100);\n z-index: var(--dimension-z-index-floating);\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--dimension-radius-half);\n background: var(--color-background-transparent);\n -webkit-backdrop-filter: blur(var(--effect-blur-sm));\n backdrop-filter: blur(var(--effect-blur-sm));\n opacity: 0;\n pointer-events: none;\n transform: translateY(-50%);\n}\n\n.conversation-list-item__row:has(:focus-visible) .conversation-list-item__actions,\n:host(.conversation-list-item--actions-open) .conversation-list-item__actions {\n opacity: 1;\n pointer-events: auto;\n}\n\n:host(.conversation-list-item--actions-open) .conversation-list-item::after {\n background: var(--ds-interaction-hover);\n}\n\n.conversation-list-item__row:active .conversation-list-item::after {\n background: var(--ds-interaction-pressed);\n}\n\n@media (hover: hover) and (pointer: fine) {\n :host(:hover) {\n position: relative;\n z-index: var(--dimension-z-index-floating);\n }\n\n .conversation-list-item__row:hover .conversation-list-item__actions {\n opacity: 1;\n pointer-events: auto;\n }\n\n /* Keep the row wash continuous while the pointer moves onto its sibling action. */\n .conversation-list-item__row:hover .conversation-list-item::after {\n background: var(--ds-interaction-hover);\n }\n}\n\n@media (hover: none) {\n .conversation-list-item__actions {\n opacity: 1;\n pointer-events: auto;\n }\n\n :host(.conversation-list-item--has-actions) .conversation-list-item__title-row {\n padding-inline-end: var(--dimension-size-400);\n }\n}\n",
276
276
  "type": "registry:ui"
277
277
  },
278
278
  {
@@ -110,7 +110,7 @@
110
110
  },
111
111
  "peerDependencies": {
112
112
  "required": [
113
- "@ds-mo/tokens ^6.0.0",
113
+ "@ds-mo/tokens ^6.2.1",
114
114
  "@ds-mo/icons ^6.6.0"
115
115
  ],
116
116
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -102,7 +102,7 @@
102
102
  },
103
103
  "peerDependencies": {
104
104
  "required": [
105
- "@ds-mo/tokens ^6.0.0",
105
+ "@ds-mo/tokens ^6.2.1",
106
106
  "@ds-mo/icons ^6.6.0"
107
107
  ],
108
108
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -198,7 +198,7 @@
198
198
  },
199
199
  "peerDependencies": {
200
200
  "required": [
201
- "@ds-mo/tokens ^6.0.0",
201
+ "@ds-mo/tokens ^6.2.1",
202
202
  "@ds-mo/icons ^6.6.0"
203
203
  ],
204
204
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -170,7 +170,7 @@
170
170
  },
171
171
  "peerDependencies": {
172
172
  "required": [
173
- "@ds-mo/tokens ^6.0.0",
173
+ "@ds-mo/tokens ^6.2.1",
174
174
  "@ds-mo/icons ^6.6.0"
175
175
  ],
176
176
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -193,7 +193,7 @@
193
193
  },
194
194
  "peerDependencies": {
195
195
  "required": [
196
- "@ds-mo/tokens ^6.0.0",
196
+ "@ds-mo/tokens ^6.2.1",
197
197
  "@ds-mo/icons ^6.6.0"
198
198
  ],
199
199
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -194,7 +194,7 @@
194
194
  },
195
195
  "peerDependencies": {
196
196
  "required": [
197
- "@ds-mo/tokens ^6.0.0",
197
+ "@ds-mo/tokens ^6.2.1",
198
198
  "@ds-mo/icons ^6.6.0"
199
199
  ],
200
200
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -566,7 +566,7 @@
566
566
  },
567
567
  "peerDependencies": {
568
568
  "required": [
569
- "@ds-mo/tokens ^6.0.0",
569
+ "@ds-mo/tokens ^6.2.1",
570
570
  "@ds-mo/icons ^6.6.0"
571
571
  ],
572
572
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -165,7 +165,7 @@
165
165
  },
166
166
  "peerDependencies": {
167
167
  "required": [
168
- "@ds-mo/tokens ^6.0.0",
168
+ "@ds-mo/tokens ^6.2.1",
169
169
  "@ds-mo/icons ^6.6.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 ^6.0.0",
120
+ "@ds-mo/tokens ^6.2.1",
121
121
  "@ds-mo/icons ^6.6.0"
122
122
  ],
123
123
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -453,7 +453,7 @@
453
453
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: anchor, items, sections.",
454
454
  "peerDependencies": {
455
455
  "required": [
456
- "@ds-mo/tokens ^6.0.0",
456
+ "@ds-mo/tokens ^6.2.1",
457
457
  "@ds-mo/icons ^6.6.0"
458
458
  ],
459
459
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -478,7 +478,7 @@
478
478
  },
479
479
  {
480
480
  "path": "src/wc/components/Menu/Menu.tsx",
481
- "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';\nimport { snapshotMenuSections } from './menu-sections';\n\nexport type MenuSelectionMode = 'none' | 'single';\nexport type MenuSize = ControlSize;\nimport {\n isMenuPickerSection,\n isMenuSwatchPickerSection,\n} from './menu-types';\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\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 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 topLayer: true,\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 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 }) | 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 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--swatch-picker': isMenuPickerSection(section),\n 'ds-choice-section': true,\n 'ds-chrome-column': true,\n 'ds-chrome-space--sm': true,\n 'ds-choice-section--divided': si < sections.length - 1,\n }}\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 {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 && this.selectionMode === 'single' && !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",
481
+ "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';\nimport { snapshotMenuSections } from './menu-sections';\n\nexport type MenuSelectionMode = 'none' | 'single';\nexport type MenuSize = ControlSize;\nimport {\n isMenuPickerSection,\n isMenuSwatchPickerSection,\n} from './menu-types';\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\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 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 topLayer: true,\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 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 }) | 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 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--swatch-picker': isMenuPickerSection(section),\n 'ds-choice-section': true,\n 'ds-chrome-column': true,\n 'ds-chrome-space--sm': true,\n 'ds-choice-section--divided': si < sections.length - 1,\n }}\n role={section.header ? 'group' : undefined}\n aria-label={section.header}\n >\n {section.header && (\n <ds-text\n class={`section-header ds-choice-section__header ds-control-section-heading ds-control--${this.size}`}\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n emphasis\n color=\"primary\"\n aria-hidden=\"true\"\n >\n {section.header}\n </ds-text>\n )}\n {isMenuSwatchPickerSection(section) ? (\n <ds-swatch-picker\n value={section.value}\n groupLabel={section.groupLabel ?? section.header ?? 'Swatch options'}\n options={section.options ?? []}\n sections={section.sections ?? []}\n onDsChange={(e: CustomEvent<string>) => {\n e.stopPropagation();\n this.handleSwatchSelect(e.detail);\n }}\n />\n ) : (\n section.items.map(item => {\n const idx = flatIdx++;\n const isFocused = this.focusedIndex === idx;\n const isSingleSelectionItem =\n !hasCompositeSections && this.selectionMode === 'single' && !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",
482
482
  "type": "registry:ui"
483
483
  },
484
484
  {
@@ -157,7 +157,7 @@
157
157
  },
158
158
  "peerDependencies": {
159
159
  "required": [
160
- "@ds-mo/tokens ^6.0.0",
160
+ "@ds-mo/tokens ^6.2.1",
161
161
  "@ds-mo/icons ^6.6.0"
162
162
  ],
163
163
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -97,7 +97,7 @@
97
97
  },
98
98
  "peerDependencies": {
99
99
  "required": [
100
- "@ds-mo/tokens ^6.0.0",
100
+ "@ds-mo/tokens ^6.2.1",
101
101
  "@ds-mo/icons ^6.6.0"
102
102
  ],
103
103
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -112,7 +112,7 @@
112
112
  "files": [
113
113
  {
114
114
  "path": "src/wc/components/MessageBubble/MessageBubble.css",
115
- "content": ":host {\n display: inline-flex;\n max-width: 80%;\n box-sizing: border-box;\n -webkit-user-select: text;\n user-select: text;\n}\n\n.message-bubble {\n min-width: 0;\n padding-block: var(--dimension-space-100);\n padding-inline: calc(var(--dimension-space-100) + var(--dimension-space-025));\n border-radius: var(--dimension-radius-050);\n background: transparent;\n color: var(--color-foreground-primary);\n font-family: var(--typography-font-family);\n font-size: var(--typography-fontsize-md);\n line-height: var(--typography-lineheight-md);\n font-weight: var(--typography-weight-regular);\n letter-spacing: var(--typography-letterspacing-negative-half);\n box-sizing: border-box;\n overflow-wrap: anywhere;\n}\n\n:host(.message-bubble--user) .message-bubble {\n background: var(--color-translucent-translucent);\n color: var(--color-foreground-secondary);\n box-shadow: inset 0 0 0 var(--dimension-stroke-width-012) var(--color-border-tertiary);\n}\n\n:host(.message-bubble--received) .message-bubble {\n box-shadow: inset 0 0 0 var(--dimension-stroke-width-012) var(--color-border-tertiary);\n}\n",
115
+ "content": ":host {\n display: inline-flex;\n max-width: 80%;\n box-sizing: border-box;\n -webkit-user-select: text;\n user-select: text;\n}\n\n.message-bubble {\n min-width: 0;\n padding-block: var(--dimension-space-100);\n padding-inline: calc(var(--dimension-space-100) + var(--dimension-space-025));\n border-radius: var(--dimension-radius-050);\n background: transparent;\n color: var(--color-foreground-primary);\n font-family: var(--typography-font-family-ui);\n font-size: var(--typography-fontsize-md);\n line-height: var(--typography-lineheight-md);\n font-weight: var(--typography-weight-regular);\n letter-spacing: var(--typography-letterspacing-negative-half);\n box-sizing: border-box;\n overflow-wrap: anywhere;\n}\n\n:host(.message-bubble--user) .message-bubble {\n background: var(--color-translucent-translucent);\n color: var(--color-foreground-secondary);\n box-shadow: inset 0 0 0 var(--dimension-stroke-width-012) var(--color-border-tertiary);\n}\n\n:host(.message-bubble--received) .message-bubble {\n box-shadow: inset 0 0 0 var(--dimension-stroke-width-012) var(--color-border-tertiary);\n}\n",
116
116
  "type": "registry:ui"
117
117
  },
118
118
  {
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
3
  "name": "message-composer",
4
4
  "title": "MessageComposer",
5
- "description": "Two-line auto-growing conversation textarea with submit, stop, error, and extensible leading-tool or trailing-action regions.",
5
+ "description": "Two-line auto-growing conversation textarea with submit, stop, an attached submission-error support surface, and extensible tool, action, and error-recovery regions.",
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": "experimental",
16
- "summary": "Two-line auto-growing conversation textarea with submit, stop, error, and extensible leading-tool or trailing-action regions.",
16
+ "summary": "Two-line auto-growing conversation textarea with submit, stop, an attached submission-error support surface, and extensible tool, action, and error-recovery regions.",
17
17
  "useWhen": [
18
18
  "A conversation accepts multi-line text and needs Enter-to-submit with a streaming stop action."
19
19
  ],
@@ -31,14 +31,17 @@
31
31
  "accessibility": [
32
32
  "Always provide a localized textarea label and submit or stop action names; the built-in icon action exposes the same name in its tooltip.",
33
33
  "Shift plus Enter inserts a newline while Enter submits non-empty content.",
34
- "Streaming and error states remain perceivable without relying on icon shape alone."
34
+ "Submission errors remain visible and announced above the editable box as part of one attached composer unit, and the editable control references their text while the error state is active.",
35
+ "Streaming and error states remain perceivable without relying on icon shape or color alone."
35
36
  ],
36
37
  "states": [
37
38
  "The owner controls draft value and transport state; the composer emits intent without performing transport.",
39
+ "Error status never clears or disables the draft; the application supplies recovery actions through error-actions and owns retry semantics. The negative support surface appears above and attached to the composer box, within the component's measured ownership.",
38
40
  "Use the tools slot for leading actions such as add and the actions slot for trailing actions such as dictation; the built-in submit or stop action remains last with an 8px gap from the preceding action. These icon buttons use the standard non-rounded control radius, and the application supplies tooltips for slotted icon actions.",
39
41
  "The composer surface uses the 10px --dimension-radius-125 token.",
40
42
  "Mouse or keyboard focus on the textarea gives the complete composer surface the same bold-brand inset stroke width and color as an active Input; the native textarea never draws a separate outline.",
41
- "The empty or otherwise inactive submit action is a disabled bordered ButtonUnfilled. Ready submit actions use submitIntent, while the streaming stop action remains a bold-brand ButtonFilled."
43
+ "The empty or otherwise inactive submit action is a disabled bordered ButtonUnfilled. Ready submit actions use submitIntent, while the streaming stop action remains a bold-brand ButtonFilled.",
44
+ "The attached error unit keeps a real boundary in forced colors and lets message text and application-owned recovery actions wrap at narrow widths."
42
45
  ],
43
46
  "responsiveBehavior": [
44
47
  "The textarea begins at two body-medium lines, grows one line at a time through six lines, and then scrolls internally."
@@ -56,6 +59,14 @@
56
59
  },
57
60
  "api": {
58
61
  "props": {
62
+ "errorMessage": {
63
+ "type": "string",
64
+ "resolvedType": "string | undefined",
65
+ "attribute": "error-message",
66
+ "required": false,
67
+ "mutable": false,
68
+ "description": "Submission failure shown while status is error."
69
+ },
59
70
  "isInactive": {
60
71
  "type": "boolean",
61
72
  "resolvedType": "boolean",
@@ -137,6 +148,14 @@
137
148
  "slots": []
138
149
  },
139
150
  "props": {
151
+ "errorMessage": {
152
+ "type": "string",
153
+ "resolvedType": "string | undefined",
154
+ "attribute": "error-message",
155
+ "required": false,
156
+ "mutable": false,
157
+ "description": "Submission failure shown while status is error."
158
+ },
140
159
  "isInactive": {
141
160
  "type": "boolean",
142
161
  "resolvedType": "boolean",
@@ -238,7 +257,7 @@
238
257
  },
239
258
  "peerDependencies": {
240
259
  "required": [
241
- "@ds-mo/tokens ^6.0.0",
260
+ "@ds-mo/tokens ^6.2.1",
242
261
  "@ds-mo/icons ^6.6.0"
243
262
  ],
244
263
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -247,22 +266,25 @@
247
266
  },
248
267
  "dependencies": [
249
268
  "@ds-mo/ui",
250
- "@ds-mo/tokens"
269
+ "@ds-mo/tokens",
270
+ "@ds-mo/icons"
251
271
  ],
252
272
  "registryDependencies": [
253
273
  "button-filled",
254
274
  "button-unfilled",
275
+ "icon",
276
+ "text",
255
277
  "tooltip"
256
278
  ],
257
279
  "files": [
258
280
  {
259
281
  "path": "src/wc/components/MessageComposer/MessageComposer.css",
260
- "content": ":host {\n display: block;\n width: 100%;\n box-sizing: border-box;\n -webkit-user-select: none;\n user-select: none;\n}\n\n.message-composer {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050);\n width: 100%;\n margin: 0;\n}\n\n.message-composer__attachments:empty,\n.message-composer__tools:empty {\n display: none;\n}\n\n.message-composer__field {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050);\n padding: var(--dimension-space-100);\n border-radius: var(--dimension-radius-125);\n background: var(--color-background-primary);\n box-shadow: var(--effect-elevation-elevated-md);\n overflow: hidden;\n}\n\n.message-composer:not(.message-composer--error) .message-composer__field:focus-within {\n box-shadow: var(--effect-elevation-elevated-md),\n inset 0 0 0 var(--dimension-stroke-width-018) var(--color-border-bold-brand);\n}\n\n.message-composer--error .message-composer__field {\n box-shadow: var(--effect-elevation-elevated-md),\n inset 0 0 0 var(--dimension-stroke-width-025) var(--color-border-bold-negative);\n}\n\ntextarea {\n width: 100%;\n min-height: calc(var(--typography-lineheight-md) * 2 + var(--dimension-space-150));\n max-height: calc(var(--typography-lineheight-md) * 6 + var(--dimension-space-150));\n padding: var(--dimension-space-075);\n border: none;\n outline: none;\n resize: none;\n overflow-y: auto;\n box-sizing: border-box;\n background: transparent;\n color: var(--color-foreground-primary);\n font-family: var(--typography-font-family);\n font-size: var(--typography-fontsize-md);\n line-height: var(--typography-lineheight-md);\n font-weight: var(--typography-weight-regular);\n letter-spacing: var(--typography-letterspacing-negative-half);\n -webkit-user-select: text;\n user-select: text;\n}\n\ntextarea::placeholder {\n color: var(--color-foreground-secondary);\n}\n\ntextarea:focus::placeholder {\n color: var(--color-foreground-quaternary);\n}\n\ntextarea:focus,\ntextarea:focus-visible {\n outline: none;\n}\n\n.message-composer__tools {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-050);\n min-height: var(--dimension-size-400);\n}\n\n.message-composer__controls {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--dimension-space-050);\n width: 100%;\n min-width: 0;\n}\n\n.message-composer__actions {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-100);\n flex: 0 0 auto;\n margin-inline-start: auto;\n}\n",
282
+ "content": "@import '../../utils/forced-colors.css';\n\n:host {\n display: block;\n width: 100%;\n box-sizing: border-box;\n -webkit-user-select: none;\n user-select: none;\n}\n\n.message-composer {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050);\n width: 100%;\n margin: 0;\n}\n\n.message-composer__attachments:empty,\n.message-composer__tools:empty,\n.message-composer__error-actions:empty {\n display: none;\n}\n\n.message-composer__error-support,\n.message-composer__error-message,\n.message-composer__error-actions {\n display: flex;\n align-items: center;\n}\n\n.message-composer__error-support {\n justify-content: space-between;\n flex-wrap: wrap;\n gap: var(--dimension-space-075) var(--dimension-space-100);\n min-width: 0;\n padding: var(--dimension-space-075) var(--dimension-space-100);\n background: var(--color-background-faint-negative);\n}\n\n.message-composer__error-message {\n flex: 1 1 var(--dimension-panel-width-xs);\n gap: var(--dimension-space-050);\n min-width: 0;\n color: var(--color-foreground-bold-negative);\n}\n\n.message-composer__error-actions {\n flex: 0 1 auto;\n flex-wrap: wrap;\n gap: var(--dimension-space-050);\n}\n\n.message-composer__stack {\n min-width: 0;\n}\n\n.message-composer--error .message-composer__stack {\n overflow: hidden;\n border: var(--dimension-stroke-width-012) solid var(--color-border-bold-negative);\n border-radius: var(--dimension-radius-125);\n background: var(--color-background-primary);\n box-shadow: var(--effect-elevation-elevated-md);\n}\n\n.message-composer__field {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050);\n padding: var(--dimension-space-100);\n border-radius: var(--dimension-radius-125);\n background: var(--color-background-primary);\n box-shadow: var(--effect-elevation-elevated-md);\n overflow: hidden;\n}\n\n.message-composer:not(.message-composer--error) .message-composer__field:focus-within {\n box-shadow: var(--effect-elevation-elevated-md),\n inset 0 0 0 var(--dimension-stroke-width-018) var(--color-border-bold-brand);\n}\n\n.message-composer--error .message-composer__field {\n border-radius: 0;\n box-shadow: none;\n}\n\ntextarea {\n width: 100%;\n min-height: calc(var(--typography-lineheight-md) * 2 + var(--dimension-space-150));\n max-height: calc(var(--typography-lineheight-md) * 6 + var(--dimension-space-150));\n padding: var(--dimension-space-075);\n border: none;\n outline: none;\n resize: none;\n overflow-y: auto;\n box-sizing: border-box;\n background: transparent;\n color: var(--color-foreground-primary);\n font-family: var(--typography-font-family-ui);\n font-size: var(--typography-fontsize-md);\n line-height: var(--typography-lineheight-md);\n font-weight: var(--typography-weight-regular);\n letter-spacing: var(--typography-letterspacing-negative-half);\n -webkit-user-select: text;\n user-select: text;\n}\n\ntextarea::placeholder {\n color: var(--color-foreground-secondary);\n}\n\ntextarea:focus::placeholder {\n color: var(--color-foreground-quaternary);\n}\n\ntextarea:focus,\ntextarea:focus-visible {\n outline: none;\n}\n\n.message-composer__tools {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-050);\n min-height: var(--dimension-size-400);\n}\n\n.message-composer__controls {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--dimension-space-050);\n width: 100%;\n min-width: 0;\n}\n\n.message-composer__actions {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-100);\n flex: 0 0 auto;\n margin-inline-start: auto;\n}\n\n@media (forced-colors: active) {\n .message-composer__field {\n border: var(--dimension-stroke-width-012) solid var(--ds-forced-color-control-boundary);\n box-shadow: none;\n }\n\n .message-composer--error .message-composer__field {\n border: 0;\n }\n\n .message-composer--error .message-composer__stack {\n border: var(--dimension-stroke-width-025) dashed var(--ds-forced-color-content);\n box-shadow: none;\n }\n\n .message-composer__error-support {\n border-block-end: var(--dimension-stroke-width-012) solid\n var(--ds-forced-color-content);\n background: var(--ds-forced-color-surface);\n }\n}\n",
261
283
  "type": "registry:ui"
262
284
  },
263
285
  {
264
286
  "path": "src/wc/components/MessageComposer/MessageComposer.tsx",
265
- "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n Watch,\n} from '@stencil/core';\nimport type { ButtonFilledIntent } from '../ButtonFilled/ButtonFilled';\nimport type { MessageComposerStatus } from '../conversation-types';\n\n@Component({\n tag: 'ds-message-composer',\n styleUrl: 'MessageComposer.css',\n scoped: true,\n})\nexport class MessageComposer {\n @Element() el!: HTMLElement;\n\n @Prop({ mutable: true }) value: string = '';\n @Prop() placeholder: string = 'Write a message';\n @Prop() label: string = 'Message';\n @Prop() status: MessageComposerStatus = 'ready';\n @Prop() isInactive: boolean = false;\n @Prop() submitIntent: ButtonFilledIntent = 'brand';\n\n @Event() dsInput!: EventEmitter<string>;\n @Event() dsSubmit!: EventEmitter<{ text: string }>;\n @Event() dsStop!: EventEmitter<void>;\n\n private textarea?: HTMLTextAreaElement;\n\n componentDidLoad() {\n this.resize();\n }\n\n @Watch('value')\n onValueChange() {\n requestAnimationFrame(() => this.resize());\n }\n\n @Method()\n async setFocus() {\n this.textarea?.focus();\n }\n\n private get streaming(): boolean {\n return this.status === 'submitted' || this.status === 'streaming';\n }\n\n private resize() {\n if (!this.textarea) return;\n this.textarea.style.height = 'auto';\n this.textarea.style.height = `${this.textarea.scrollHeight}px`;\n }\n\n private handleInput = (event: Event) => {\n this.value = (event.target as HTMLTextAreaElement).value;\n this.resize();\n this.dsInput.emit(this.value);\n };\n\n private submit() {\n const text = this.value.trim();\n if (!text || this.isInactive || this.streaming) return;\n this.dsSubmit.emit({ text });\n }\n\n private handleKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'Enter' || event.shiftKey || event.isComposing) return;\n event.preventDefault();\n this.submit();\n };\n\n private handleAction = () => {\n if (this.streaming) this.dsStop.emit();\n else this.submit();\n };\n\n render() {\n const error = this.status === 'error';\n const actionInactive = !this.streaming && (this.isInactive || !this.value.trim());\n return (\n <Host>\n <form\n class={{ 'message-composer': true, 'message-composer--error': error }}\n aria-busy={this.streaming ? 'true' : undefined}\n onSubmit={(event: Event) => {\n event.preventDefault();\n this.submit();\n }}\n >\n <div class=\"message-composer__attachments\">\n <slot name=\"attachments\" />\n </div>\n <div class=\"message-composer__field\">\n <textarea\n ref={element => {\n this.textarea = element;\n }}\n value={this.value}\n placeholder={this.placeholder}\n aria-label={this.label}\n aria-invalid={error ? 'true' : undefined}\n disabled={this.isInactive}\n rows={2}\n onInput={this.handleInput}\n onKeyDown={this.handleKeyDown}\n />\n <div class=\"message-composer__controls\">\n <div class=\"message-composer__tools\">\n <slot name=\"tools\" />\n </div>\n <div class=\"message-composer__actions\">\n <slot name=\"actions\" />\n {actionInactive ? (\n <ds-tooltip label=\"Send message\" side=\"top\" size=\"sm\">\n <ds-button-unfilled\n class=\"message-composer__action\"\n variant=\"icon\"\n icon=\"ArrowUp\"\n size=\"md\"\n aria-label=\"Send message\"\n hasBorder\n isInactive\n onDsClick={this.handleAction}\n />\n </ds-tooltip>\n ) : (\n <ds-tooltip\n label={this.streaming ? 'Stop response' : 'Send message'}\n side=\"top\"\n size=\"sm\"\n >\n <ds-button-filled\n class=\"message-composer__action\"\n variant=\"icon\"\n icon={this.streaming ? 'SquareFilled' : 'ArrowUp'}\n intent={this.streaming ? 'brand' : this.submitIntent}\n contrast=\"bold\"\n size=\"md\"\n aria-label={this.streaming ? 'Stop response' : 'Send message'}\n onDsClick={this.handleAction}\n />\n </ds-tooltip>\n )}\n </div>\n </div>\n </div>\n </form>\n </Host>\n );\n }\n}\n",
287
+ "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n Watch,\n} from '@stencil/core';\nimport type { ButtonFilledIntent } from '../ButtonFilled/ButtonFilled';\nimport type { MessageComposerStatus } from '../conversation-types';\n\nlet composerId = 0;\n\n@Component({\n tag: 'ds-message-composer',\n styleUrl: 'MessageComposer.css',\n scoped: true,\n})\nexport class MessageComposer {\n @Element() el!: HTMLElement;\n\n @Prop({ mutable: true }) value: string = '';\n @Prop() placeholder: string = 'Write a message';\n @Prop() label: string = 'Message';\n @Prop() status: MessageComposerStatus = 'ready';\n /** Submission failure shown while status is error. */\n @Prop() errorMessage?: string;\n @Prop() isInactive: boolean = false;\n @Prop() submitIntent: ButtonFilledIntent = 'brand';\n\n @Event() dsInput!: EventEmitter<string>;\n @Event() dsSubmit!: EventEmitter<{ text: string }>;\n @Event() dsStop!: EventEmitter<void>;\n\n private textarea?: HTMLTextAreaElement;\n private readonly errorId = `ds-message-composer-error-${++composerId}`;\n\n componentDidLoad() {\n this.resize();\n }\n\n @Watch('value')\n onValueChange() {\n requestAnimationFrame(() => this.resize());\n }\n\n @Method()\n async setFocus() {\n this.textarea?.focus();\n }\n\n private get streaming(): boolean {\n return this.status === 'submitted' || this.status === 'streaming';\n }\n\n private resize() {\n if (!this.textarea) return;\n this.textarea.style.height = 'auto';\n this.textarea.style.height = `${this.textarea.scrollHeight}px`;\n }\n\n private handleInput = (event: Event) => {\n this.value = (event.target as HTMLTextAreaElement).value;\n this.resize();\n this.dsInput.emit(this.value);\n };\n\n private submit() {\n const text = this.value.trim();\n if (!text || this.isInactive || this.streaming) return;\n this.dsSubmit.emit({ text });\n }\n\n private handleKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'Enter' || event.shiftKey || event.isComposing) return;\n event.preventDefault();\n this.submit();\n };\n\n private handleAction = () => {\n if (this.streaming) this.dsStop.emit();\n else this.submit();\n };\n\n render() {\n const error = this.status === 'error';\n const visibleErrorMessage = this.errorMessage || 'Message could not be sent.';\n const actionInactive = !this.streaming && (this.isInactive || !this.value.trim());\n return (\n <Host>\n <form\n class={{ 'message-composer': true, 'message-composer--error': error }}\n aria-busy={this.streaming ? 'true' : undefined}\n onSubmit={(event: Event) => {\n event.preventDefault();\n this.submit();\n }}\n >\n <div class=\"message-composer__attachments\">\n <slot name=\"attachments\" />\n </div>\n <div class=\"message-composer__stack\">\n {error ? (\n <div class=\"message-composer__error-support\">\n <div id={this.errorId} class=\"message-composer__error-message\" role=\"alert\">\n <ds-icon name=\"ErrorTriangle\" size=\"xs\" color=\"inherit\" />\n <ds-text variant=\"text-body-small\" color=\"negative\">\n {visibleErrorMessage}\n </ds-text>\n </div>\n <div class=\"message-composer__error-actions\">\n <slot name=\"error-actions\" />\n </div>\n </div>\n ) : null}\n <div class=\"message-composer__field\">\n <textarea\n ref={element => {\n this.textarea = element;\n }}\n value={this.value}\n placeholder={this.placeholder}\n aria-label={this.label}\n aria-invalid={error ? 'true' : undefined}\n aria-describedby={error ? this.errorId : undefined}\n disabled={this.isInactive}\n rows={2}\n onInput={this.handleInput}\n onKeyDown={this.handleKeyDown}\n />\n <div class=\"message-composer__controls\">\n <div class=\"message-composer__tools\">\n <slot name=\"tools\" />\n </div>\n <div class=\"message-composer__actions\">\n <slot name=\"actions\" />\n {actionInactive ? (\n <ds-tooltip label=\"Send message\" side=\"top\" size=\"sm\">\n <ds-button-unfilled\n class=\"message-composer__action\"\n variant=\"icon\"\n icon=\"ArrowUp\"\n size=\"md\"\n aria-label=\"Send message\"\n hasBorder\n isInactive\n onDsClick={this.handleAction}\n />\n </ds-tooltip>\n ) : (\n <ds-tooltip\n label={this.streaming ? 'Stop response' : 'Send message'}\n side=\"top\"\n size=\"sm\"\n >\n <ds-button-filled\n class=\"message-composer__action\"\n variant=\"icon\"\n icon={this.streaming ? 'SquareFilled' : 'ArrowUp'}\n intent={this.streaming ? 'brand' : this.submitIntent}\n contrast=\"bold\"\n size=\"md\"\n aria-label={this.streaming ? 'Stop response' : 'Send message'}\n onDsClick={this.handleAction}\n />\n </ds-tooltip>\n )}\n </div>\n </div>\n </div>\n </div>\n </form>\n </Host>\n );\n }\n}\n",
266
288
  "type": "registry:ui"
267
289
  }
268
290
  ]