@ds-mo/ui 9.1.0 → 9.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (318) hide show
  1. package/dist/.build-stamp +1 -1
  2. package/dist/agent-patterns.json +165 -1
  3. package/dist/agent.json +2110 -634
  4. package/dist/angular/ds-agent-activity.d.ts +12 -0
  5. package/dist/angular/ds-agent-activity.js +38 -0
  6. package/dist/angular/ds-agent-response.d.ts +12 -0
  7. package/dist/angular/ds-agent-response.js +38 -0
  8. package/dist/angular/ds-agent-source-list.d.ts +12 -0
  9. package/dist/angular/ds-agent-source-list.js +38 -0
  10. package/dist/angular/ds-agent-tool-call.d.ts +12 -0
  11. package/dist/angular/ds-agent-tool-call.js +38 -0
  12. package/dist/angular/ds-attachment-list.d.ts +12 -0
  13. package/dist/angular/ds-attachment-list.js +38 -0
  14. package/dist/angular/ds-avatar.d.ts +12 -0
  15. package/dist/angular/ds-avatar.js +38 -0
  16. package/dist/angular/ds-code-block.d.ts +12 -0
  17. package/dist/angular/ds-code-block.js +38 -0
  18. package/dist/angular/ds-conversation-list-item.d.ts +19 -0
  19. package/dist/angular/ds-conversation-list-item.js +42 -0
  20. package/dist/angular/ds-conversation-list-section.d.ts +12 -0
  21. package/dist/angular/ds-conversation-list-section.js +38 -0
  22. package/dist/angular/ds-conversation-list.d.ts +12 -0
  23. package/dist/angular/ds-conversation-list.js +37 -0
  24. package/dist/angular/ds-markdown.d.ts +12 -0
  25. package/dist/angular/ds-markdown.js +38 -0
  26. package/dist/angular/ds-menu.d.ts +1 -1
  27. package/dist/angular/ds-menu.js +3 -3
  28. package/dist/angular/ds-message-bubble.d.ts +12 -0
  29. package/dist/angular/ds-message-bubble.js +38 -0
  30. package/dist/angular/ds-message-composer.d.ts +23 -0
  31. package/dist/angular/ds-message-composer.js +49 -0
  32. package/dist/angular/ds-message-scroller.d.ts +15 -0
  33. package/dist/angular/ds-message-scroller.js +43 -0
  34. package/dist/angular/ds-message.d.ts +12 -0
  35. package/dist/angular/ds-message.js +38 -0
  36. package/dist/angular/ds-panel-tool-header.d.ts +25 -0
  37. package/dist/angular/ds-panel-tool-header.js +49 -0
  38. package/dist/angular/ds-panel-tool-search.d.ts +19 -0
  39. package/dist/angular/ds-panel-tool-search.js +49 -0
  40. package/dist/angular/ds-panel-tools.d.ts +30 -1
  41. package/dist/angular/ds-panel-tools.js +14 -5
  42. package/dist/angular/index.d.ts +1 -1
  43. package/dist/angular/index.js +17 -0
  44. package/dist/angular/proxies.d.ts +17 -0
  45. package/dist/angular/proxies.js +17 -0
  46. package/dist/components/ds-agent-activity.d.ts +11 -0
  47. package/dist/components/ds-agent-activity.js +2 -0
  48. package/dist/components/ds-agent-activity.js.map +1 -0
  49. package/dist/components/ds-agent-response.d.ts +11 -0
  50. package/dist/components/ds-agent-response.js +2 -0
  51. package/dist/components/ds-agent-response.js.map +1 -0
  52. package/dist/components/ds-agent-source-list.d.ts +11 -0
  53. package/dist/components/ds-agent-source-list.js +2 -0
  54. package/dist/components/ds-agent-source-list.js.map +1 -0
  55. package/dist/components/ds-agent-tool-call.d.ts +11 -0
  56. package/dist/components/ds-agent-tool-call.js +2 -0
  57. package/dist/components/ds-agent-tool-call.js.map +1 -0
  58. package/dist/components/ds-app-shell.js +1 -1
  59. package/dist/components/ds-app-shell.js.map +1 -1
  60. package/dist/components/ds-attachment-list.d.ts +11 -0
  61. package/dist/components/ds-attachment-list.js +2 -0
  62. package/dist/components/ds-attachment-list.js.map +1 -0
  63. package/dist/components/ds-avatar.d.ts +11 -0
  64. package/dist/components/ds-avatar.js +2 -0
  65. package/dist/components/ds-avatar.js.map +1 -0
  66. package/dist/components/ds-badge.js +1 -1
  67. package/dist/components/ds-banner.js +1 -1
  68. package/dist/components/ds-bar-nav.js +1 -1
  69. package/dist/components/ds-bar-nav.js.map +1 -1
  70. package/dist/components/ds-button-filled.js +1 -1
  71. package/dist/components/ds-button-unfilled.js +1 -1
  72. package/dist/components/ds-card-data-viz-donut.js +1 -1
  73. package/dist/components/ds-card-setting.js +1 -1
  74. package/dist/components/ds-card-shell-data-viz.js +1 -1
  75. package/dist/components/ds-card.js +1 -1
  76. package/dist/components/ds-chart-bar.js +1 -1
  77. package/dist/components/ds-chart-bar.js.map +1 -1
  78. package/dist/components/ds-chart-donut.js +1 -1
  79. package/dist/components/ds-chart-legend.js +1 -1
  80. package/dist/components/ds-chart-line.js +1 -1
  81. package/dist/components/ds-checkbox.js +1 -1
  82. package/dist/components/ds-chip.js +1 -1
  83. package/dist/components/ds-code-block.d.ts +11 -0
  84. package/dist/components/ds-code-block.js +2 -0
  85. package/dist/components/ds-code-block.js.map +1 -0
  86. package/dist/components/ds-conversation-list-item.d.ts +11 -0
  87. package/dist/components/ds-conversation-list-item.js +2 -0
  88. package/dist/components/ds-conversation-list-item.js.map +1 -0
  89. package/dist/components/ds-conversation-list-section.d.ts +11 -0
  90. package/dist/components/ds-conversation-list-section.js +2 -0
  91. package/dist/components/ds-conversation-list-section.js.map +1 -0
  92. package/dist/components/ds-conversation-list.d.ts +11 -0
  93. package/dist/components/ds-conversation-list.js +2 -0
  94. package/dist/components/ds-conversation-list.js.map +1 -0
  95. package/dist/components/ds-divider.js +1 -1
  96. package/dist/components/ds-divider.js.map +1 -1
  97. package/dist/components/ds-empty-state.js +1 -1
  98. package/dist/components/ds-field.js +1 -1
  99. package/dist/components/ds-icon.js +1 -1
  100. package/dist/components/ds-input.js +1 -1
  101. package/dist/components/ds-markdown.d.ts +11 -0
  102. package/dist/components/ds-markdown.js +2 -0
  103. package/dist/components/ds-markdown.js.map +1 -0
  104. package/dist/components/ds-menu.js +1 -1
  105. package/dist/components/ds-message-bubble.d.ts +11 -0
  106. package/dist/components/ds-message-bubble.js +2 -0
  107. package/dist/components/ds-message-bubble.js.map +1 -0
  108. package/dist/components/ds-message-composer.d.ts +11 -0
  109. package/dist/components/ds-message-composer.js +2 -0
  110. package/dist/components/ds-message-composer.js.map +1 -0
  111. package/dist/components/ds-message-scroller.d.ts +11 -0
  112. package/dist/components/ds-message-scroller.js +2 -0
  113. package/dist/components/ds-message-scroller.js.map +1 -0
  114. package/dist/components/ds-message.d.ts +11 -0
  115. package/dist/components/ds-message.js +2 -0
  116. package/dist/components/ds-message.js.map +1 -0
  117. package/dist/components/ds-modal.js +1 -1
  118. package/dist/components/ds-panel-nav.js +1 -1
  119. package/dist/components/ds-panel-nav.js.map +1 -1
  120. package/dist/components/ds-panel-sub-nav.js +1 -1
  121. package/dist/components/ds-panel-tool-header.d.ts +11 -0
  122. package/dist/components/ds-panel-tool-header.js +2 -0
  123. package/dist/components/ds-panel-tool-header.js.map +1 -0
  124. package/dist/components/ds-panel-tool-search.d.ts +11 -0
  125. package/dist/components/ds-panel-tool-search.js +2 -0
  126. package/dist/components/ds-panel-tool-search.js.map +1 -0
  127. package/dist/components/ds-panel-tools.js +1 -1
  128. package/dist/components/ds-panel-tools.js.map +1 -1
  129. package/dist/components/ds-radio.js +1 -1
  130. package/dist/components/ds-select-multi.js +1 -1
  131. package/dist/components/ds-select-multi.js.map +1 -1
  132. package/dist/components/ds-select.js +1 -1
  133. package/dist/components/ds-select.js.map +1 -1
  134. package/dist/components/ds-shell-gradient-picker.js +1 -1
  135. package/dist/components/ds-shell-gradient-picker.js.map +1 -1
  136. package/dist/components/ds-shell-gradient-swatch.js +1 -1
  137. package/dist/components/ds-skeleton.js +1 -1
  138. package/dist/components/ds-slider.js +1 -1
  139. package/dist/components/ds-slider.js.map +1 -1
  140. package/dist/components/ds-swatch-picker.js +1 -1
  141. package/dist/components/ds-switch.js +1 -1
  142. package/dist/components/ds-tab-group.js +1 -1
  143. package/dist/components/ds-tab-group.js.map +1 -1
  144. package/dist/components/ds-tag.js +1 -1
  145. package/dist/components/ds-text.js +1 -1
  146. package/dist/components/ds-tooltip-data-viz.js +1 -1
  147. package/dist/components/ds-tooltip.js +1 -1
  148. package/dist/components/index.d.ts +14 -0
  149. package/dist/components/p--KbMQblZ.js +2 -0
  150. package/dist/components/p--KbMQblZ.js.map +1 -0
  151. package/dist/components/p-2br9YDbG.js +2 -0
  152. package/dist/components/p-2br9YDbG.js.map +1 -0
  153. package/dist/components/p-41UAx2eq.js +2 -0
  154. package/dist/components/p-41UAx2eq.js.map +1 -0
  155. package/dist/components/{p-HBIxvS5C.js → p-B1pWGQn1.js} +2 -2
  156. package/dist/components/p-B7NbIYjS.js +2 -0
  157. package/dist/components/p-B7NbIYjS.js.map +1 -0
  158. package/dist/components/p-BBu_Svz7.js +2 -0
  159. package/dist/components/p-BBu_Svz7.js.map +1 -0
  160. package/dist/components/p-BEQB8EnB.js +2 -0
  161. package/dist/components/p-BEQB8EnB.js.map +1 -0
  162. package/dist/components/{p-CZ9wemOA.js → p-BFWC17em.js} +2 -2
  163. package/dist/components/{p-KRGBGTu5.js → p-BO6C8abO.js} +2 -2
  164. package/dist/components/{p-CbS_WWs2.js → p-BSOZXagj.js} +2 -2
  165. package/dist/components/p-BSOZXagj.js.map +1 -0
  166. package/dist/components/{p-DT2MK8Kt.js → p-BZAEZENv.js} +2 -2
  167. package/dist/components/p-BhC1YyId.js +2 -0
  168. package/dist/components/p-BhC1YyId.js.map +1 -0
  169. package/dist/components/p-BjtXYFhw.js +2 -0
  170. package/dist/components/p-BjtXYFhw.js.map +1 -0
  171. package/dist/components/{p-B-zhgAvL.js → p-Bo5wikia.js} +2 -2
  172. package/dist/components/p-Bx1G7P6W.js +2 -0
  173. package/dist/components/p-Bx1G7P6W.js.map +1 -0
  174. package/dist/components/p-C0sq948W.js +2 -0
  175. package/dist/components/p-C0sq948W.js.map +1 -0
  176. package/dist/components/p-CC1C8s6m.js +2 -0
  177. package/dist/components/p-CC1C8s6m.js.map +1 -0
  178. package/dist/components/p-CKaJx0IX.js +2 -0
  179. package/dist/components/p-CKaJx0IX.js.map +1 -0
  180. package/dist/components/{p-DyZj3k9S.js → p-CZMEhdlA.js} +2 -2
  181. package/dist/components/{p-BbV68F23.js → p-CiwdnKKp.js} +2 -2
  182. package/dist/components/{p-BLzivrE3.js → p-CnM9SW_8.js} +2 -2
  183. package/dist/components/{p-C7UFYvsO.js → p-CuKn9I5f.js} +2 -2
  184. package/dist/components/p-DFMM60My.js +2 -0
  185. package/dist/components/p-DFMM60My.js.map +1 -0
  186. package/dist/components/p-DIsDiGcp.js +2 -0
  187. package/dist/components/p-DIsDiGcp.js.map +1 -0
  188. package/dist/components/p-DVsje4AY.js +2 -0
  189. package/dist/components/p-DVsje4AY.js.map +1 -0
  190. package/dist/components/p-Di-ThR90.js +2 -0
  191. package/dist/components/p-Di-ThR90.js.map +1 -0
  192. package/dist/components/{p-C7RObioO.js → p-Di4HBckb.js} +2 -2
  193. package/dist/components/p-a9nGf1s6.js +2 -0
  194. package/dist/components/p-a9nGf1s6.js.map +1 -0
  195. package/dist/components/p-gidVv1FX.js +2 -0
  196. package/dist/components/p-gidVv1FX.js.map +1 -0
  197. package/dist/components/p-rEe-3KR_.js +2 -0
  198. package/dist/components/p-rEe-3KR_.js.map +1 -0
  199. package/dist/components/p-t9Ei1kHP.js +2 -0
  200. package/dist/components/p-t9Ei1kHP.js.map +1 -0
  201. package/dist/docs/components.json +7834 -3899
  202. package/dist/lib/components/Menu/menu-placement.d.ts +13 -0
  203. package/dist/lib/shell/index.d.ts +1 -1
  204. package/dist/lib/shell/index.js +8 -0
  205. package/dist/lib/shell/index.js.map +2 -2
  206. package/dist/mcp-data/registry/agent-activity.json +158 -0
  207. package/dist/mcp-data/registry/agent-response.json +211 -0
  208. package/dist/mcp-data/registry/agent-source-list.json +153 -0
  209. package/dist/mcp-data/registry/agent-tool-call.json +213 -0
  210. package/dist/mcp-data/registry/app-shell.json +2 -2
  211. package/dist/mcp-data/registry/attachment-list.json +137 -0
  212. package/dist/mcp-data/registry/avatar.json +136 -0
  213. package/dist/mcp-data/registry/bar-nav.json +2 -2
  214. package/dist/mcp-data/registry/code-block.json +150 -0
  215. package/dist/mcp-data/registry/conversation-list-item.json +265 -0
  216. package/dist/mcp-data/registry/conversation-list-section.json +139 -0
  217. package/dist/mcp-data/registry/conversation-list.json +101 -0
  218. package/dist/mcp-data/registry/markdown.json +141 -0
  219. package/dist/mcp-data/registry/menu.json +27 -7
  220. package/dist/mcp-data/registry/message-bubble.json +119 -0
  221. package/dist/mcp-data/registry/message-composer.json +265 -0
  222. package/dist/mcp-data/registry/message-scroller.json +217 -0
  223. package/dist/mcp-data/registry/message.json +255 -0
  224. package/dist/mcp-data/registry/panel-nav.json +2 -2
  225. package/dist/mcp-data/registry/panel-tool-header.json +342 -0
  226. package/dist/mcp-data/registry/panel-tool-search.json +387 -0
  227. package/dist/mcp-data/registry/panel-tools.json +150 -10
  228. package/dist/mcp-data/registry/registry.json +2146 -619
  229. package/dist/mcp-data/registry/select.json +1 -1
  230. package/dist/mcp-data/registry/slider.json +4 -4
  231. package/dist/mcp-data/registry/tab-group.json +2 -2
  232. package/dist/mcp-data/registry/text.json +1 -1
  233. package/dist/react/components.d.ts +17 -0
  234. package/dist/react/components.js +17 -0
  235. package/dist/react/ds-agent-activity.d.ts +9 -0
  236. package/dist/react/ds-agent-activity.js +12 -0
  237. package/dist/react/ds-agent-response.d.ts +9 -0
  238. package/dist/react/ds-agent-response.js +12 -0
  239. package/dist/react/ds-agent-source-list.d.ts +9 -0
  240. package/dist/react/ds-agent-source-list.js +12 -0
  241. package/dist/react/ds-agent-tool-call.d.ts +9 -0
  242. package/dist/react/ds-agent-tool-call.js +12 -0
  243. package/dist/react/ds-attachment-list.d.ts +9 -0
  244. package/dist/react/ds-attachment-list.js +12 -0
  245. package/dist/react/ds-avatar.d.ts +9 -0
  246. package/dist/react/ds-avatar.js +12 -0
  247. package/dist/react/ds-code-block.d.ts +9 -0
  248. package/dist/react/ds-code-block.js +12 -0
  249. package/dist/react/ds-conversation-list-item.d.ts +14 -0
  250. package/dist/react/ds-conversation-list-item.js +12 -0
  251. package/dist/react/ds-conversation-list-section.d.ts +9 -0
  252. package/dist/react/ds-conversation-list-section.js +12 -0
  253. package/dist/react/ds-conversation-list.d.ts +9 -0
  254. package/dist/react/ds-conversation-list.js +12 -0
  255. package/dist/react/ds-markdown.d.ts +9 -0
  256. package/dist/react/ds-markdown.js +12 -0
  257. package/dist/react/ds-message-bubble.d.ts +9 -0
  258. package/dist/react/ds-message-bubble.js +12 -0
  259. package/dist/react/ds-message-composer.d.ts +16 -0
  260. package/dist/react/ds-message-composer.js +16 -0
  261. package/dist/react/ds-message-scroller.d.ts +12 -0
  262. package/dist/react/ds-message-scroller.js +12 -0
  263. package/dist/react/ds-message.d.ts +9 -0
  264. package/dist/react/ds-message.js +12 -0
  265. package/dist/react/ds-panel-tool-header.d.ts +17 -0
  266. package/dist/react/ds-panel-tool-header.js +16 -0
  267. package/dist/react/ds-panel-tool-search.d.ts +14 -0
  268. package/dist/react/ds-panel-tool-search.js +16 -0
  269. package/dist/react/ds-panel-tools.d.ts +10 -0
  270. package/dist/react/ds-panel-tools.js +3 -0
  271. package/dist/types/components/AgentActivity/AgentActivity.d.ts +14 -0
  272. package/dist/types/components/AgentResponse/AgentResponse.d.ts +12 -0
  273. package/dist/types/components/AgentSourceList/AgentSourceList.d.ts +9 -0
  274. package/dist/types/components/AgentToolCall/AgentToolCall.d.ts +13 -0
  275. package/dist/types/components/AppShell/AppShell.d.ts +4 -0
  276. package/dist/types/components/AttachmentList/AttachmentList.d.ts +8 -0
  277. package/dist/types/components/Avatar/Avatar.d.ts +8 -0
  278. package/dist/types/components/CodeBlock/CodeBlock.d.ts +11 -0
  279. package/dist/types/components/ConversationList/ConversationList.d.ts +4 -0
  280. package/dist/types/components/ConversationListItem/ConversationListItem.d.ts +22 -0
  281. package/dist/types/components/ConversationListSection/ConversationListSection.d.ts +9 -0
  282. package/dist/types/components/Markdown/Markdown.d.ts +15 -0
  283. package/dist/types/components/Menu/Menu.d.ts +3 -0
  284. package/dist/types/components/Menu/menu-placement.d.ts +13 -0
  285. package/dist/types/components/Menu/menu-types.d.ts +1 -1
  286. package/dist/types/components/Message/Message.d.ts +17 -0
  287. package/dist/types/components/MessageBubble/MessageBubble.d.ts +6 -0
  288. package/dist/types/components/MessageComposer/MessageComposer.d.ts +29 -0
  289. package/dist/types/components/MessageScroller/MessageScroller.d.ts +37 -0
  290. package/dist/types/components/PanelNav/PanelNav.d.ts +3 -0
  291. package/dist/types/components/PanelToolHeader/PanelToolHeader.d.ts +34 -0
  292. package/dist/types/components/PanelToolSearch/PanelToolSearch.d.ts +35 -0
  293. package/dist/types/components/PanelTools/PanelTools.d.ts +40 -1
  294. package/dist/types/components/PanelTools/panel-tools-types.d.ts +20 -0
  295. package/dist/types/components/PanelTools/panel-tools-utils.d.ts +2 -0
  296. package/dist/types/components/Slider/Slider.d.ts +1 -1
  297. package/dist/types/components/conversation-types.d.ts +55 -0
  298. package/dist/types/components.d.ts +1185 -8
  299. package/dist/types/shell/index.d.ts +1 -1
  300. package/dist/types/utils/choice-list-parts.d.ts +4 -1
  301. package/package.json +4 -1
  302. package/dist/components/p-CXF8yd-g.js +0 -2
  303. package/dist/components/p-CXF8yd-g.js.map +0 -1
  304. package/dist/components/p-CbS_WWs2.js.map +0 -1
  305. package/dist/components/p-VKIsOVza.js +0 -2
  306. package/dist/components/p-VKIsOVza.js.map +0 -1
  307. package/dist/components/p-tQ1QHeOE.js +0 -2
  308. package/dist/components/p-tQ1QHeOE.js.map +0 -1
  309. /package/dist/components/{p-HBIxvS5C.js.map → p-B1pWGQn1.js.map} +0 -0
  310. /package/dist/components/{p-CZ9wemOA.js.map → p-BFWC17em.js.map} +0 -0
  311. /package/dist/components/{p-KRGBGTu5.js.map → p-BO6C8abO.js.map} +0 -0
  312. /package/dist/components/{p-DT2MK8Kt.js.map → p-BZAEZENv.js.map} +0 -0
  313. /package/dist/components/{p-B-zhgAvL.js.map → p-Bo5wikia.js.map} +0 -0
  314. /package/dist/components/{p-DyZj3k9S.js.map → p-CZMEhdlA.js.map} +0 -0
  315. /package/dist/components/{p-BbV68F23.js.map → p-CiwdnKKp.js.map} +0 -0
  316. /package/dist/components/{p-BLzivrE3.js.map → p-CnM9SW_8.js.map} +0 -0
  317. /package/dist/components/{p-C7UFYvsO.js.map → p-CuKn9I5f.js.map} +0 -0
  318. /package/dist/components/{p-C7RObioO.js.map → p-Di4HBckb.js.map} +0 -0
@@ -0,0 +1,141 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "markdown",
4
+ "title": "Markdown",
5
+ "description": "Safe CommonMark and GFM renderer built from parsed nodes without raw HTML injection.",
6
+ "type": "registry:ui",
7
+ "meta": {
8
+ "library": "@ds-mo/ui",
9
+ "distribution": "npm",
10
+ "storybook": "https://zainadeel.github.io/compomo/?path=/story/markdown",
11
+ "source": "src/wc/components/Markdown/Markdown.tsx",
12
+ "intentStatus": "complete",
13
+ "intent": {
14
+ "audience": "specialized",
15
+ "status": "experimental",
16
+ "summary": "Safe CommonMark and GFM renderer built from parsed nodes without raw HTML injection.",
17
+ "useWhen": [
18
+ "Trusted or untrusted message text needs headings, lists, links, tables, tasks, quotes, and code."
19
+ ],
20
+ "avoidWhen": [
21
+ "The application needs arbitrary HTML, custom plugins, or a full document CMS renderer."
22
+ ],
23
+ "commonlyComposedWith": [
24
+ "component:ds-code-block",
25
+ "component:ds-message-bubble"
26
+ ],
27
+ "patterns": [
28
+ "pattern:conversation",
29
+ "pattern:agent-chat"
30
+ ],
31
+ "accessibility": [
32
+ "Semantic parsed elements preserve heading, list, table, quote, and code relationships.",
33
+ "Raw HTML is ignored and unsafe link schemes are not made interactive.",
34
+ "Link text must remain descriptive in authored content."
35
+ ],
36
+ "states": [
37
+ "Streaming updates are coalesced to one parse and render per animation frame."
38
+ ],
39
+ "responsiveBehavior": [
40
+ "Tables and code scroll horizontally instead of forcing the transcript wider."
41
+ ],
42
+ "references": [
43
+ {
44
+ "label": "Storybook examples",
45
+ "path": "src/wc/components/MessageScroller/MessageScroller.stories.ts"
46
+ }
47
+ ]
48
+ },
49
+ "api": {
50
+ "props": {
51
+ "content": {
52
+ "type": "string",
53
+ "resolvedType": "string",
54
+ "attribute": "content",
55
+ "default": "''",
56
+ "required": false,
57
+ "mutable": false
58
+ },
59
+ "streaming": {
60
+ "type": "boolean",
61
+ "resolvedType": "boolean",
62
+ "attribute": "streaming",
63
+ "default": "false",
64
+ "required": false,
65
+ "mutable": false
66
+ }
67
+ },
68
+ "events": [],
69
+ "methods": [],
70
+ "slots": []
71
+ },
72
+ "props": {
73
+ "content": {
74
+ "type": "string",
75
+ "resolvedType": "string",
76
+ "attribute": "content",
77
+ "default": "''",
78
+ "required": false,
79
+ "mutable": false
80
+ },
81
+ "streaming": {
82
+ "type": "boolean",
83
+ "resolvedType": "boolean",
84
+ "attribute": "streaming",
85
+ "default": "false",
86
+ "required": false,
87
+ "mutable": false
88
+ }
89
+ },
90
+ "events": [],
91
+ "methods": [],
92
+ "slots": [],
93
+ "exports": {
94
+ "customElement": "ds-markdown",
95
+ "react": "DsMarkdown",
96
+ "angular": "DsMarkdown"
97
+ },
98
+ "consumption": {
99
+ "install": "npm install @ds-mo/ui @ds-mo/tokens @ds-mo/icons",
100
+ "cssSetup": "import '@ds-mo/tokens';\nimport '@ds-mo/tokens/reset';\nimport '@ds-mo/tokens/globals';",
101
+ "customElements": {
102
+ "import": "import '@ds-mo/ui/dist/components/ds-markdown.js';",
103
+ "example": "<ds-markdown></ds-markdown>"
104
+ },
105
+ "react": {
106
+ "import": "import { DsMarkdown } from '@ds-mo/ui/react';",
107
+ "example": "<DsMarkdown />"
108
+ },
109
+ "angular": {
110
+ "import": "import { DsMarkdown } from '@ds-mo/ui/angular/ds-markdown';",
111
+ "example": "<ds-markdown></ds-markdown>"
112
+ },
113
+ "peerDependencies": {
114
+ "required": [
115
+ "@ds-mo/tokens ^3.1.1",
116
+ "@ds-mo/icons ^6.2.0"
117
+ ],
118
+ "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
119
+ }
120
+ }
121
+ },
122
+ "dependencies": [
123
+ "@ds-mo/ui",
124
+ "@ds-mo/tokens"
125
+ ],
126
+ "registryDependencies": [
127
+ "code-block"
128
+ ],
129
+ "files": [
130
+ {
131
+ "path": "src/wc/components/Markdown/Markdown.css",
132
+ "content": ":host {\n display: block;\n min-width: 0;\n color: inherit;\n box-sizing: border-box;\n}\n\n.markdown {\n min-width: 0;\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 overflow-wrap: anywhere;\n}\n\n.markdown > :first-child {\n margin-top: 0;\n}\n\n.markdown > :last-child {\n margin-bottom: 0;\n}\n\np,\nul,\nol,\nblockquote,\n.markdown__table-wrap,\nds-code-block {\n margin: 0 0 var(--dimension-space-150);\n}\n\nh1,\nh2,\nh3,\nh4,\nh5,\nh6 {\n margin: var(--dimension-space-200) 0 var(--dimension-space-100);\n color: var(--color-foreground-primary);\n font-family: var(--typography-font-family);\n font-weight: var(--typography-weight-semibold);\n}\n\nh1,\nh2 {\n font-size: var(--typography-fontsize-xl);\n line-height: var(--typography-lineheight-xl);\n}\n\nh3,\nh4,\nh5,\nh6 {\n font-size: var(--typography-fontsize-lg);\n line-height: var(--typography-lineheight-lg);\n}\n\nul,\nol {\n padding-left: var(--dimension-space-300);\n}\n\nli + li {\n margin-top: var(--dimension-space-050);\n}\n\nli > input {\n margin-right: var(--dimension-space-050);\n}\n\nblockquote {\n padding-left: var(--dimension-space-150);\n border-left: var(--dimension-stroke-width-025) solid var(--color-border-secondary);\n color: var(--color-foreground-secondary);\n}\n\na {\n color: var(--color-foreground-brand);\n text-decoration: underline;\n}\n\ncode:not(ds-code-block code) {\n padding: var(--dimension-space-012) var(--dimension-space-025);\n border-radius: var(--dimension-radius-025);\n background: var(--color-background-secondary);\n font-family: var(--typography-fontfamily-mono);\n font-size: var(--typography-fontsize-sm);\n}\n\nhr {\n margin: var(--dimension-space-200) 0;\n border: none;\n border-top: var(--dimension-stroke-width-012) solid var(--color-border-tertiary);\n}\n\n.markdown__table-wrap {\n overflow-x: auto;\n}\n\ntable {\n width: 100%;\n border-collapse: collapse;\n}\n\nth,\ntd {\n padding: var(--dimension-space-075);\n border: var(--dimension-stroke-width-012) solid var(--color-border-tertiary);\n text-align: left;\n vertical-align: top;\n}\n\nth {\n font-weight: var(--typography-weight-semibold);\n}\n",
133
+ "type": "registry:ui"
134
+ },
135
+ {
136
+ "path": "src/wc/components/Markdown/Markdown.tsx",
137
+ "content": "import { Component, h, Host, Prop, State, VNode, Watch } from '@stencil/core';\nimport type { Nodes, Root } from 'mdast';\n\ntype MarkdownRender = VNode | string | null | MarkdownRender[];\ntype MarkdownParser = {\n fromMarkdown: typeof import('mdast-util-from-markdown').fromMarkdown;\n gfmFromMarkdown: typeof import('mdast-util-gfm').gfmFromMarkdown;\n gfm: typeof import('micromark-extension-gfm').gfm;\n};\n\nlet parserPromise: Promise<MarkdownParser> | undefined;\n\nfunction loadParser(): Promise<MarkdownParser> {\n parserPromise ??= Promise.all([\n import('mdast-util-from-markdown'),\n import('mdast-util-gfm'),\n import('micromark-extension-gfm'),\n ]).then(([fromMarkdownModule, gfmFromMarkdownModule, gfmModule]) => ({\n fromMarkdown: fromMarkdownModule.fromMarkdown,\n gfmFromMarkdown: gfmFromMarkdownModule.gfmFromMarkdown,\n gfm: gfmModule.gfm,\n }));\n return parserPromise;\n}\n\nfunction safeHref(value: string | null | undefined): string | undefined {\n if (!value) return undefined;\n try {\n const url = new URL(value, document.baseURI);\n if (!['http:', 'https:', 'mailto:'].includes(url.protocol)) return undefined;\n return url.href;\n } catch {\n return undefined;\n }\n}\n\n@Component({\n tag: 'ds-markdown',\n styleUrl: 'Markdown.css',\n scoped: true,\n})\nexport class Markdown {\n @Prop() content: string = '';\n @Prop() streaming: boolean = false;\n\n @State() private tree: Root | null = null;\n private parseFrame?: number;\n private parseRevision = 0;\n\n componentWillLoad() {\n // The browser-targeted named-character decoder used by the Markdown parser\n // creates a DOM element when its module is evaluated. Keep public package\n // imports server-safe by loading that parser only in a browser context.\n if (typeof document !== 'undefined') return this.parse();\n }\n\n disconnectedCallback() {\n this.parseRevision += 1;\n if (this.parseFrame && typeof cancelAnimationFrame === 'function') {\n cancelAnimationFrame(this.parseFrame);\n }\n }\n\n @Watch('content')\n scheduleParse() {\n if (typeof requestAnimationFrame !== 'function') return;\n if (this.parseFrame) cancelAnimationFrame(this.parseFrame);\n this.parseFrame = requestAnimationFrame(() => {\n this.parseFrame = undefined;\n void this.parse();\n });\n }\n\n private async parse() {\n const revision = ++this.parseRevision;\n try {\n const { fromMarkdown, gfmFromMarkdown, gfm } = await loadParser();\n const tree = fromMarkdown(this.content, {\n extensions: [gfm()],\n mdastExtensions: [gfmFromMarkdown()],\n });\n if (revision === this.parseRevision) this.tree = tree;\n } catch {\n if (revision === this.parseRevision) this.tree = null;\n }\n }\n\n private children(node: Nodes): MarkdownRender[] {\n if (!('children' in node)) return [];\n return node.children.map(child => this.renderNode(child));\n }\n\n private renderNode(node: Nodes): MarkdownRender {\n switch (node.type) {\n case 'root':\n return this.children(node);\n case 'text':\n // Keep prose text on an element boundary. Besides giving streaming updates a\n // stable render target, this avoids axe-core treating a bare Stencil text\n // vnode as an element while it generates a contrast-result selector.\n return <span class=\"markdown__text\">{node.value}</span>;\n case 'paragraph':\n return <p class=\"markdown__paragraph\">{this.children(node)}</p>;\n case 'heading': {\n const Tag = `h${node.depth}`;\n return <Tag class=\"markdown__heading\">{this.children(node)}</Tag>;\n }\n case 'strong':\n return <strong class=\"markdown__strong\">{this.children(node)}</strong>;\n case 'emphasis':\n return <em class=\"markdown__emphasis\">{this.children(node)}</em>;\n case 'delete':\n return <del class=\"markdown__delete\">{this.children(node)}</del>;\n case 'blockquote':\n return <blockquote class=\"markdown__blockquote\">{this.children(node)}</blockquote>;\n case 'break':\n return <br class=\"markdown__break\" />;\n case 'thematicBreak':\n return <hr class=\"markdown__thematic-break\" />;\n case 'inlineCode':\n return <code class=\"markdown__inline-code\">{node.value}</code>;\n case 'code':\n return (\n <ds-code-block\n class=\"markdown__code-block\"\n code={node.value}\n language={node.lang ?? ''}\n filename={node.meta ?? ''}\n />\n );\n case 'link': {\n const href = safeHref(node.url);\n return href ? (\n <a class=\"markdown__link\" href={href} target=\"_blank\" rel=\"noopener noreferrer\">\n {this.children(node)}\n </a>\n ) : (\n this.children(node)\n );\n }\n case 'list': {\n const Tag = node.ordered ? 'ol' : 'ul';\n return (\n <Tag class=\"markdown__list\" start={node.ordered && node.start ? node.start : undefined}>\n {this.children(node)}\n </Tag>\n );\n }\n case 'listItem':\n return (\n <li class=\"markdown__list-item\">\n {typeof node.checked === 'boolean' ? (\n <input\n class=\"markdown__task-indicator\"\n type=\"checkbox\"\n checked={node.checked}\n disabled\n aria-hidden=\"true\"\n tabIndex={-1}\n />\n ) : null}\n {this.children(node)}\n </li>\n );\n case 'table': {\n const [heading, ...rows] = node.children;\n return (\n <div class=\"markdown__table-wrap\">\n <table class=\"markdown__table\">\n {heading ? (\n <thead class=\"markdown__table-head\">\n <tr class=\"markdown__table-row\">\n {heading.children.map(cell => (\n <th class=\"markdown__table-heading\" scope=\"col\">\n {this.children(cell)}\n </th>\n ))}\n </tr>\n </thead>\n ) : null}\n <tbody class=\"markdown__table-body\">{rows.map(row => this.renderNode(row))}</tbody>\n </table>\n </div>\n );\n }\n case 'tableRow':\n return <tr class=\"markdown__table-row\">{this.children(node)}</tr>;\n case 'tableCell':\n return <td class=\"markdown__table-cell\">{this.children(node)}</td>;\n case 'image': {\n const href = safeHref(node.url);\n return href ? (\n <a class=\"markdown__image-link\" href={href} target=\"_blank\" rel=\"noopener noreferrer\">\n {node.alt || 'View image'}\n </a>\n ) : (\n node.alt || null\n );\n }\n case 'html':\n case 'definition':\n case 'footnoteDefinition':\n return null;\n case 'footnoteReference':\n return <sup class=\"markdown__footnote\">{node.label}</sup>;\n default:\n return this.children(node);\n }\n }\n\n render() {\n return (\n <Host aria-busy={this.streaming ? 'true' : undefined}>\n <div class=\"markdown\">\n {this.tree ? (\n this.renderNode(this.tree)\n ) : (\n <p class=\"markdown__paragraph\">\n <span class=\"markdown__text\">{this.content}</span>\n </p>\n )}\n </div>\n </Host>\n );\n }\n}\n",
138
+ "type": "registry:ui"
139
+ }
140
+ ]
141
+ }
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
3
  "name": "menu",
4
4
  "title": "Menu",
5
- "description": "Anchored action menu with flat commands, grouped sections, selected command state, supplemental notification dots, switches, and destructive actions.",
5
+ "description": "Anchored action menu with flat commands, grouped sections, single-selection radio choices, supplemental notification dots, switches, and destructive actions.",
6
6
  "type": "registry:ui",
7
7
  "meta": {
8
8
  "library": "@ds-mo/ui",
@@ -13,10 +13,10 @@
13
13
  "intent": {
14
14
  "audience": "general",
15
15
  "status": "stable",
16
- "summary": "Anchored action menu with flat commands, grouped sections, selected command state, supplemental notification dots, switches, and destructive actions.",
16
+ "summary": "Anchored action menu with flat commands, grouped sections, single-selection radio choices, supplemental notification dots, switches, and destructive actions.",
17
17
  "useWhen": [
18
18
  "A trigger reveals a short contextual list of commands or state-changing actions.",
19
- "Actions need grouping, selected state, supplemental notification dots, switch rows, or destructive treatment."
19
+ "Actions need grouping, mutually exclusive selected state, supplemental notification dots, switch rows, or destructive treatment."
20
20
  ],
21
21
  "avoidWhen": [
22
22
  "The user is selecting a value for a form field.",
@@ -54,6 +54,7 @@
54
54
  "Open with visible initial focus only when the trigger was activated from the keyboard.",
55
55
  "Keep disabled items discoverable only when their unavailable state is meaningful.",
56
56
  "Switch rows use menuitemcheckbox semantics on the row; the nested ds-switch is an aria-hidden md presentation indicator.",
57
+ "Single-selection menus use menuitemradio semantics and aria-checked on each row; the leading radio mark is presentation-only.",
57
58
  "A swatch-picker section changes the popup to a non-modal dialog so its radio group and ordinary preference buttons keep valid nested semantics; Tab moves between its controls, then leaves and closes at the popup boundary.",
58
59
  "Use notification dots only as supplemental status and not as the sole indication of important information."
59
60
  ],
@@ -61,6 +62,7 @@
61
62
  "The host owns open state and responds to selection and close events.",
62
63
  "Use either flat items or sections, not both.",
63
64
  "After dsSelect from a switch row, update switchValue in the items property so the controlled indicator reflects the new state.",
65
+ "For selectionMode single, keep exactly one item isSelected and update the items property after dsSelect.",
64
66
  "Arrays, objects, and element anchors must be assigned as JavaScript properties.",
65
67
  "Use a swatch-picker section for a small curated visual preference and apply dsSwatchSelect immediately without closing the popup; the legacy gradient-picker section and dsGradientSelect remain compatibility-only."
66
68
  ],
@@ -188,6 +190,15 @@
188
190
  "required": false,
189
191
  "mutable": false
190
192
  },
193
+ "selectionMode": {
194
+ "type": "MenuSelectionMode",
195
+ "resolvedType": "\"none\" | \"single\"",
196
+ "attribute": "selection-mode",
197
+ "default": "'none'",
198
+ "required": false,
199
+ "mutable": false,
200
+ "description": "Give ordinary rows mutually-exclusive radio-menu semantics using isSelected."
201
+ },
191
202
  "side": {
192
203
  "type": "MenuSide",
193
204
  "resolvedType": "\"bottom\" | \"left\" | \"right\" | \"top\"",
@@ -337,6 +348,15 @@
337
348
  "required": false,
338
349
  "mutable": false
339
350
  },
351
+ "selectionMode": {
352
+ "type": "MenuSelectionMode",
353
+ "resolvedType": "\"none\" | \"single\"",
354
+ "attribute": "selection-mode",
355
+ "default": "'none'",
356
+ "required": false,
357
+ "mutable": false,
358
+ "description": "Give ordinary rows mutually-exclusive radio-menu semantics using isSelected."
359
+ },
340
360
  "side": {
341
361
  "type": "MenuSide",
342
362
  "resolvedType": "\"bottom\" | \"left\" | \"right\" | \"top\"",
@@ -432,17 +452,17 @@
432
452
  "files": [
433
453
  {
434
454
  "path": "src/wc/components/Menu/Menu.css",
435
- "content": "@import '../../utils/choice-list.css';\n\n/* Inactive: `.ds-control-inactive` (25% opacity). */\n\n.menu-item--destructive .menu-item__label {\n /* Beat ds-text color-* host classes for destructive rows. */\n color: var(--color-foreground-bold-negative) !important;\n}\n\n/* Standard md icon zone keeps the dot aligned with other control suffixes. */\n.menu-item__dot-box {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--ds-control-icon, var(--dimension-iconography-md));\n height: var(--ds-control-icon, var(--dimension-iconography-md));\n pointer-events: none;\n}\n\n/* ── Switch suffix ───────────────────────────────────────────────────────── */\n\n.menu-item__switch {\n flex-shrink: 0;\n}\n",
455
+ "content": "@import '../../utils/choice-list.css';\n\n/* Inactive: `.ds-control-inactive` (25% opacity). */\n\n.menu-item--destructive .menu-item__label {\n /* Beat ds-text color-* host classes for destructive rows. */\n color: var(--color-foreground-bold-negative) !important;\n}\n\n/* Standard md icon zone keeps the dot aligned with other control suffixes. */\n.menu-item__dot-box {\n display: flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--ds-control-icon, var(--dimension-iconography-md));\n height: var(--ds-control-icon, var(--dimension-iconography-md));\n pointer-events: none;\n}\n\n/* ── Switch suffix ───────────────────────────────────────────────────────── */\n\n.menu-item__switch {\n flex-shrink: 0;\n}\n\n/* ── Single-selection radio indicator ───────────────────────────────────── */\n\n.menu-item__radio-box {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--ds-control-icon, var(--dimension-iconography-md));\n height: var(--ds-control-icon, var(--dimension-iconography-md));\n pointer-events: none;\n}\n\n.menu-item__radio {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: var(--dimension-iconography-sm);\n height: var(--dimension-iconography-sm);\n border-radius: var(--dimension-radius-half);\n box-shadow: inset 0 0 0 var(--dimension-stroke-width-015) var(--color-foreground-tertiary);\n}\n\n.menu-item__radio--checked {\n background-color: var(--color-background-bold-brand);\n box-shadow: none;\n}\n\n.menu-item__radio-dot {\n width: var(--dimension-size-100);\n height: var(--dimension-size-100);\n border-radius: var(--dimension-radius-half);\n background-color: var(--color-foreground-on-bold-background-primary);\n}\n",
436
456
  "type": "registry:ui"
437
457
  },
438
458
  {
439
459
  "path": "src/wc/components/Menu/Menu.tsx",
440
- "content": "import { Component, Prop, State, Event, EventEmitter, Element, Watch, Listen, h, Host } from '@stencil/core';\nimport {\n choicePopupMinWidth,\n resolveChoicePopupAlignOffset,\n resolveCssLengthPx,\n resolveMotionTimeMs,\n TOKEN_CSS_LENGTHS,\n TOKEN_DEFAULTS,\n type ChoicePopupAnchorAlignment,\n} from '../../utils';\nimport { computeMenuPosition, type MenuAlign, type MenuSide } from './menu-position';\nimport type { MenuItemData, MenuSection } from './menu-types';\nimport {\n isMenuGradientPickerSection,\n isMenuPickerSection,\n isMenuSwatchPickerSection,\n} from './menu-types';\nimport {\n shellGradientPickerSections,\n type ShellGradientPreset,\n} from '../../shell/shell-gradient-presets';\n\n/** rAF retries while the popup mounts or the anchor resolves. */\nconst POSITION_RETRY_BUDGET = 8;\nconst MENU_FOCUSABLE_SELECTOR = [\n 'a[href]',\n 'button:not([disabled])',\n 'input:not([disabled]):not([type=\"hidden\"])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(',');\n\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 @Prop() sections: MenuSection[] = [];\n @Prop() side: MenuSide = 'bottom';\n @Prop() align: MenuAlign = 'start';\n /** Align choice-row edges to the anchor by default; use `popup-frame` only for custom frame geometry. */\n @Prop() anchorAlignment: ChoicePopupAnchorAlignment = 'choice-cell';\n /** Gap between anchor and menu — number (px) or TokoMo length (`var(--dimension-space-050)`, etc.). */\n @Prop() sideOffset: number | string = TOKEN_CSS_LENGTHS.space050;\n /** Cross-axis offset — number (px) or TokoMo length. */\n @Prop() alignOffset: number | string = 0;\n @Prop() menuWidth: string | undefined;\n @Prop() minWidth: string | undefined;\n /** External trigger element to position against. Set via JS: menuEl.anchor = buttonEl */\n @Prop() anchor: HTMLElement | undefined;\n /** ID of the external trigger element for positioning */\n @Prop() anchorId: string | undefined;\n /** Show a visible ring on the initially focused menu item. Use only when the opener was keyboard-driven. */\n @Prop() initialFocusVisible: boolean = false;\n /** Accessible name for the popup menu. */\n @Prop() menuLabel: string = 'Menu';\n\n @State() private shouldRender: boolean = false;\n @State() private closing: boolean = false;\n @State() private pos: { x: number; y: number } = { x: 0, y: 0 };\n @State() private focusedIndex: number = 0;\n @State() private positionReady: boolean = false;\n @State() private focusRingVisible: boolean = false;\n\n @Event() dsClose!: EventEmitter<void>;\n @Event() dsSelect!: EventEmitter<MenuItemData>;\n /** Emitted when a `gradient-picker` section swatch is chosen. */\n @Event() dsGradientSelect!: EventEmitter<ShellGradientPreset>;\n /** Emitted when a generic `swatch-picker` section option is chosen. */\n @Event() dsSwatchSelect!: EventEmitter<string>;\n\n private clickOutsideHandler: ((e: MouseEvent) => void) | null = null;\n private scrollResizeHandler: (() => void) | null = null;\n private closeTimer: ReturnType<typeof setTimeout> | null = null;\n private itemEls: HTMLElement[] = [];\n private positionRetryRaf: number | null = null;\n private listenersReady = false;\n\n componentDidLoad() {\n if (this.open) this.onOpenChange(true);\n }\n\n disconnectedCallback() {\n this.cancelPositionRetry();\n this.teardownListeners();\n }\n\n @Watch('open')\n onOpenChange(isOpen: boolean) {\n if (isOpen) {\n this.teardownListeners();\n this.shouldRender = true;\n this.closing = false;\n this.positionReady = false;\n this.listenersReady = false;\n this.focusRingVisible = this.initialFocusVisible;\n this.listenersReady = true;\n this.setupListeners();\n this.schedulePositionUpdate(() => {\n this.focusInitialItem();\n });\n } else if (this.shouldRender) {\n this.cancelPositionRetry();\n this.closing = true;\n this.teardownListeners();\n this.listenersReady = false;\n const closeAnimationMs = this.closeAnimationMs;\n if (closeAnimationMs <= 0) {\n this.finishClose();\n return;\n }\n this.closeTimer = setTimeout(() => {\n this.closeTimer = null;\n this.finishClose();\n }, closeAnimationMs);\n }\n }\n\n @Watch('anchor')\n @Watch('anchorId')\n onAnchorChange() {\n if (this.open) this.schedulePositionUpdate();\n }\n\n @Watch('side')\n @Watch('align')\n @Watch('anchorAlignment')\n @Watch('sideOffset')\n @Watch('alignOffset')\n onPositionPropsChange() {\n if (this.open) this.schedulePositionUpdate();\n }\n\n private get viewportPadPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n }\n\n private get sideOffsetPx(): number {\n return resolveCssLengthPx(this.sideOffset, TOKEN_DEFAULTS.space050);\n }\n\n private get alignOffsetPx(): number {\n return resolveCssLengthPx(this.alignOffset, 0);\n }\n\n private get popupFallbackWidthPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuWidthXs, TOKEN_DEFAULTS.menuWidthXs);\n }\n\n private get popupFallbackHeightPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuFallbackHeight, TOKEN_DEFAULTS.menuFallbackHeight);\n }\n\n private get closeAnimationMs(): number {\n return resolveMotionTimeMs(TOKEN_DEFAULTS.motionShort2, TOKEN_DEFAULTS.animationDurationShort3);\n }\n\n private finishClose() {\n this.shouldRender = false;\n this.closing = false;\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 (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(document.querySelectorAll<HTMLElement>(MENU_FOCUSABLE_SELECTOR))\n .filter(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(popup.querySelectorAll<HTMLElement>(MENU_FOCUSABLE_SELECTOR))\n .filter(element => element.getClientRects().length > 0);\n const currentIndex = focusables.indexOf(event.target as HTMLElement);\n if (currentIndex < 0) return true;\n return event.shiftKey ? currentIndex === 0 : currentIndex === focusables.length - 1;\n }\n\n private get activeSections(): MenuSection[] {\n if (this.sections.length > 0) return this.sections;\n if (this.items.length > 0) return [{ items: this.items }];\n return [];\n }\n\n private get flatItems(): MenuItemData[] {\n return this.activeSections.flatMap(section =>\n isMenuPickerSection(section) ? [] : section.items,\n );\n }\n\n /** Rich preference content is a non-modal dialog, not an ARIA action menu. */\n private get hasCompositeSections(): boolean {\n return this.activeSections.some(isMenuPickerSection);\n }\n\n private cancelPositionRetry() {\n if (this.positionRetryRaf !== null) {\n cancelAnimationFrame(this.positionRetryRaf);\n this.positionRetryRaf = null;\n }\n }\n\n /** Retry until anchor + popup exist — do not reveal at 0,0 on a failed first pass. */\n private schedulePositionUpdate(onReady?: () => void) {\n if (!this.open) return;\n\n this.cancelPositionRetry();\n this.positionReady = false;\n\n let remaining = POSITION_RETRY_BUDGET;\n\n const attempt = () => {\n this.positionRetryRaf = null;\n if (!this.open) return;\n\n if (this.calculatePosition()) {\n this.positionReady = true;\n onReady?.();\n return;\n }\n\n if (remaining > 0) {\n remaining -= 1;\n this.positionRetryRaf = requestAnimationFrame(attempt);\n }\n };\n\n this.positionRetryRaf = requestAnimationFrame(attempt);\n }\n\n /** @returns `true` when anchor and popup were found and `pos` was updated. */\n private calculatePosition(): boolean {\n const anchorEl = this.resolvedAnchor;\n if (!anchorEl) return false;\n const popup = this.el.querySelector('.menu-popup') as HTMLElement | null;\n if (!popup) return false;\n\n const anchorRect = anchorEl.getBoundingClientRect();\n const sectionInsetPx = this.viewportPadPx;\n if (!this.minWidth) {\n if (this.anchorAlignment === 'choice-cell' && (this.side === 'top' || this.side === 'bottom')) {\n popup.style.minWidth = `max(var(--dimension-menu-width-xs), ${choicePopupMinWidth(anchorRect.width, sectionInsetPx)}px)`;\n } else {\n popup.style.removeProperty('min-width');\n }\n }\n\n this.pos = computeMenuPosition({\n anchorRect,\n popupWidth: popup.offsetWidth || this.popupFallbackWidthPx,\n popupHeight: popup.offsetHeight || this.popupFallbackHeightPx,\n side: this.side,\n align: this.align,\n sideOffsetPx: this.sideOffsetPx,\n alignOffsetPx: resolveChoicePopupAlignOffset({\n align: this.align,\n alignOffsetPx: this.alignOffsetPx,\n sectionInsetPx,\n anchorAlignment: this.anchorAlignment,\n }),\n viewportPadPx: this.viewportPadPx,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n });\n return true;\n }\n\n /** Focus the selected item when present, otherwise the first enabled item. */\n private focusInitialItem() {\n if (this.hasCompositeSections) {\n requestAnimationFrame(() => {\n this.el.querySelector<HTMLElement>('.menu-popup [tabindex=\"0\"]')?.focus();\n });\n return;\n }\n\n const flat = this.flatItems;\n const selectedIdx = flat.findIndex(it => it.isSelected && !it.isInactive);\n const firstEnabledIdx = flat.findIndex(it => !it.isInactive);\n this.focusedIndex = selectedIdx >= 0\n ? selectedIdx\n : (firstEnabledIdx >= 0 ? firstEnabledIdx : 0);\n\n requestAnimationFrame(() => {\n const btns = this.el.querySelectorAll<HTMLElement>('.menu-item');\n btns[this.focusedIndex]?.focus();\n });\n }\n\n private setupListeners() {\n this.clickOutsideHandler = (e: MouseEvent) => {\n const t = e.target as Node;\n const popup = this.el.querySelector('.menu-popup');\n const anchorEl = this.resolvedAnchor;\n if (popup?.contains(t) || anchorEl?.contains(t)) return;\n this.close();\n };\n\n this.scrollResizeHandler = () => {\n if (this.shouldRender && !this.closing) this.calculatePosition();\n };\n\n document.addEventListener('mousedown', this.clickOutsideHandler, true);\n window.addEventListener('scroll', this.scrollResizeHandler, true);\n window.addEventListener('resize', this.scrollResizeHandler);\n }\n\n private teardownListeners() {\n if (this.clickOutsideHandler) {\n document.removeEventListener('mousedown', this.clickOutsideHandler, true);\n this.clickOutsideHandler = null;\n }\n if (this.scrollResizeHandler) {\n window.removeEventListener('scroll', this.scrollResizeHandler, true);\n window.removeEventListener('resize', this.scrollResizeHandler);\n this.scrollResizeHandler = null;\n }\n if (this.closeTimer) {\n clearTimeout(this.closeTimer);\n this.closeTimer = null;\n }\n }\n\n private close(restoreFocus = true) {\n 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.map((it, i) => ({ it, i })).filter(({ it }) => !it.isInactive).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(); 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(); 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(); e.stopPropagation();\n this.focusRingVisible = true;\n this.focusedIndex = enabled[0];\n this.focusItem(this.focusedIndex);\n break;\n case 'End':\n e.preventDefault(); 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 this.dsSelect.emit(item);\n }\n\n private handleGradientSelect(preset: ShellGradientPreset) {\n this.dsGradientSelect.emit(preset);\n }\n\n private handleSwatchSelect(value: string) {\n this.dsSwatchSelect.emit(value);\n }\n\n render() {\n if (!this.shouldRender) return <Host style={{ display: 'contents' }} />;\n\n const sections = this.activeSections;\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 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={this.hasCompositeSections ? 'dialog' : 'menu'}\n aria-label={this.menuLabel}\n aria-orientation={this.hasCompositeSections ? undefined : 'vertical'}\n >\n <div class=\"ds-choice-list\">\n {sections.map((section, si) => (\n <div\n key={si}\n class={{\n 'menu-section': true,\n 'menu-section--divided': si < sections.length - 1,\n 'menu-section--gradient-picker': isMenuPickerSection(section),\n 'ds-choice-section': true,\n 'ds-choice-section--divided': si < sections.length - 1,\n }}\n role={section.header ? 'group' : undefined}\n aria-label={section.header}\n >\n {section.header && (\n <ds-text\n class=\"section-header ds-choice-section__header ds-control--md\"\n as=\"span\"\n variant=\"text-body-small\"\n emphasis\n color=\"primary\"\n aria-hidden=\"true\"\n >\n {section.header}\n </ds-text>\n )}\n {isMenuGradientPickerSection(section) ? (\n <ds-swatch-picker\n value={section.value}\n groupLabel={section.header ?? 'Shell gradient theme'}\n sections={shellGradientPickerSections()}\n onDsChange={(e: CustomEvent<string>) => {\n e.stopPropagation();\n this.handleGradientSelect(e.detail as ShellGradientPreset);\n }}\n />\n ) : isMenuSwatchPickerSection(section) ? (\n <ds-swatch-picker\n value={section.value}\n groupLabel={section.groupLabel ?? section.header ?? 'Swatch options'}\n options={section.options ?? []}\n sections={section.sections ?? []}\n onDsChange={(e: CustomEvent<string>) => {\n e.stopPropagation();\n this.handleSwatchSelect(e.detail);\n }}\n />\n ) : (\n section.items.map(item => {\n const idx = flatIdx++;\n const isFocused = this.focusedIndex === idx;\n return (\n <button\n key={idx}\n type=\"button\"\n class={{\n 'menu-item': true,\n 'ds-choice-item': true,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-focus-ring--visible': isFocused && this.focusRingVisible,\n 'ds-interaction-fill': !item.isInactive,\n 'ds-interaction-fill--selected': !!item.isSelected && !item.isInactive,\n 'menu-item--selected': !!item.isSelected,\n 'ds-control-inactive': !!item.isInactive,\n 'menu-item--destructive': !!item.isDestructive,\n 'menu-item--focused': isFocused,\n }}\n role={this.hasCompositeSections ? undefined : item.showSwitch ? 'menuitemcheckbox' : 'menuitem'}\n aria-checked={!this.hasCompositeSections && item.showSwitch ? String(!!item.switchValue) : undefined}\n aria-pressed={this.hasCompositeSections\n ? String(item.showSwitch ? !!item.switchValue : !!item.isSelected)\n : undefined}\n aria-current={!this.hasCompositeSections && !item.showSwitch && item.isSelected ? 'true' : undefined}\n disabled={item.isInactive}\n tabIndex={this.hasCompositeSections ? 0 : isFocused ? 0 : -1}\n onMouseDown={() => { this.focusRingVisible = false; }}\n onClick={() => this.handleItemClick(item)}\n onFocus={() => { this.focusedIndex = idx; }}\n >\n <div class=\"menu-item__content ds-choice-item__content ds-interaction-fill__content\">\n <ds-text\n class=\"menu-item__label ds-choice-item__label\"\n as=\"span\"\n variant=\"text-body-medium\"\n color={item.isSelected ? 'primary' : 'secondary'}\n >\n {item.label}\n </ds-text>\n {item.subtext && (\n <ds-text class=\"menu-item__subtext ds-choice-item__subtext\" as=\"span\" variant=\"text-body-small\" color=\"secondary\">\n {item.subtext}\n </ds-text>\n )}\n </div>\n {item.dot && (\n <span class=\"menu-item__dot-box ds-interaction-fill__content\" aria-hidden=\"true\">\n <ds-badge\n class=\"menu-item__dot\"\n variant=\"dot\"\n hasRing={false}\n label=\"\"\n />\n </span>\n )}\n {item.showSwitch && (\n <ds-switch\n class=\"menu-item__switch ds-interaction-fill__content\"\n size=\"md\"\n checked={!!item.switchValue}\n presentation\n />\n )}\n </button>\n );\n }))}\n </div>\n ))}\n </div>\n </div>\n </Host>\n );\n }\n}\n",
460
+ "content": "import {\n Component,\n Prop,\n State,\n Event,\n EventEmitter,\n Element,\n Watch,\n Listen,\n h,\n Host,\n} from '@stencil/core';\nimport {\n choicePopupMinWidth,\n resolveChoicePopupAlignOffset,\n resolveCssLengthPx,\n resolveMotionTimeMs,\n TOKEN_CSS_LENGTHS,\n TOKEN_DEFAULTS,\n type ChoicePopupAnchorAlignment,\n} from '../../utils';\nimport { computeMenuPosition, type MenuAlign, type MenuSide } from './menu-position';\nimport type { MenuItemData, MenuSection } from './menu-types';\n\nexport type MenuSelectionMode = 'none' | 'single';\nimport {\n isMenuGradientPickerSection,\n isMenuPickerSection,\n isMenuSwatchPickerSection,\n} from './menu-types';\nimport {\n shellGradientPickerSections,\n type ShellGradientPreset,\n} from '../../shell/shell-gradient-presets';\n\n/** rAF retries while the popup mounts or the anchor resolves. */\nconst POSITION_RETRY_BUDGET = 8;\nconst MENU_FOCUSABLE_SELECTOR = [\n 'a[href]',\n 'button:not([disabled])',\n 'input:not([disabled]):not([type=\"hidden\"])',\n 'select:not([disabled])',\n 'textarea:not([disabled])',\n '[tabindex]:not([tabindex=\"-1\"])',\n].join(',');\n\n@Component({\n tag: 'ds-menu',\n styleUrl: 'Menu.css',\n scoped: true,\n})\nexport class Menu {\n @Element() el!: HTMLElement;\n\n @Prop({ mutable: true }) open: boolean = false;\n @Prop() items: MenuItemData[] = [];\n /** Give ordinary rows mutually-exclusive radio-menu semantics using isSelected. */\n @Prop() selectionMode: MenuSelectionMode = 'none';\n @Prop() sections: MenuSection[] = [];\n @Prop() side: MenuSide = 'bottom';\n @Prop() align: MenuAlign = 'start';\n /** Align choice-row edges to the anchor by default; use `popup-frame` only for custom frame geometry. */\n @Prop() anchorAlignment: ChoicePopupAnchorAlignment = 'choice-cell';\n /** Gap between anchor and menu — number (px) or TokoMo length (`var(--dimension-space-050)`, etc.). */\n @Prop() sideOffset: number | string = TOKEN_CSS_LENGTHS.space050;\n /** Cross-axis offset — number (px) or TokoMo length. */\n @Prop() alignOffset: number | string = 0;\n @Prop() menuWidth: string | undefined;\n @Prop() minWidth: string | undefined;\n /** External trigger element to position against. Set via JS: menuEl.anchor = buttonEl */\n @Prop() anchor: HTMLElement | undefined;\n /** ID of the external trigger element for positioning */\n @Prop() anchorId: string | undefined;\n /** Show a visible ring on the initially focused menu item. Use only when the opener was keyboard-driven. */\n @Prop() initialFocusVisible: boolean = false;\n /** Accessible name for the popup menu. */\n @Prop() menuLabel: string = 'Menu';\n\n @State() private shouldRender: boolean = false;\n @State() private closing: boolean = false;\n @State() private pos: { x: number; y: number } = { x: 0, y: 0 };\n @State() private focusedIndex: number = 0;\n @State() private positionReady: boolean = false;\n @State() private focusRingVisible: boolean = false;\n\n @Event() dsClose!: EventEmitter<void>;\n @Event() dsSelect!: EventEmitter<MenuItemData>;\n /** Emitted when a `gradient-picker` section swatch is chosen. */\n @Event() dsGradientSelect!: EventEmitter<ShellGradientPreset>;\n /** Emitted when a generic `swatch-picker` section option is chosen. */\n @Event() dsSwatchSelect!: EventEmitter<string>;\n\n private clickOutsideHandler: ((e: MouseEvent) => void) | null = null;\n private scrollResizeHandler: (() => void) | null = null;\n private closeTimer: ReturnType<typeof setTimeout> | null = null;\n private itemEls: HTMLElement[] = [];\n private positionRetryRaf: number | null = null;\n private listenersReady = false;\n\n componentDidLoad() {\n if (this.open) this.onOpenChange(true);\n }\n\n disconnectedCallback() {\n this.cancelPositionRetry();\n this.teardownListeners();\n }\n\n @Watch('open')\n onOpenChange(isOpen: boolean) {\n if (isOpen) {\n this.teardownListeners();\n this.shouldRender = true;\n this.closing = false;\n this.positionReady = false;\n this.listenersReady = false;\n this.focusRingVisible = this.initialFocusVisible;\n this.listenersReady = true;\n this.setupListeners();\n this.schedulePositionUpdate(() => {\n this.focusInitialItem();\n });\n } else if (this.shouldRender) {\n this.cancelPositionRetry();\n this.closing = true;\n this.teardownListeners();\n this.listenersReady = false;\n const closeAnimationMs = this.closeAnimationMs;\n if (closeAnimationMs <= 0) {\n this.finishClose();\n return;\n }\n this.closeTimer = setTimeout(() => {\n this.closeTimer = null;\n this.finishClose();\n }, closeAnimationMs);\n }\n }\n\n @Watch('anchor')\n @Watch('anchorId')\n onAnchorChange() {\n if (this.open) this.schedulePositionUpdate();\n }\n\n @Watch('side')\n @Watch('align')\n @Watch('anchorAlignment')\n @Watch('sideOffset')\n @Watch('alignOffset')\n onPositionPropsChange() {\n if (this.open) this.schedulePositionUpdate();\n }\n\n private get viewportPadPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n }\n\n private get sideOffsetPx(): number {\n return resolveCssLengthPx(this.sideOffset, TOKEN_DEFAULTS.space050);\n }\n\n private get alignOffsetPx(): number {\n return resolveCssLengthPx(this.alignOffset, 0);\n }\n\n private get popupFallbackWidthPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuWidthXs, TOKEN_DEFAULTS.menuWidthXs);\n }\n\n private get popupFallbackHeightPx(): number {\n return resolveCssLengthPx(TOKEN_DEFAULTS.menuFallbackHeight, TOKEN_DEFAULTS.menuFallbackHeight);\n }\n\n private get closeAnimationMs(): number {\n return resolveMotionTimeMs(TOKEN_DEFAULTS.motionShort2, TOKEN_DEFAULTS.animationDurationShort3);\n }\n\n private finishClose() {\n this.shouldRender = false;\n this.closing = false;\n }\n\n private get resolvedAnchor(): HTMLElement | null {\n if (this.anchor) return this.anchor;\n if (this.anchorId) return document.getElementById(this.anchorId);\n return null;\n }\n\n private get anchorFocusTarget(): HTMLElement | null {\n const anchor = this.resolvedAnchor;\n if (!anchor) return null;\n if (anchor.matches(MENU_FOCUSABLE_SELECTOR)) return anchor;\n return anchor.querySelector<HTMLElement>(MENU_FOCUSABLE_SELECTOR) ?? anchor;\n }\n\n private focusAnchor() {\n const anchor = this.resolvedAnchor as\n | (HTMLElement & { setFocus?: () => Promise<void> | void })\n | null;\n if (anchor?.setFocus) {\n anchor.setFocus();\n return;\n }\n this.anchorFocusTarget?.focus();\n }\n\n private moveFocusAfterTab(backwards: boolean) {\n const anchor = this.anchorFocusTarget;\n if (!anchor) return;\n if (backwards) {\n anchor.focus();\n return;\n }\n\n const candidates = Array.from(\n document.querySelectorAll<HTMLElement>(MENU_FOCUSABLE_SELECTOR)\n ).filter(\n element =>\n !element.closest('.menu-popup') &&\n !element.closest('[inert]') &&\n element.getClientRects().length > 0\n );\n const index = candidates.indexOf(anchor);\n (candidates[index + 1] ?? anchor).focus();\n }\n\n private compositeTabLeavesPopup(event: KeyboardEvent): boolean {\n const popup = this.el.querySelector<HTMLElement>('.menu-popup');\n if (!popup) return true;\n const focusables = Array.from(\n popup.querySelectorAll<HTMLElement>(MENU_FOCUSABLE_SELECTOR)\n ).filter(element => element.getClientRects().length > 0);\n const currentIndex = focusables.indexOf(event.target as HTMLElement);\n if (currentIndex < 0) return true;\n return event.shiftKey ? currentIndex === 0 : currentIndex === focusables.length - 1;\n }\n\n private get activeSections(): MenuSection[] {\n if (this.sections.length > 0) return this.sections;\n if (this.items.length > 0) return [{ items: this.items }];\n return [];\n }\n\n private get flatItems(): MenuItemData[] {\n return this.activeSections.flatMap(section =>\n isMenuPickerSection(section) ? [] : section.items\n );\n }\n\n /** Rich preference content is a non-modal dialog, not an ARIA action menu. */\n private get hasCompositeSections(): boolean {\n return this.activeSections.some(isMenuPickerSection);\n }\n\n private cancelPositionRetry() {\n if (this.positionRetryRaf !== null) {\n cancelAnimationFrame(this.positionRetryRaf);\n this.positionRetryRaf = null;\n }\n }\n\n /** Retry until anchor + popup exist — do not reveal at 0,0 on a failed first pass. */\n private schedulePositionUpdate(onReady?: () => void) {\n if (!this.open) return;\n\n this.cancelPositionRetry();\n this.positionReady = false;\n\n let remaining = POSITION_RETRY_BUDGET;\n\n const attempt = () => {\n this.positionRetryRaf = null;\n if (!this.open) return;\n\n if (this.calculatePosition()) {\n this.positionReady = true;\n onReady?.();\n return;\n }\n\n if (remaining > 0) {\n remaining -= 1;\n this.positionRetryRaf = requestAnimationFrame(attempt);\n }\n };\n\n this.positionRetryRaf = requestAnimationFrame(attempt);\n }\n\n /** @returns `true` when anchor and popup were found and `pos` was updated. */\n private calculatePosition(): boolean {\n const anchorEl = this.resolvedAnchor;\n if (!anchorEl) return false;\n const popup = this.el.querySelector('.menu-popup') as HTMLElement | null;\n if (!popup) return false;\n\n const anchorRect = anchorEl.getBoundingClientRect();\n const sectionInsetPx = this.viewportPadPx;\n if (!this.minWidth) {\n if (\n this.anchorAlignment === 'choice-cell' &&\n (this.side === 'top' || this.side === 'bottom')\n ) {\n popup.style.minWidth = `max(var(--dimension-menu-width-xs), ${choicePopupMinWidth(\n anchorRect.width,\n sectionInsetPx\n )}px)`;\n } else {\n popup.style.removeProperty('min-width');\n }\n }\n\n this.pos = computeMenuPosition({\n anchorRect,\n popupWidth: popup.offsetWidth || this.popupFallbackWidthPx,\n popupHeight: popup.offsetHeight || this.popupFallbackHeightPx,\n side: this.side,\n align: this.align,\n sideOffsetPx: this.sideOffsetPx,\n alignOffsetPx: resolveChoicePopupAlignOffset({\n align: this.align,\n alignOffsetPx: this.alignOffsetPx,\n sectionInsetPx,\n anchorAlignment: this.anchorAlignment,\n }),\n viewportPadPx: this.viewportPadPx,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n });\n return true;\n }\n\n /** Focus the selected item when present, otherwise the first enabled item. */\n private focusInitialItem() {\n if (this.hasCompositeSections) {\n requestAnimationFrame(() => {\n this.el.querySelector<HTMLElement>('.menu-popup [tabindex=\"0\"]')?.focus();\n });\n return;\n }\n\n const flat = this.flatItems;\n const selectedIdx = flat.findIndex(it => it.isSelected && !it.isInactive);\n const firstEnabledIdx = flat.findIndex(it => !it.isInactive);\n this.focusedIndex = selectedIdx >= 0 ? selectedIdx : firstEnabledIdx >= 0 ? firstEnabledIdx : 0;\n\n requestAnimationFrame(() => {\n const btns = this.el.querySelectorAll<HTMLElement>('.menu-item');\n btns[this.focusedIndex]?.focus();\n });\n }\n\n private setupListeners() {\n this.clickOutsideHandler = (e: MouseEvent) => {\n const t = e.target as Node;\n const popup = this.el.querySelector('.menu-popup');\n const anchorEl = this.resolvedAnchor;\n if (popup?.contains(t) || anchorEl?.contains(t)) return;\n this.close();\n };\n\n this.scrollResizeHandler = () => {\n if (this.shouldRender && !this.closing) this.calculatePosition();\n };\n\n document.addEventListener('mousedown', this.clickOutsideHandler, true);\n window.addEventListener('scroll', this.scrollResizeHandler, true);\n window.addEventListener('resize', this.scrollResizeHandler);\n }\n\n private teardownListeners() {\n if (this.clickOutsideHandler) {\n document.removeEventListener('mousedown', this.clickOutsideHandler, true);\n this.clickOutsideHandler = null;\n }\n if (this.scrollResizeHandler) {\n window.removeEventListener('scroll', this.scrollResizeHandler, true);\n window.removeEventListener('resize', this.scrollResizeHandler);\n this.scrollResizeHandler = null;\n }\n if (this.closeTimer) {\n clearTimeout(this.closeTimer);\n this.closeTimer = null;\n }\n }\n\n private close(restoreFocus = true) {\n 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 this.dsSelect.emit(item);\n }\n\n private handleGradientSelect(preset: ShellGradientPreset) {\n this.dsGradientSelect.emit(preset);\n }\n\n private handleSwatchSelect(value: string) {\n this.dsSwatchSelect.emit(value);\n }\n\n render() {\n if (!this.shouldRender) return <Host style={{ display: 'contents' }} />;\n\n const sections = this.activeSections;\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 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={this.hasCompositeSections ? 'dialog' : 'menu'}\n aria-label={this.menuLabel}\n aria-orientation={this.hasCompositeSections ? undefined : 'vertical'}\n >\n <div class=\"ds-choice-list\">\n {sections.map((section, si) => (\n <div\n key={si}\n class={{\n 'menu-section': true,\n 'menu-section--divided': si < sections.length - 1,\n 'menu-section--gradient-picker': isMenuPickerSection(section),\n 'ds-choice-section': true,\n 'ds-choice-section--divided': si < sections.length - 1,\n }}\n role={section.header ? 'group' : undefined}\n aria-label={section.header}\n >\n {section.header && (\n <ds-text\n class=\"section-header ds-choice-section__header ds-control--md\"\n as=\"span\"\n variant=\"text-body-small\"\n emphasis\n color=\"primary\"\n aria-hidden=\"true\"\n >\n {section.header}\n </ds-text>\n )}\n {isMenuGradientPickerSection(section) ? (\n <ds-swatch-picker\n value={section.value}\n groupLabel={section.header ?? 'Shell gradient theme'}\n sections={shellGradientPickerSections()}\n onDsChange={(e: CustomEvent<string>) => {\n e.stopPropagation();\n this.handleGradientSelect(e.detail as ShellGradientPreset);\n }}\n />\n ) : isMenuSwatchPickerSection(section) ? (\n <ds-swatch-picker\n value={section.value}\n groupLabel={section.groupLabel ?? section.header ?? 'Swatch options'}\n options={section.options ?? []}\n sections={section.sections ?? []}\n onDsChange={(e: CustomEvent<string>) => {\n e.stopPropagation();\n this.handleSwatchSelect(e.detail);\n }}\n />\n ) : (\n section.items.map(item => {\n const idx = flatIdx++;\n const isFocused = this.focusedIndex === idx;\n const isRadioItem =\n !this.hasCompositeSections &&\n this.selectionMode === 'single' &&\n !item.showSwitch;\n return (\n <button\n key={idx}\n type=\"button\"\n class={{\n 'menu-item': true,\n 'ds-choice-item': true,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-focus-ring--visible': isFocused && this.focusRingVisible,\n 'ds-interaction-fill': !item.isInactive,\n 'ds-interaction-fill--selected': !!item.isSelected && !item.isInactive,\n 'menu-item--selected': !!item.isSelected,\n 'ds-control-inactive': !!item.isInactive,\n 'menu-item--destructive': !!item.isDestructive,\n 'menu-item--focused': isFocused,\n }}\n role={\n this.hasCompositeSections\n ? undefined\n : item.showSwitch\n ? 'menuitemcheckbox'\n : isRadioItem\n ? 'menuitemradio'\n : 'menuitem'\n }\n aria-checked={\n !this.hasCompositeSections && item.showSwitch\n ? String(!!item.switchValue)\n : isRadioItem\n ? String(!!item.isSelected)\n : undefined\n }\n aria-pressed={\n this.hasCompositeSections\n ? String(item.showSwitch ? !!item.switchValue : !!item.isSelected)\n : undefined\n }\n aria-current={\n !this.hasCompositeSections &&\n !item.showSwitch &&\n !isRadioItem &&\n item.isSelected\n ? 'true'\n : undefined\n }\n disabled={item.isInactive}\n tabIndex={this.hasCompositeSections ? 0 : isFocused ? 0 : -1}\n onMouseDown={() => {\n this.focusRingVisible = false;\n }}\n onClick={() => this.handleItemClick(item)}\n onFocus={() => {\n this.focusedIndex = idx;\n }}\n >\n {isRadioItem && (\n <span\n class=\"menu-item__radio-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <span\n class={{\n 'menu-item__radio': true,\n 'menu-item__radio--checked': !!item.isSelected,\n }}\n >\n {item.isSelected && <span class=\"menu-item__radio-dot\" />}\n </span>\n </span>\n )}\n <div class=\"menu-item__content ds-choice-item__content ds-interaction-fill__content\">\n <ds-text\n class=\"menu-item__label ds-choice-item__label\"\n as=\"span\"\n variant=\"text-body-medium\"\n color={item.isSelected ? 'primary' : 'secondary'}\n >\n {item.label}\n </ds-text>\n {item.subtext && (\n <ds-text\n class=\"menu-item__subtext ds-choice-item__subtext\"\n as=\"span\"\n variant=\"text-body-small\"\n color=\"secondary\"\n >\n {item.subtext}\n </ds-text>\n )}\n </div>\n {item.dot && (\n <span\n class=\"menu-item__dot-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-badge\n class=\"menu-item__dot\"\n variant=\"dot\"\n hasRing={false}\n label=\"\"\n />\n </span>\n )}\n {item.showSwitch && (\n <ds-switch\n class=\"menu-item__switch ds-interaction-fill__content\"\n size=\"md\"\n checked={!!item.switchValue}\n presentation\n />\n )}\n </button>\n );\n })\n )}\n </div>\n ))}\n </div>\n </div>\n </Host>\n );\n }\n}\n",
441
461
  "type": "registry:ui"
442
462
  },
443
463
  {
444
464
  "path": "src/wc/components/Menu/menu-placement.ts",
445
- "content": "import type { ChoicePopupAnchorAlignment } from '../../utils/choice-popup-alignment';\nimport type { MenuAlign, MenuSide } from './menu-position';\n\n/** Placement props bindable on `ds-menu` (side, align, offsets). */\nexport interface MenuPlacement {\n side: MenuSide;\n align: MenuAlign;\n anchorAlignment: ChoicePopupAnchorAlignment;\n sideOffset: number | string;\n alignOffset: number | string;\n}\n\n/**\n * Canonical `ds-menu` placement for the **panel-nav footer user menu**.\n * Anchor from `dsNavUserAction` detail `{ anchor }` (id `ds-panel-nav-user-menu-anchor`).\n *\n * Not for BarNav overflow menus — those use `side=\"bottom\"` / `align=\"end\"` internally.\n * `align: 'end'` bottom-aligns the last choice cell on the anchor; the side offset nudges into the content area:\n * - `sideOffset` space-100 + space-050 (12px): 4px past the panel-nav outer edge (anchor is ~8px inset).\n * - `anchorAlignment: 'choice-cell'`: the shared section inset puts the last row edge on the footer user row.\n */\nexport const PANEL_NAV_USER_MENU_PLACEMENT = {\n side: 'right',\n align: 'end',\n anchorAlignment: 'choice-cell',\n sideOffset: 'calc(var(--dimension-space-100) + var(--dimension-space-050))',\n alignOffset: 0,\n} as const satisfies MenuPlacement;\n",
465
+ "content": "import type { ChoicePopupAnchorAlignment } from '../../utils/choice-popup-alignment';\nimport type { MenuAlign, MenuSide } from './menu-position';\n\n/** Placement props bindable on `ds-menu` (side, align, offsets). */\nexport interface MenuPlacement {\n side: MenuSide;\n align: MenuAlign;\n anchorAlignment: ChoicePopupAnchorAlignment;\n sideOffset: number | string;\n alignOffset: number | string;\n}\n\n/**\n * Canonical `ds-menu` placement for the **panel-nav footer user menu**.\n * Anchor from `dsNavUserAction` detail `{ anchor }` (id `ds-panel-nav-user-menu-anchor`).\n *\n * Not for BarNav overflow menus — those use `side=\"bottom\"` / `align=\"end\"` internally.\n * `align: 'end'` bottom-aligns the last choice cell on the anchor; the side offset nudges into the content area:\n * - `sideOffset` space-100 + space-050 (12px): 4px past the panel-nav outer edge (anchor is ~8px inset).\n * - `anchorAlignment: 'choice-cell'`: the shared section inset puts the last row edge on the footer user row.\n */\nexport const PANEL_NAV_USER_MENU_PLACEMENT = {\n side: 'right',\n align: 'end',\n anchorAlignment: 'choice-cell',\n sideOffset: 'calc(var(--dimension-space-100) + var(--dimension-space-050))',\n alignOffset: 0,\n} as const satisfies MenuPlacement;\n\n/**\n * Canonical placement for a menu opened by a PanelTools header action.\n * Matches BarNav overflow: the 32px trigger is 8px inside the 48px header, so\n * a 12px side offset places the popup 4px below the header edge. Choice-cell\n * alignment keeps the final menu item edge aligned with the trigger.\n */\nexport const PANEL_TOOLS_HEADER_MENU_PLACEMENT = {\n side: 'bottom',\n align: 'end',\n anchorAlignment: 'choice-cell',\n sideOffset: 'calc(var(--dimension-space-100) + var(--dimension-space-050))',\n alignOffset: 0,\n} as const satisfies MenuPlacement;\n",
446
466
  "type": "registry:ui"
447
467
  },
448
468
  {
@@ -452,7 +472,7 @@
452
472
  },
453
473
  {
454
474
  "path": "src/wc/components/Menu/menu-types.ts",
455
- "content": "import type { ShellGradientPreset } from '../../shell/shell-gradient-presets';\nimport type { SwatchPickerOption, SwatchPickerSection } from '../SwatchPicker/swatch-picker-types';\n\nexport type { MenuAlign, MenuSide } from './menu-position';\nexport {\n PANEL_NAV_USER_MENU_PLACEMENT,\n type MenuPlacement,\n} from './menu-placement';\n\nexport interface MenuItemData {\n label: string;\n value?: string;\n subtext?: string;\n /** Show a supplemental brand notification dot. */\n dot?: boolean;\n isSelected?: boolean;\n isInactive?: boolean;\n isDestructive?: boolean;\n /** Render a compact switch indicator; the menu row owns interaction. */\n showSwitch?: boolean;\n switchValue?: boolean;\n}\n\nexport interface MenuItemsSection {\n header?: string;\n items: MenuItemData[];\n}\n\nexport interface MenuGradientPickerSection {\n header?: string;\n /** @deprecated Use the generic `swatch-picker` section. */\n variant: 'gradient-picker';\n value: ShellGradientPreset;\n}\n\nexport interface MenuSwatchPickerSection {\n header?: string;\n variant: 'swatch-picker';\n value: string;\n options?: SwatchPickerOption[];\n sections?: SwatchPickerSection[];\n groupLabel?: string;\n}\n\nexport type MenuSection =\n | MenuItemsSection\n | MenuGradientPickerSection\n | MenuSwatchPickerSection;\n\nexport function isMenuGradientPickerSection(\n section: MenuSection,\n): section is MenuGradientPickerSection {\n return 'variant' in section && section.variant === 'gradient-picker';\n}\n\nexport function isMenuSwatchPickerSection(\n section: MenuSection,\n): section is MenuSwatchPickerSection {\n return 'variant' in section && section.variant === 'swatch-picker';\n}\n\nexport function isMenuPickerSection(\n section: MenuSection,\n): section is MenuGradientPickerSection | MenuSwatchPickerSection {\n return isMenuGradientPickerSection(section) || isMenuSwatchPickerSection(section);\n}\n\nexport type { ShellGradientPreset };\nexport type { SwatchPickerOption, SwatchPickerSection };\n",
475
+ "content": "import type { ShellGradientPreset } from '../../shell/shell-gradient-presets';\nimport type { SwatchPickerOption, SwatchPickerSection } from '../SwatchPicker/swatch-picker-types';\n\nexport type { MenuAlign, MenuSide } from './menu-position';\nexport {\n PANEL_NAV_USER_MENU_PLACEMENT,\n PANEL_TOOLS_HEADER_MENU_PLACEMENT,\n type MenuPlacement,\n} from './menu-placement';\n\nexport interface MenuItemData {\n label: string;\n value?: string;\n subtext?: string;\n /** Show a supplemental brand notification dot. */\n dot?: boolean;\n isSelected?: boolean;\n isInactive?: boolean;\n isDestructive?: boolean;\n /** Render a compact switch indicator; the menu row owns interaction. */\n showSwitch?: boolean;\n switchValue?: boolean;\n}\n\nexport interface MenuItemsSection {\n header?: string;\n items: MenuItemData[];\n}\n\nexport interface MenuGradientPickerSection {\n header?: string;\n /** @deprecated Use the generic `swatch-picker` section. */\n variant: 'gradient-picker';\n value: ShellGradientPreset;\n}\n\nexport interface MenuSwatchPickerSection {\n header?: string;\n variant: 'swatch-picker';\n value: string;\n options?: SwatchPickerOption[];\n sections?: SwatchPickerSection[];\n groupLabel?: string;\n}\n\nexport type MenuSection =\n | MenuItemsSection\n | MenuGradientPickerSection\n | MenuSwatchPickerSection;\n\nexport function isMenuGradientPickerSection(\n section: MenuSection,\n): section is MenuGradientPickerSection {\n return 'variant' in section && section.variant === 'gradient-picker';\n}\n\nexport function isMenuSwatchPickerSection(\n section: MenuSection,\n): section is MenuSwatchPickerSection {\n return 'variant' in section && section.variant === 'swatch-picker';\n}\n\nexport function isMenuPickerSection(\n section: MenuSection,\n): section is MenuGradientPickerSection | MenuSwatchPickerSection {\n return isMenuGradientPickerSection(section) || isMenuSwatchPickerSection(section);\n}\n\nexport type { ShellGradientPreset };\nexport type { SwatchPickerOption, SwatchPickerSection };\n",
456
476
  "type": "registry:ui"
457
477
  }
458
478
  ]
@@ -0,0 +1,119 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "message-bubble",
4
+ "title": "MessageBubble",
5
+ "description": "Visible message content surface with a body-medium default, separated from message-row direction and metadata.",
6
+ "type": "registry:ui",
7
+ "meta": {
8
+ "library": "@ds-mo/ui",
9
+ "distribution": "npm",
10
+ "storybook": "https://zainadeel.github.io/compomo/?path=/story/message-bubble",
11
+ "source": "src/wc/components/MessageBubble/MessageBubble.tsx",
12
+ "intentStatus": "complete",
13
+ "intent": {
14
+ "audience": "general",
15
+ "status": "experimental",
16
+ "summary": "Visible message content surface with a body-medium default, separated from message-row direction and metadata.",
17
+ "useWhen": [
18
+ "Message content needs a primary, secondary, ghost, or error surface within a Message row.",
19
+ "Plain incoming or outgoing message copy should use the shared body-medium regular typography recipe without application styling."
20
+ ],
21
+ "avoidWhen": [
22
+ "The surface is a general card, callout, or form container.",
23
+ "The application wants to promote ordinary conversation copy to body-large; message typography stays body-medium unless a distinct authored rich-content element owns its own complete type recipe."
24
+ ],
25
+ "commonlyComposedWith": [
26
+ "component:ds-message",
27
+ "component:ds-markdown"
28
+ ],
29
+ "patterns": [
30
+ "pattern:conversation"
31
+ ],
32
+ "accessibility": [
33
+ "Surface styling does not add semantics; meaningful error or status copy belongs in message content or metadata."
34
+ ],
35
+ "responsiveBehavior": [
36
+ "The bubble wraps content and allows wide ghost responses to use more transcript width."
37
+ ],
38
+ "references": [
39
+ {
40
+ "label": "Storybook examples",
41
+ "path": "src/wc/components/MessageScroller/MessageScroller.stories.ts"
42
+ }
43
+ ]
44
+ },
45
+ "api": {
46
+ "props": {
47
+ "variant": {
48
+ "type": "MessageBubbleVariant",
49
+ "resolvedType": "\"error\" | \"ghost\" | \"primary\" | \"secondary\"",
50
+ "attribute": "variant",
51
+ "default": "'secondary'",
52
+ "required": false,
53
+ "mutable": false
54
+ }
55
+ },
56
+ "events": [],
57
+ "methods": [],
58
+ "slots": []
59
+ },
60
+ "props": {
61
+ "variant": {
62
+ "type": "MessageBubbleVariant",
63
+ "resolvedType": "\"error\" | \"ghost\" | \"primary\" | \"secondary\"",
64
+ "attribute": "variant",
65
+ "default": "'secondary'",
66
+ "required": false,
67
+ "mutable": false
68
+ }
69
+ },
70
+ "events": [],
71
+ "methods": [],
72
+ "slots": [],
73
+ "exports": {
74
+ "customElement": "ds-message-bubble",
75
+ "react": "DsMessageBubble",
76
+ "angular": "DsMessageBubble"
77
+ },
78
+ "consumption": {
79
+ "install": "npm install @ds-mo/ui @ds-mo/tokens @ds-mo/icons",
80
+ "cssSetup": "import '@ds-mo/tokens';\nimport '@ds-mo/tokens/reset';\nimport '@ds-mo/tokens/globals';",
81
+ "customElements": {
82
+ "import": "import '@ds-mo/ui/dist/components/ds-message-bubble.js';",
83
+ "example": "<ds-message-bubble></ds-message-bubble>"
84
+ },
85
+ "react": {
86
+ "import": "import { DsMessageBubble } from '@ds-mo/ui/react';",
87
+ "example": "<DsMessageBubble />"
88
+ },
89
+ "angular": {
90
+ "import": "import { DsMessageBubble } from '@ds-mo/ui/angular/ds-message-bubble';",
91
+ "example": "<ds-message-bubble></ds-message-bubble>"
92
+ },
93
+ "peerDependencies": {
94
+ "required": [
95
+ "@ds-mo/tokens ^3.1.1",
96
+ "@ds-mo/icons ^6.2.0"
97
+ ],
98
+ "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
99
+ }
100
+ }
101
+ },
102
+ "dependencies": [
103
+ "@ds-mo/ui",
104
+ "@ds-mo/tokens"
105
+ ],
106
+ "registryDependencies": [],
107
+ "files": [
108
+ {
109
+ "path": "src/wc/components/MessageBubble/MessageBubble.css",
110
+ "content": ":host {\n display: inline-flex;\n max-width: 80%;\n box-sizing: border-box;\n}\n\n:host(.message-bubble--ghost) {\n display: block;\n max-width: 100%;\n}\n\n.message-bubble {\n min-width: 0;\n padding: var(--dimension-space-100);\n border-start-start-radius: var(\n --ds-message-bubble-radius-start-start,\n var(--dimension-radius-050)\n );\n border-start-end-radius: var(--ds-message-bubble-radius-start-end, var(--dimension-radius-050));\n border-end-start-radius: var(--ds-message-bubble-radius-end-start, var(--dimension-radius-050));\n border-end-end-radius: var(--ds-message-bubble-radius-end-end, var(--dimension-radius-050));\n background: var(--color-background-secondary);\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--primary) .message-bubble {\n background: var(--color-background-bold-brand);\n color: var(--color-foreground-faint-brand);\n}\n\n:host(.message-bubble--secondary) .message-bubble {\n box-shadow: inset 0 0 0 var(--dimension-stroke-width-012) var(--color-border-tertiary);\n}\n\n:host(.message-bubble--ghost) .message-bubble {\n padding-inline: 0;\n background: transparent;\n border-radius: 0;\n}\n\n:host(.message-bubble--error) .message-bubble {\n background: var(--color-background-faint-negative);\n color: var(--color-foreground-bold-negative);\n box-shadow: inset 0 0 0 var(--dimension-stroke-width-012) var(--color-border-bold-negative);\n}\n",
111
+ "type": "registry:ui"
112
+ },
113
+ {
114
+ "path": "src/wc/components/MessageBubble/MessageBubble.tsx",
115
+ "content": "import { Component, h, Host, Prop } from '@stencil/core';\n\nexport type MessageBubbleVariant = 'primary' | 'secondary' | 'ghost' | 'error';\n\n@Component({\n tag: 'ds-message-bubble',\n styleUrl: 'MessageBubble.css',\n scoped: true,\n})\nexport class MessageBubble {\n @Prop() variant: MessageBubbleVariant = 'secondary';\n\n render() {\n return (\n <Host class={`message-bubble--${this.variant}`}>\n <div class=\"message-bubble\"><slot /></div>\n </Host>\n );\n }\n}\n",
116
+ "type": "registry:ui"
117
+ }
118
+ ]
119
+ }