@ds-mo/ui 12.9.0 → 13.1.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 (470) hide show
  1. package/README.md +1 -0
  2. package/dist/.package-ready.json +1 -0
  3. package/dist/agent-patterns.json +124 -135
  4. package/dist/agent.json +249 -1108
  5. package/dist/angular/ds-bar-nav.d.ts +1 -1
  6. package/dist/angular/ds-bar-nav.js +3 -3
  7. package/dist/angular/ds-button-filled.d.ts +1 -1
  8. package/dist/angular/ds-button-filled.js +3 -3
  9. package/dist/angular/ds-card-data-viz.d.ts +18 -0
  10. package/dist/angular/{ds-card-data-viz-donut.js → ds-card-data-viz.js} +12 -12
  11. package/dist/angular/ds-card-overview.d.ts +1 -1
  12. package/dist/angular/ds-card-overview.js +3 -3
  13. package/dist/angular/ds-chart-bar.d.ts +1 -1
  14. package/dist/angular/ds-chart-bar.js +3 -3
  15. package/dist/angular/ds-menu.d.ts +1 -7
  16. package/dist/angular/ds-menu.js +2 -5
  17. package/dist/angular/ds-mobile-header.d.ts +1 -1
  18. package/dist/angular/ds-mobile-header.js +3 -3
  19. package/dist/angular/ds-mobile-section-switcher.d.ts +1 -1
  20. package/dist/angular/ds-mobile-section-switcher.js +3 -3
  21. package/dist/angular/ds-panel-tools.d.ts +1 -1
  22. package/dist/angular/ds-panel-tools.js +3 -3
  23. package/dist/angular/ds-select.d.ts +5 -4
  24. package/dist/angular/ds-select.js +3 -3
  25. package/dist/angular/ds-shell-app.d.ts +90 -1
  26. package/dist/angular/ds-shell-app.js +44 -4
  27. package/dist/angular/ds-shell-tools.d.ts +1 -1
  28. package/dist/angular/ds-shell-tools.js +3 -3
  29. package/dist/angular/index.d.ts +1 -1
  30. package/dist/angular/index.js +1 -8
  31. package/dist/angular/proxies.d.ts +1 -8
  32. package/dist/angular/proxies.js +1 -8
  33. package/dist/components/ds-agent-activity.js +1 -1
  34. package/dist/components/ds-agent-response.js +1 -1
  35. package/dist/components/ds-agent-source-list.js +1 -1
  36. package/dist/components/ds-agent-tool-call.js +1 -1
  37. package/dist/components/ds-attachment-list.js +1 -1
  38. package/dist/components/ds-badge.js +1 -1
  39. package/dist/components/ds-bar-nav.js +1 -1
  40. package/dist/components/ds-bar-nav.js.map +1 -1
  41. package/dist/components/ds-bar-title.js +1 -1
  42. package/dist/components/ds-bar-title.js.map +1 -1
  43. package/dist/components/ds-bar-workflow.js +1 -1
  44. package/dist/components/ds-breadcrumb.js +1 -1
  45. package/dist/components/ds-button-filled.js +1 -1
  46. package/dist/components/ds-button-unfilled.js +1 -1
  47. package/dist/components/{ds-select-multi.d.ts → ds-card-data-viz.d.ts} +4 -4
  48. package/dist/components/ds-card-data-viz.js +2 -0
  49. package/dist/components/ds-card-data-viz.js.map +1 -0
  50. package/dist/components/ds-card-overview.js +1 -1
  51. package/dist/components/ds-card-overview.js.map +1 -1
  52. package/dist/components/ds-card-setting.js +1 -1
  53. package/dist/components/ds-chart-bar.js +1 -1
  54. package/dist/components/ds-chart-bar.js.map +1 -1
  55. package/dist/components/ds-chart-donut.js +1 -1
  56. package/dist/components/ds-chart-donut.js.map +1 -1
  57. package/dist/components/ds-chart-legend.js +1 -1
  58. package/dist/components/ds-chart-legend.js.map +1 -1
  59. package/dist/components/ds-chart-line.js +1 -1
  60. package/dist/components/ds-chart-line.js.map +1 -1
  61. package/dist/components/ds-checkbox.js +1 -1
  62. package/dist/components/ds-chip.js +1 -1
  63. package/dist/components/ds-chip.js.map +1 -1
  64. package/dist/components/ds-code-block.js +1 -1
  65. package/dist/components/ds-conversation-list-item.js +1 -1
  66. package/dist/components/ds-conversation-list-item.js.map +1 -1
  67. package/dist/components/ds-conversation-list-section.js +1 -1
  68. package/dist/components/ds-conversation-list.js +1 -1
  69. package/dist/components/ds-divider.js +1 -1
  70. package/dist/components/ds-empty-state.js +1 -1
  71. package/dist/components/ds-field.js +1 -1
  72. package/dist/components/ds-input.js +1 -1
  73. package/dist/components/ds-input.js.map +1 -1
  74. package/dist/components/ds-markdown.js +1 -1
  75. package/dist/components/ds-menu.js +1 -1
  76. package/dist/components/ds-message-bubble.js +1 -1
  77. package/dist/components/ds-message-composer.js +1 -1
  78. package/dist/components/ds-message-composer.js.map +1 -1
  79. package/dist/components/ds-message-scroller.js +1 -1
  80. package/dist/components/ds-message.js +1 -1
  81. package/dist/components/ds-mobile-bar-nav.js +1 -1
  82. package/dist/components/ds-mobile-bar-nav.js.map +1 -1
  83. package/dist/components/ds-mobile-header.js +1 -1
  84. package/dist/components/ds-mobile-section-switcher.js +1 -1
  85. package/dist/components/ds-mobile-sheet-nav.js +1 -1
  86. package/dist/components/ds-mobile-sheet-nav.js.map +1 -1
  87. package/dist/components/ds-modal.js +1 -1
  88. package/dist/components/ds-panel-nav.js +1 -1
  89. package/dist/components/ds-panel-nav.js.map +1 -1
  90. package/dist/components/ds-panel-sub-nav.js +1 -1
  91. package/dist/components/ds-panel-sub-nav.js.map +1 -1
  92. package/dist/components/ds-panel-tool-header.js +1 -1
  93. package/dist/components/ds-panel-tool-search.js +1 -1
  94. package/dist/components/ds-panel-tools.js +1 -1
  95. package/dist/components/ds-radio.js +1 -1
  96. package/dist/components/ds-radio.js.map +1 -1
  97. package/dist/components/ds-scroll-overlay.js +1 -1
  98. package/dist/components/ds-select.js +1 -1
  99. package/dist/components/ds-select.js.map +1 -1
  100. package/dist/components/ds-shell-app.js +1 -1
  101. package/dist/components/ds-shell-app.js.map +1 -1
  102. package/dist/components/ds-shell-page.js +1 -1
  103. package/dist/components/ds-shell-page.js.map +1 -1
  104. package/dist/components/ds-shell-tools.js +1 -1
  105. package/dist/components/ds-shell-tools.js.map +1 -1
  106. package/dist/components/ds-skeleton.js +1 -1
  107. package/dist/components/ds-slider.js +1 -1
  108. package/dist/components/ds-slider.js.map +1 -1
  109. package/dist/components/ds-swatch-picker.js +1 -1
  110. package/dist/components/ds-switch.js +1 -1
  111. package/dist/components/ds-tab-group.js +1 -1
  112. package/dist/components/ds-tag.js +1 -1
  113. package/dist/components/ds-tag.js.map +1 -1
  114. package/dist/components/ds-text.js +1 -1
  115. package/dist/components/ds-toast.js +1 -1
  116. package/dist/components/ds-tooltip-data-viz.js +1 -1
  117. package/dist/components/ds-tooltip.js +1 -1
  118. package/dist/components/ds-typing-indicator.js +1 -1
  119. package/dist/components/{p-CgEYdMee.js → p-6qXDqCj_.js} +2 -2
  120. package/dist/components/{p-Cr3tEx7o.js → p-Ay1AX7Ql.js} +2 -2
  121. package/dist/components/p-B-qDb_Zo.js +2 -0
  122. package/dist/components/p-B-qDb_Zo.js.map +1 -0
  123. package/dist/components/{p-BOCfICln.js → p-B1XW4VSn.js} +2 -2
  124. package/dist/components/p-B9p6w-_T.js +2 -0
  125. package/dist/components/p-B9p6w-_T.js.map +1 -0
  126. package/dist/components/p-BBFEhNzM.js +2 -0
  127. package/dist/components/p-BBFEhNzM.js.map +1 -0
  128. package/dist/components/p-BGUK8-Qm.js +2 -0
  129. package/dist/components/p-BGUK8-Qm.js.map +1 -0
  130. package/dist/components/p-BO4cj3iX.js +2 -0
  131. package/dist/components/{p-DCvA3Dc9.js.map → p-BO4cj3iX.js.map} +1 -1
  132. package/dist/components/{p-BKOlvCGS.js → p-BUZXuod2.js} +2 -2
  133. package/dist/components/p-BYWkksDO.js +2 -0
  134. package/dist/components/p-BYWkksDO.js.map +1 -0
  135. package/dist/components/p-BbMJTilO.js +2 -0
  136. package/dist/components/p-BbMJTilO.js.map +1 -0
  137. package/dist/components/{p-BO5aqZcf.js → p-BcTYhmku.js} +2 -2
  138. package/dist/components/p-BnQXAbif.js +2 -0
  139. package/dist/components/p-BnQXAbif.js.map +1 -0
  140. package/dist/components/p-BnbbnuVC.js +2 -0
  141. package/dist/components/p-BnbbnuVC.js.map +1 -0
  142. package/dist/components/{p-BDKtu6Wb.js → p-BroVnDzE.js} +2 -2
  143. package/dist/components/{p-CIiBpjCn.js → p-BsoqsbBh.js} +2 -2
  144. package/dist/components/p-BybqPD8K.js +2 -0
  145. package/dist/components/p-BybqPD8K.js.map +1 -0
  146. package/dist/components/{p-CCzG5xDD.js → p-C-aCzwhA.js} +2 -2
  147. package/dist/components/p-C-aCzwhA.js.map +1 -0
  148. package/dist/components/p-C-kzknkR.js +2 -0
  149. package/dist/components/p-C-kzknkR.js.map +1 -0
  150. package/dist/components/p-C-vRkDyn.js +2 -0
  151. package/dist/components/p-C-vRkDyn.js.map +1 -0
  152. package/dist/components/p-C4uYvyq6.js +2 -0
  153. package/dist/components/p-C4uYvyq6.js.map +1 -0
  154. package/dist/components/{p-C8HU4W8q.js → p-CBazaRlE.js} +2 -2
  155. package/dist/components/p-CH9yArA1.js +2 -0
  156. package/dist/components/p-CH9yArA1.js.map +1 -0
  157. package/dist/components/p-CHpNyAo1.js +2 -0
  158. package/dist/components/{p-CChcuL04.js.map → p-CHpNyAo1.js.map} +1 -1
  159. package/dist/components/{p-BhV5e6vF.js → p-CHuh71-U.js} +2 -2
  160. package/dist/components/p-CJDTh9Tg.js +2 -0
  161. package/dist/components/p-CJDTh9Tg.js.map +1 -0
  162. package/dist/components/p-CJbLY1tu.js +2 -0
  163. package/dist/components/p-CJbLY1tu.js.map +1 -0
  164. package/dist/components/p-CreGah0j.js +2 -0
  165. package/dist/components/p-Cvxm1OZB.js +2 -0
  166. package/dist/components/p-Cvxm1OZB.js.map +1 -0
  167. package/dist/components/p-D2xWcVv6.js +2 -0
  168. package/dist/components/p-D2xWcVv6.js.map +1 -0
  169. package/dist/components/p-DAW6G8Sl.js +2 -0
  170. package/dist/components/p-DAW6G8Sl.js.map +1 -0
  171. package/dist/components/p-DEaszb15.js +2 -0
  172. package/dist/components/p-DEaszb15.js.map +1 -0
  173. package/dist/components/{p-BgGP65Nn.js → p-DOUk4KoT.js} +1 -1
  174. package/dist/components/p-DOUk4KoT.js.map +1 -0
  175. package/dist/components/p-DbQr7Kwr.js +2 -0
  176. package/dist/components/p-DbQr7Kwr.js.map +1 -0
  177. package/dist/components/{p-Cuih680v.js → p-Df17mJqI.js} +2 -2
  178. package/dist/components/p-DhEdICfD.js +2 -0
  179. package/dist/components/p-DhEdICfD.js.map +1 -0
  180. package/dist/components/p-bxoryr-3.js +2 -0
  181. package/dist/components/p-bxoryr-3.js.map +1 -0
  182. package/dist/components/p-eAfNzlMa.js +2 -0
  183. package/dist/components/p-eAfNzlMa.js.map +1 -0
  184. package/dist/components/p-gZni7A2L.js +2 -0
  185. package/dist/components/p-gZni7A2L.js.map +1 -0
  186. package/dist/components/{p-YR1XeuZf.js → p-ohsLFap_.js} +2 -2
  187. package/dist/components/{p-DjbTFy2B.js → p-ymtdhOU5.js} +1 -1
  188. package/dist/components/p-ymtdhOU5.js.map +1 -0
  189. package/dist/docs/components.json +929 -2371
  190. package/dist/framework/angular.d.ts +0 -1
  191. package/dist/framework/angular.js +0 -1
  192. package/dist/lib/components/BarTitle/bar-title-types.d.ts +28 -0
  193. package/dist/lib/components/Breadcrumb/breadcrumb-types.d.ts +16 -0
  194. package/dist/lib/components/ButtonFilled/ButtonFilled.d.ts +83 -0
  195. package/dist/lib/components/MobileHeader/mobile-header-types.d.ts +4 -0
  196. package/dist/lib/components/PanelTools/panel-tools-types.d.ts +21 -7
  197. package/dist/lib/components/ShellApp/shell-app-types.d.ts +91 -0
  198. package/dist/lib/shell/index.d.ts +5 -13
  199. package/dist/lib/shell/index.js +87 -358
  200. package/dist/lib/shell/index.js.map +3 -3
  201. package/dist/lib/shell/shell-responsive.d.ts +2 -1
  202. package/dist/lib/utils/button-render.d.ts +17 -0
  203. package/dist/lib/utils/button-types.d.ts +10 -0
  204. package/dist/lib/utils/choice-list.d.ts +25 -0
  205. package/dist/lib/utils/control-press.d.ts +8 -0
  206. package/dist/lib/utils/form-association.d.ts +22 -0
  207. package/dist/lib/utils/index.d.ts +4 -0
  208. package/dist/lib/utils/index.js +63 -0
  209. package/dist/lib/utils/index.js.map +3 -3
  210. package/dist/lib/utils/required-validity.d.ts +1 -1
  211. package/dist/lib/utils/safe-url.d.ts +11 -0
  212. package/dist/mcp/cli.js +38 -10
  213. package/dist/mcp-data/registry/agent-activity.json +1 -1
  214. package/dist/mcp-data/registry/agent-response.json +1 -1
  215. package/dist/mcp-data/registry/agent-source-list.json +2 -2
  216. package/dist/mcp-data/registry/agent-tool-call.json +1 -1
  217. package/dist/mcp-data/registry/attachment-list.json +2 -2
  218. package/dist/mcp-data/registry/avatar.json +1 -1
  219. package/dist/mcp-data/registry/badge.json +1 -1
  220. package/dist/mcp-data/registry/bar-nav.json +4 -22
  221. package/dist/mcp-data/registry/bar-title.json +1 -1
  222. package/dist/mcp-data/registry/bar-workflow.json +1 -1
  223. package/dist/mcp-data/registry/breadcrumb.json +2 -2
  224. package/dist/mcp-data/registry/button-filled.json +5 -20
  225. package/dist/mcp-data/registry/button-unfilled.json +15 -12
  226. package/dist/mcp-data/registry/card-data-viz.json +241 -0
  227. package/dist/mcp-data/registry/card-overview.json +39 -11
  228. package/dist/mcp-data/registry/card-setting.json +3 -3
  229. package/dist/mcp-data/registry/chart-bar.json +67 -19
  230. package/dist/mcp-data/registry/chart-donut.json +8 -8
  231. package/dist/mcp-data/registry/chart-legend.json +9 -11
  232. package/dist/mcp-data/registry/chart-line.json +2 -3
  233. package/dist/mcp-data/registry/checkbox.json +3 -3
  234. package/dist/mcp-data/registry/chip.json +1 -1
  235. package/dist/mcp-data/registry/code-block.json +1 -1
  236. package/dist/mcp-data/registry/conversation-list-item.json +2 -2
  237. package/dist/mcp-data/registry/conversation-list-section.json +1 -1
  238. package/dist/mcp-data/registry/conversation-list.json +1 -1
  239. package/dist/mcp-data/registry/divider.json +1 -1
  240. package/dist/mcp-data/registry/empty-state.json +1 -1
  241. package/dist/mcp-data/registry/field.json +3 -4
  242. package/dist/mcp-data/registry/icon.json +1 -1
  243. package/dist/mcp-data/registry/input.json +2 -2
  244. package/dist/mcp-data/registry/loader.json +1 -1
  245. package/dist/mcp-data/registry/markdown.json +2 -2
  246. package/dist/mcp-data/registry/menu.json +11 -22
  247. package/dist/mcp-data/registry/message-bubble.json +1 -1
  248. package/dist/mcp-data/registry/message-composer.json +1 -1
  249. package/dist/mcp-data/registry/message-scroller.json +1 -1
  250. package/dist/mcp-data/registry/message.json +1 -1
  251. package/dist/mcp-data/registry/mobile-bar-nav.json +2 -2
  252. package/dist/mcp-data/registry/mobile-header.json +2 -38
  253. package/dist/mcp-data/registry/mobile-section-switcher.json +2 -20
  254. package/dist/mcp-data/registry/mobile-sheet-nav.json +1 -1
  255. package/dist/mcp-data/registry/modal.json +1 -1
  256. package/dist/mcp-data/registry/panel-nav.json +3 -3
  257. package/dist/mcp-data/registry/panel-sub-nav.json +1 -1
  258. package/dist/mcp-data/registry/panel-tool-header.json +1 -1
  259. package/dist/mcp-data/registry/panel-tool-search.json +2 -2
  260. package/dist/mcp-data/registry/panel-tools.json +20 -56
  261. package/dist/mcp-data/registry/radio.json +2 -2
  262. package/dist/mcp-data/registry/registry.json +295 -1175
  263. package/dist/mcp-data/registry/scroll-overlay.json +1 -1
  264. package/dist/mcp-data/registry/select.json +60 -26
  265. package/dist/mcp-data/registry/shell-app.json +346 -21
  266. package/dist/mcp-data/registry/shell-page.json +2 -2
  267. package/dist/mcp-data/registry/shell-tools.json +13 -44
  268. package/dist/mcp-data/registry/skeleton.json +1 -1
  269. package/dist/mcp-data/registry/slider.json +2 -2
  270. package/dist/mcp-data/registry/swatch-picker.json +1 -1
  271. package/dist/mcp-data/registry/switch.json +2 -2
  272. package/dist/mcp-data/registry/tab-group.json +1 -1
  273. package/dist/mcp-data/registry/tag.json +1 -1
  274. package/dist/mcp-data/registry/text.json +1 -1
  275. package/dist/mcp-data/registry/toast.json +1 -1
  276. package/dist/mcp-data/registry/tooltip-data-viz.json +1 -1
  277. package/dist/mcp-data/registry/tooltip.json +1 -1
  278. package/dist/mcp-data/registry/typing-indicator.json +1 -1
  279. package/dist/react/components.d.ts +1 -8
  280. package/dist/react/components.js +1 -8
  281. package/dist/react/ds-card-data-viz.d.ts +12 -0
  282. package/dist/react/ds-card-data-viz.js +12 -0
  283. package/dist/react/ds-menu.d.ts +1 -2
  284. package/dist/react/ds-menu.js +0 -1
  285. package/dist/react/ds-select.d.ts +2 -2
  286. package/dist/react/ds-shell-app.d.ts +24 -1
  287. package/dist/react/ds-shell-app.js +16 -1
  288. package/dist/types/components/AgentSourceList/AgentSourceList.d.ts +0 -1
  289. package/dist/types/components/BarNav/BarNav.d.ts +0 -2
  290. package/dist/types/components/BarNav/bar-nav-utils.d.ts +2 -4
  291. package/dist/types/components/ButtonFilled/ButtonFilled.d.ts +5 -11
  292. package/dist/types/components/ButtonUnfilled/ButtonUnfilled.d.ts +13 -10
  293. package/dist/types/components/CardDataViz/CardDataViz.d.ts +32 -0
  294. package/dist/types/components/CardDataViz/index.d.ts +2 -0
  295. package/dist/types/components/CardOverview/CardOverview.d.ts +17 -0
  296. package/dist/types/components/CardOverview/card-overview-controller.d.ts +16 -0
  297. package/dist/types/components/ChartBar/ChartBar.d.ts +12 -1
  298. package/dist/types/components/ChartBar/index.d.ts +2 -1
  299. package/dist/types/components/Menu/Menu.d.ts +0 -4
  300. package/dist/types/components/Menu/menu-sections.d.ts +4 -0
  301. package/dist/types/components/Menu/menu-types.d.ts +2 -11
  302. package/dist/types/components/MobileHeader/MobileHeader.d.ts +0 -5
  303. package/dist/types/components/MobileSectionSwitcher/MobileSectionSwitcher.d.ts +0 -2
  304. package/dist/types/components/PanelTools/PanelTools.d.ts +0 -4
  305. package/dist/types/components/PanelTools/index.d.ts +1 -1
  306. package/dist/types/components/PanelTools/panel-tools-types.d.ts +21 -7
  307. package/dist/types/components/PanelTools/panel-tools-utils.d.ts +1 -1
  308. package/dist/types/components/Select/Select.d.ts +12 -4
  309. package/dist/types/components/Select/index.d.ts +1 -1
  310. package/dist/types/components/ShellApp/ShellApp.d.ts +98 -0
  311. package/dist/types/components/ShellApp/shell-app-types.d.ts +92 -0
  312. package/dist/types/components/ShellPage/ShellPage.d.ts +5 -0
  313. package/dist/types/components/ShellTools/ShellTools.d.ts +2 -2
  314. package/dist/types/components.d.ts +210 -889
  315. package/dist/types/shell/index.d.ts +5 -13
  316. package/dist/types/shell/shell-responsive.d.ts +2 -1
  317. package/dist/types/utils/button-render.d.ts +18 -0
  318. package/dist/types/utils/button-story-foundation.d.ts +9 -0
  319. package/dist/types/utils/button-types.d.ts +11 -0
  320. package/dist/types/utils/form-association.d.ts +23 -0
  321. package/dist/types/utils/index.d.ts +4 -0
  322. package/dist/types/utils/required-validity.d.ts +1 -1
  323. package/dist/types/utils/safe-url.d.ts +12 -0
  324. package/dist/types/utils/select-controller.d.ts +1 -1
  325. package/package.json +18 -10
  326. package/dist/.build-stamp +0 -1
  327. package/dist/angular/ds-card-data-viz-bar.d.ts +0 -18
  328. package/dist/angular/ds-card-data-viz-bar.js +0 -42
  329. package/dist/angular/ds-card-data-viz-donut.d.ts +0 -18
  330. package/dist/angular/ds-card-data-viz-line.d.ts +0 -18
  331. package/dist/angular/ds-card-data-viz-line.js +0 -42
  332. package/dist/angular/ds-card-shell-data-viz.d.ts +0 -12
  333. package/dist/angular/ds-card-shell-data-viz.js +0 -38
  334. package/dist/angular/ds-chart-bar-stacked.d.ts +0 -12
  335. package/dist/angular/ds-chart-bar-stacked.js +0 -38
  336. package/dist/angular/ds-select-multi.d.ts +0 -28
  337. package/dist/angular/ds-select-multi.js +0 -49
  338. package/dist/angular/ds-shell-gradient-picker.d.ts +0 -16
  339. package/dist/angular/ds-shell-gradient-picker.js +0 -42
  340. package/dist/angular/ds-shell-gradient-swatch.d.ts +0 -16
  341. package/dist/angular/ds-shell-gradient-swatch.js +0 -42
  342. package/dist/components/ds-card-data-viz-bar.d.ts +0 -11
  343. package/dist/components/ds-card-data-viz-bar.js +0 -2
  344. package/dist/components/ds-card-data-viz-bar.js.map +0 -1
  345. package/dist/components/ds-card-data-viz-donut.d.ts +0 -11
  346. package/dist/components/ds-card-data-viz-donut.js +0 -2
  347. package/dist/components/ds-card-data-viz-donut.js.map +0 -1
  348. package/dist/components/ds-card-data-viz-line.d.ts +0 -11
  349. package/dist/components/ds-card-data-viz-line.js +0 -2
  350. package/dist/components/ds-card-data-viz-line.js.map +0 -1
  351. package/dist/components/ds-card-shell-data-viz.d.ts +0 -11
  352. package/dist/components/ds-card-shell-data-viz.js +0 -2
  353. package/dist/components/ds-card-shell-data-viz.js.map +0 -1
  354. package/dist/components/ds-chart-bar-stacked.d.ts +0 -11
  355. package/dist/components/ds-chart-bar-stacked.js +0 -2
  356. package/dist/components/ds-chart-bar-stacked.js.map +0 -1
  357. package/dist/components/ds-select-multi.js +0 -2
  358. package/dist/components/ds-select-multi.js.map +0 -1
  359. package/dist/components/ds-shell-gradient-picker.d.ts +0 -11
  360. package/dist/components/ds-shell-gradient-picker.js +0 -2
  361. package/dist/components/ds-shell-gradient-picker.js.map +0 -1
  362. package/dist/components/ds-shell-gradient-swatch.d.ts +0 -11
  363. package/dist/components/ds-shell-gradient-swatch.js +0 -2
  364. package/dist/components/ds-shell-gradient-swatch.js.map +0 -1
  365. package/dist/components/p-2foAUsDN.js +0 -2
  366. package/dist/components/p-2foAUsDN.js.map +0 -1
  367. package/dist/components/p-8GK7B7K4.js +0 -2
  368. package/dist/components/p-8GK7B7K4.js.map +0 -1
  369. package/dist/components/p-B5tMlca-.js +0 -2
  370. package/dist/components/p-B5tMlca-.js.map +0 -1
  371. package/dist/components/p-B88n0pO3.js +0 -2
  372. package/dist/components/p-B88n0pO3.js.map +0 -1
  373. package/dist/components/p-BBePKRHC.js +0 -2
  374. package/dist/components/p-BBePKRHC.js.map +0 -1
  375. package/dist/components/p-BOQnc-M4.js +0 -2
  376. package/dist/components/p-BOQnc-M4.js.map +0 -1
  377. package/dist/components/p-BgGP65Nn.js.map +0 -1
  378. package/dist/components/p-BrusnEYP.js +0 -2
  379. package/dist/components/p-BrusnEYP.js.map +0 -1
  380. package/dist/components/p-C61yJkfr.js +0 -2
  381. package/dist/components/p-C61yJkfr.js.map +0 -1
  382. package/dist/components/p-CChcuL04.js +0 -2
  383. package/dist/components/p-CCzG5xDD.js.map +0 -1
  384. package/dist/components/p-CGVXbXv9.js +0 -2
  385. package/dist/components/p-CGVXbXv9.js.map +0 -1
  386. package/dist/components/p-CH3w-P2H.js +0 -2
  387. package/dist/components/p-CH3w-P2H.js.map +0 -1
  388. package/dist/components/p-Cuqxh-Tq.js +0 -2
  389. package/dist/components/p-Cuqxh-Tq.js.map +0 -1
  390. package/dist/components/p-D1HFMTdC.js +0 -2
  391. package/dist/components/p-D1HFMTdC.js.map +0 -1
  392. package/dist/components/p-D2x17ei-.js +0 -2
  393. package/dist/components/p-D2x17ei-.js.map +0 -1
  394. package/dist/components/p-DCvA3Dc9.js +0 -2
  395. package/dist/components/p-DLZm_-sU.js +0 -2
  396. package/dist/components/p-DLZm_-sU.js.map +0 -1
  397. package/dist/components/p-DRnIuCiT.js +0 -2
  398. package/dist/components/p-DjKkSqrG.js +0 -2
  399. package/dist/components/p-DjKkSqrG.js.map +0 -1
  400. package/dist/components/p-DjbTFy2B.js.map +0 -1
  401. package/dist/components/p-DlscFn2m.js +0 -2
  402. package/dist/components/p-DlscFn2m.js.map +0 -1
  403. package/dist/components/p-DxkEC0hR.js +0 -2
  404. package/dist/components/p-DxkEC0hR.js.map +0 -1
  405. package/dist/components/p-Re2kPrXm.js +0 -2
  406. package/dist/components/p-Re2kPrXm.js.map +0 -1
  407. package/dist/components/p-XMZCaXs7.js +0 -2
  408. package/dist/components/p-XMZCaXs7.js.map +0 -1
  409. package/dist/components/p-dyUp3Adm.js +0 -2
  410. package/dist/components/p-dyUp3Adm.js.map +0 -1
  411. package/dist/components/p-nkYN0Bub.js +0 -2
  412. package/dist/components/p-nkYN0Bub.js.map +0 -1
  413. package/dist/framework/select-multi-value-accessor.d.ts +0 -9
  414. package/dist/framework/select-multi-value-accessor.js +0 -40
  415. package/dist/lib/shell/badge-gradient-ring.d.ts +0 -27
  416. package/dist/lib/shell/chrome-transition.d.ts +0 -32
  417. package/dist/lib/shell/shell-chrome-metrics.d.ts +0 -11
  418. package/dist/lib/shell/shell-gradient.d.ts +0 -41
  419. package/dist/lib/shell/shell-view-transition.d.ts +0 -23
  420. package/dist/mcp-data/registry/card-data-viz-bar.json +0 -213
  421. package/dist/mcp-data/registry/card-data-viz-donut.json +0 -214
  422. package/dist/mcp-data/registry/card-data-viz-line.json +0 -207
  423. package/dist/mcp-data/registry/card-shell-data-viz.json +0 -180
  424. package/dist/mcp-data/registry/chart-bar-stacked.json +0 -230
  425. package/dist/mcp-data/registry/select-multi.json +0 -758
  426. package/dist/mcp-data/registry/shell-gradient-picker.json +0 -195
  427. package/dist/mcp-data/registry/shell-gradient-swatch.json +0 -215
  428. package/dist/react/ds-card-data-viz-bar.d.ts +0 -12
  429. package/dist/react/ds-card-data-viz-bar.js +0 -12
  430. package/dist/react/ds-card-data-viz-donut.d.ts +0 -12
  431. package/dist/react/ds-card-data-viz-donut.js +0 -12
  432. package/dist/react/ds-card-data-viz-line.d.ts +0 -12
  433. package/dist/react/ds-card-data-viz-line.js +0 -12
  434. package/dist/react/ds-card-shell-data-viz.d.ts +0 -9
  435. package/dist/react/ds-card-shell-data-viz.js +0 -12
  436. package/dist/react/ds-chart-bar-stacked.d.ts +0 -9
  437. package/dist/react/ds-chart-bar-stacked.js +0 -12
  438. package/dist/react/ds-select-multi.d.ts +0 -14
  439. package/dist/react/ds-select-multi.js +0 -16
  440. package/dist/react/ds-shell-gradient-picker.d.ts +0 -12
  441. package/dist/react/ds-shell-gradient-picker.js +0 -12
  442. package/dist/react/ds-shell-gradient-swatch.d.ts +0 -12
  443. package/dist/react/ds-shell-gradient-swatch.js +0 -12
  444. package/dist/types/components/CardDataVizBar/CardDataVizBar.d.ts +0 -24
  445. package/dist/types/components/CardDataVizBar/index.d.ts +0 -2
  446. package/dist/types/components/CardDataVizDonut/CardDataVizDonut.d.ts +0 -28
  447. package/dist/types/components/CardDataVizDonut/index.d.ts +0 -2
  448. package/dist/types/components/CardDataVizLine/CardDataVizLine.d.ts +0 -24
  449. package/dist/types/components/CardDataVizLine/index.d.ts +0 -2
  450. package/dist/types/components/CardShellDataViz/CardShellDataViz.d.ts +0 -13
  451. package/dist/types/components/CardShellDataViz/index.d.ts +0 -2
  452. package/dist/types/components/ChartBarStacked/ChartBarStacked.d.ts +0 -26
  453. package/dist/types/components/ChartBarStacked/index.d.ts +0 -3
  454. package/dist/types/components/SelectMulti/SelectMulti.d.ts +0 -121
  455. package/dist/types/components/SelectMulti/index.d.ts +0 -2
  456. package/dist/types/components/ShellGradientPicker/ShellGradientPicker.d.ts +0 -12
  457. package/dist/types/components/ShellGradientSwatch/ShellGradientSwatch.d.ts +0 -15
  458. package/dist/types/components/ShellGradientSwatch/shell-gradient-swatch-types.d.ts +0 -4
  459. /package/dist/components/{p-CgEYdMee.js.map → p-6qXDqCj_.js.map} +0 -0
  460. /package/dist/components/{p-Cr3tEx7o.js.map → p-Ay1AX7Ql.js.map} +0 -0
  461. /package/dist/components/{p-BOCfICln.js.map → p-B1XW4VSn.js.map} +0 -0
  462. /package/dist/components/{p-BKOlvCGS.js.map → p-BUZXuod2.js.map} +0 -0
  463. /package/dist/components/{p-BO5aqZcf.js.map → p-BcTYhmku.js.map} +0 -0
  464. /package/dist/components/{p-BDKtu6Wb.js.map → p-BroVnDzE.js.map} +0 -0
  465. /package/dist/components/{p-CIiBpjCn.js.map → p-BsoqsbBh.js.map} +0 -0
  466. /package/dist/components/{p-C8HU4W8q.js.map → p-CBazaRlE.js.map} +0 -0
  467. /package/dist/components/{p-BhV5e6vF.js.map → p-CHuh71-U.js.map} +0 -0
  468. /package/dist/components/{p-DRnIuCiT.js.map → p-CreGah0j.js.map} +0 -0
  469. /package/dist/components/{p-Cuih680v.js.map → p-Df17mJqI.js.map} +0 -0
  470. /package/dist/components/{p-YR1XeuZf.js.map → p-ohsLFap_.js.map} +0 -0
@@ -77,7 +77,7 @@
77
77
  ],
78
78
  "frameworkCaveats": {
79
79
  "customElements": [
80
- "Assign tabs as a JavaScript property for structured data; tabs-json is the string fallback.",
80
+ "Assign tabs as a JavaScript property and replace the array reference when the collection changes.",
81
81
  "Update tabs, heading, basePath, currentUrl, and navStyle from the same resolved route state."
82
82
  ],
83
83
  "react": [
@@ -156,15 +156,6 @@
156
156
  "mutable": false,
157
157
  "description": "Tab items for the left section.\nSet via JS property: `el.tabs = [...]`. Replace the array reference to update."
158
158
  },
159
- "tabsJson": {
160
- "type": "string",
161
- "resolvedType": "string",
162
- "attribute": "tabs-json",
163
- "default": "''",
164
- "required": false,
165
- "mutable": false,
166
- "description": "JSON fallback for `tabs` — useful when framework bindings don't propagate arrays."
167
- },
168
159
  "value": {
169
160
  "type": "string",
170
161
  "resolvedType": "string",
@@ -240,15 +231,6 @@
240
231
  "mutable": false,
241
232
  "description": "Tab items for the left section.\nSet via JS property: `el.tabs = [...]`. Replace the array reference to update."
242
233
  },
243
- "tabsJson": {
244
- "type": "string",
245
- "resolvedType": "string",
246
- "attribute": "tabs-json",
247
- "default": "''",
248
- "required": false,
249
- "mutable": false,
250
- "description": "JSON fallback for `tabs` — useful when framework bindings don't propagate arrays."
251
- },
252
234
  "value": {
253
235
  "type": "string",
254
236
  "resolvedType": "string",
@@ -294,7 +276,7 @@
294
276
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: tabs.",
295
277
  "peerDependencies": {
296
278
  "required": [
297
- "@ds-mo/tokens ^5.0.0",
279
+ "@ds-mo/tokens ^5.1.0",
298
280
  "@ds-mo/icons ^6.4.0"
299
281
  ],
300
282
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -319,7 +301,7 @@
319
301
  },
320
302
  {
321
303
  "path": "src/wc/components/BarNav/BarNav.tsx",
322
- "content": "import {\n Component,\n Prop,\n Event,\n EventEmitter,\n Element,\n Watch,\n State,\n h,\n Host,\n} from '@stencil/core';\nimport type { NavChromeStyle } from '../../shell/nav-chrome';\nimport type { MenuItemData } from '../Menu/menu-types';\nimport { getSelectableTabs, isTabDivider, type TabItemTab } from '../TabGroup/tab-item-utils';\nimport type { BarNavTab } from './bar-nav-types';\nimport {\n deriveBarNavValueFromUrl,\n parseJsonArrayProp,\n shouldResyncBarNavProps,\n} from './bar-nav-utils';\nimport {\n tabsOverflowContainer,\n tabsToOverflowMenuSections,\n trimTrailingDividers,\n visibleTabCountForWidth,\n} from './bar-nav-tabs-menu-utils';\nimport {\n CHROME_TRANSITION_END,\n CHROME_TRANSITION_START,\n ChromeTransitionGate,\n createRafCoalescer,\n readChromeTransitionSource,\n readChromeTransitionPhase,\n} from '../../shell/chrome-transition';\n\n@Component({\n tag: 'ds-bar-nav',\n styleUrl: 'BarNav.css',\n scoped: true,\n})\nexport class BarNav {\n /** Style slot: `dashboard` or `settings`. Colors match for now. */\n @Prop({ attribute: 'nav-style', reflect: true }) navStyle: NavChromeStyle = 'dashboard';\n\n @Element() el!: HTMLElement;\n\n /**\n * Tab items for the left section.\n * Set via JS property: `el.tabs = [...]`. Replace the array reference to update.\n */\n @Prop() tabs: BarNavTab[] = [];\n\n /** JSON fallback for `tabs` — useful when framework bindings don't propagate arrays. */\n @Prop({ attribute: 'tabs-json' }) tabsJson: string = '';\n\n /** ID of the currently active tab. Overridden when `currentUrl` + `basePath` are set. */\n @Prop({ mutable: true }) value: string = '';\n\n /**\n * Fallback heading shown when no tabs are provided.\n * When tabs are present the heading is hidden.\n */\n @Prop() heading: string | undefined;\n\n /** Section base path (e.g. `/dashboard/safety`). Used with `currentUrl` to derive `value`. */\n @Prop() basePath: string = '';\n\n /** Current route URL. When set with `basePath`, the active tab is derived automatically. */\n @Prop() currentUrl: string = '';\n @Prop() moreTabsLabel: string = 'More tabs';\n\n /** Emitted when the active tab changes. Detail = tab id. */\n @Event() dsTabChange!: EventEmitter<string>;\n\n @State() private resolvedTabs: BarNavTab[] = [];\n @State() private urlDerivedValue: string = '';\n @State() private hideTabsForDetailRoute = false;\n @State() private tabsCollapsed = false;\n @State() private menuOpen = false;\n @State() private menuTriggerActive = false;\n /** When false, defer showing expanded tabs or the collapsed trigger until intrinsic width is measured (avoids a one-frame flash of the full tab row on narrow viewports). */\n @State() private tabLayoutCommitted = false;\n @State() private visibleTabs: BarNavTab[] = [];\n @State() private overflowTabs: BarNavTab[] = [];\n @State() private menuInitialFocusVisible = false;\n @State() private overflowRovingFocused = false;\n @State() private focusedTabId = '';\n\n private static readonly HOST_PROP_SYNC_BUDGET = 8;\n private static readonly INTRINSIC_WIDTH_RETRY_MAX = 3;\n private static readonly TAB_LAYOUT_COMMIT_MAX_FRAMES = 16;\n private static readonly OVERFLOW_HYSTERESIS_PX = 8;\n\n private headerEl: HTMLElement | null = null;\n private triggerEl: (HTMLElement & { setFocus?: () => Promise<void> }) | null = null;\n private menuEl: HTMLDsMenuElement | null = null;\n private visibleTabListEl: HTMLElement | null = null;\n private probeTabListEl: HTMLElement | null = null;\n private resizeObserver: ResizeObserver | null = null;\n private intrinsicWidthRetryCount = 0;\n private tabLayoutPendingFrames = 0;\n private readonly panelNavTransition = new ChromeTransitionGate();\n private readonly panelToolsTransition = new ChromeTransitionGate();\n private readonly overflowCoalescer = createRafCoalescer(() => {\n this.updateTabsCollapsed();\n });\n private chromeTransitionShell: HTMLElement | null = null;\n /** Matches `basePath` once tabs + URL are reconciled for the active section. */\n @State() private committedSection = '';\n /** Section waiting for URL + tabs to catch up after a cross-area navigation. */\n @State() private pendingSection = '';\n\n private get effectiveValue(): string {\n if (this.currentUrl && this.basePath) {\n return this.urlDerivedValue;\n }\n return this.value;\n }\n\n private get renderedSelectableTabs(): TabItemTab[] {\n const tabs = this.hasOverflowTabs ? this.visibleTabs : this.resolvedTabs;\n return getSelectableTabs(tabs);\n }\n\n private get hasOverflowTabs(): boolean {\n return this.tabLayoutCommitted && this.tabsCollapsed && this.overflowTabs.length > 0;\n }\n\n private syncFocusedTabId(preferred: string, tabs: TabItemTab[]) {\n if (tabs.some(tab => tab.id === preferred && !tab.isInactive)) {\n this.focusedTabId = preferred;\n return;\n }\n const first = tabs.find(tab => !tab.isInactive);\n this.focusedTabId = first?.id ?? '';\n }\n\n private digestTabsConfig(tabs: BarNavTab[]): string {\n return tabs.map(t => (isTabDivider(t) ? '|' : t.id)).join(',');\n }\n\n private resetTabOverflowLayout() {\n this.tabLayoutCommitted = false;\n this.tabsCollapsed = false;\n this.menuOpen = false;\n this.menuTriggerActive = false;\n this.menuInitialFocusVisible = false;\n this.intrinsicWidthRetryCount = 0;\n this.tabLayoutPendingFrames = 0;\n this.visibleTabs = [];\n this.overflowTabs = [];\n }\n\n @Watch('tabs')\n @Watch('tabsJson')\n @Watch('currentUrl')\n @Watch('basePath')\n onHostPropsChange() {\n this.applyHostProps();\n }\n\n @Watch('value')\n @Watch('urlDerivedValue')\n onSelectionChange() {\n this.syncFocusedTabId(this.effectiveValue, this.renderedSelectableTabs);\n this.syncMenuSections();\n this.scheduleOverflowCheck();\n }\n\n @Watch('menuOpen')\n onMenuOpenChange(open: boolean) {\n if (this.menuEl) {\n this.menuEl.open = open;\n }\n }\n\n @Watch('tabsCollapsed')\n onTabsCollapsedChange(collapsed: boolean, prevCollapsed: boolean | undefined) {\n if (!collapsed) {\n this.menuOpen = false;\n this.menuTriggerActive = false;\n this.menuInitialFocusVisible = false;\n this.menuEl = null;\n }\n this.scheduleOverflowCheck();\n if (collapsed) {\n requestAnimationFrame(() => {\n this.scheduleOverflowCheck();\n });\n }\n if (collapsed || prevCollapsed === undefined || prevCollapsed === collapsed) return;\n requestAnimationFrame(() => this.scheduleOverflowCheck());\n queueMicrotask(() => this.focusVisibleSelectedTab());\n }\n\n componentWillLoad() {\n this.applyHostProps();\n this.syncFocusedTabId(this.effectiveValue, getSelectableTabs(this.resolvedTabs));\n }\n\n componentDidLoad() {\n this.syncHostPropsIfNeeded();\n this.scheduleDeferredHostPropSync();\n this.setupOverflowObserver();\n this.bindChromeTransitionListeners();\n this.scheduleOverflowCheck();\n }\n\n componentDidRender() {\n if (this.chromeOverflowPaused()) {\n this.syncMenuAnchor();\n return;\n }\n this.scheduleOverflowCheck();\n this.syncMenuAnchor();\n }\n\n disconnectedCallback() {\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n this.overflowCoalescer.cancel();\n this.unbindChromeTransitionListeners();\n }\n\n private bindChromeTransitionListeners() {\n const shell = this.el.closest<HTMLElement>('ds-shell-app');\n if (!shell) return;\n this.chromeTransitionShell = shell;\n shell.addEventListener(CHROME_TRANSITION_START, this.onChromeTransitionStart);\n shell.addEventListener(CHROME_TRANSITION_END, this.onChromeTransitionEnd);\n }\n\n private unbindChromeTransitionListeners() {\n if (!this.chromeTransitionShell) return;\n this.chromeTransitionShell.removeEventListener(\n CHROME_TRANSITION_START,\n this.onChromeTransitionStart,\n );\n this.chromeTransitionShell.removeEventListener(\n CHROME_TRANSITION_END,\n this.onChromeTransitionEnd,\n );\n this.chromeTransitionShell = null;\n }\n\n private onChromeTransitionStart = (event: Event) => {\n const source = readChromeTransitionSource(event);\n if (source === 'panel-nav') {\n this.panelNavTransition.enter();\n return;\n }\n if (source === 'panel-tools') {\n const phase = readChromeTransitionPhase(event) ?? 'opening';\n if (phase === 'closing') {\n this.panelToolsTransition.enter();\n }\n }\n };\n\n private chromeOverflowPaused(): boolean {\n return this.panelNavTransition.isActive || this.panelToolsTransition.isActive;\n }\n\n private onChromeTransitionEnd = (event: Event) => {\n const source = readChromeTransitionSource(event);\n if (source === 'panel-nav') {\n this.panelNavTransition.exit();\n if (!this.panelNavTransition.isActive) {\n this.scheduleOverflowCheck();\n }\n return;\n }\n if (source === 'panel-tools') {\n this.panelToolsTransition.exit();\n this.scheduleOverflowCheck();\n }\n };\n\n /** Re-resolve props assigned by the host after componentWillLoad (Angular ngAfterViewInit). */\n private syncHostPropsIfNeeded() {\n if (shouldResyncBarNavProps(this.resolvedTabs, this.tabs, this.tabsJson)) {\n this.applyHostProps();\n } else if (this.currentUrl && this.basePath) {\n this.syncValueFromUrl();\n }\n }\n\n /** Poll across animation frames — host props may land without triggering @Watch. */\n private scheduleDeferredHostPropSync() {\n let remaining = BarNav.HOST_PROP_SYNC_BUDGET;\n const tick = () => {\n this.syncHostPropsIfNeeded();\n if (--remaining > 0) {\n requestAnimationFrame(tick);\n }\n };\n queueMicrotask(tick);\n }\n\n private setupOverflowObserver() {\n if (typeof ResizeObserver === 'undefined' || !this.headerEl) return;\n this.resizeObserver?.disconnect();\n this.resizeObserver = new ResizeObserver(() => {\n if (this.chromeOverflowPaused()) return;\n this.scheduleOverflowCheck();\n });\n this.resizeObserver.observe(this.headerEl);\n }\n\n private scheduleOverflowCheck() {\n if (this.chromeOverflowPaused()) return;\n this.overflowCoalescer.schedule();\n }\n\n /** Width available for the tab row or collapsed trigger. */\n private getLeftZoneAvailableWidth(): number {\n if (!this.headerEl) return 0;\n\n const headerStyle = getComputedStyle(this.headerEl);\n const horizontalPadding =\n (parseFloat(headerStyle.paddingLeft) || 0) + (parseFloat(headerStyle.paddingRight) || 0);\n\n return Math.max(0, this.headerEl.clientWidth - horizontalPadding);\n }\n\n private getTabsIntrinsicWidth(): number {\n return this.probeTabListEl?.scrollWidth ?? 0;\n }\n\n private getHeaderGap(): number {\n if (!this.headerEl) return 0;\n const style = getComputedStyle(this.headerEl);\n return parseFloat(style.columnGap || style.gap) || 0;\n }\n\n private getOverflowTriggerReserveWidth(): number {\n if (!this.triggerEl) {\n return 40;\n }\n return this.triggerEl.getBoundingClientRect().width + this.getHeaderGap();\n }\n\n private getProbeTabMetrics():\n | {\n itemWidths: number[];\n itemGap: number;\n listExtraWidth: number;\n }\n | null {\n const tabList = this.probeTabListEl;\n if (!tabList) return null;\n\n const children = Array.from(tabList.children) as HTMLElement[];\n const itemWidths = children.map(child => child.getBoundingClientRect().width);\n if (itemWidths.length === 0 || itemWidths.some(width => width === 0)) return null;\n\n const style = getComputedStyle(tabList);\n const itemGap = parseFloat(style.columnGap || style.gap) || 0;\n const childrenWidth = itemWidths.reduce((sum, width) => sum + width, 0);\n const gapWidth = itemGap * Math.max(0, itemWidths.length - 1);\n const listExtraWidth = Math.max(0, tabList.scrollWidth - childrenWidth - gapWidth);\n\n return { itemWidths, itemGap, listExtraWidth };\n }\n\n private setOverflowTabLayout(visibleTabs: BarNavTab[], overflowTabs: BarNavTab[]) {\n const nextCollapsed = overflowTabs.length > 0;\n const visibleChanged =\n this.digestTabsConfig(visibleTabs) !== this.digestTabsConfig(this.visibleTabs);\n const overflowChanged =\n this.digestTabsConfig(overflowTabs) !== this.digestTabsConfig(this.overflowTabs);\n\n if (visibleChanged) {\n this.visibleTabs = visibleTabs;\n }\n if (overflowChanged) {\n this.overflowTabs = overflowTabs;\n }\n if (nextCollapsed !== this.tabsCollapsed) {\n this.tabsCollapsed = nextCollapsed;\n if (!nextCollapsed) {\n this.menuOpen = false;\n this.menuTriggerActive = false;\n }\n }\n this.syncFocusedTabId(\n this.effectiveValue,\n getSelectableTabs(nextCollapsed ? visibleTabs : this.resolvedTabs),\n );\n }\n\n private scheduleIntrinsicWidthRetry() {\n if (this.intrinsicWidthRetryCount >= BarNav.INTRINSIC_WIDTH_RETRY_MAX) return;\n this.intrinsicWidthRetryCount++;\n requestAnimationFrame(() => this.scheduleOverflowCheck());\n }\n\n private forceTabLayoutCommit(fallbackCollapsed: boolean) {\n this.intrinsicWidthRetryCount = 0;\n this.tabLayoutPendingFrames = 0;\n this.setOverflowTabLayout(\n fallbackCollapsed ? [] : this.resolvedTabs,\n fallbackCollapsed ? this.resolvedTabs : [],\n );\n this.tabLayoutCommitted = true;\n }\n\n private updateTabsCollapsed() {\n if (!this.headerEl || this.resolvedTabs.length === 0 || this.hideTabsForDetailRoute) {\n if (this.tabsCollapsed) {\n this.setOverflowTabLayout([], []);\n }\n this.intrinsicWidthRetryCount = 0;\n this.tabLayoutPendingFrames = 0;\n this.tabLayoutCommitted = true;\n return;\n }\n\n const metrics = this.getProbeTabMetrics();\n const intrinsicWidth = this.getTabsIntrinsicWidth();\n if (!metrics || intrinsicWidth === 0) {\n this.tabLayoutPendingFrames += 1;\n if (this.intrinsicWidthRetryCount >= BarNav.INTRINSIC_WIDTH_RETRY_MAX) {\n this.forceTabLayoutCommit(false);\n } else if (this.tabLayoutPendingFrames >= BarNav.TAB_LAYOUT_COMMIT_MAX_FRAMES) {\n this.forceTabLayoutCommit(false);\n } else {\n this.scheduleIntrinsicWidthRetry();\n }\n return;\n }\n this.intrinsicWidthRetryCount = 0;\n this.tabLayoutPendingFrames = 0;\n\n const shouldCollapse = tabsOverflowContainer(\n intrinsicWidth,\n this.getLeftZoneAvailableWidth(),\n this.tabsCollapsed,\n BarNav.OVERFLOW_HYSTERESIS_PX,\n );\n\n if (!shouldCollapse) {\n this.setOverflowTabLayout(this.resolvedTabs, []);\n this.tabLayoutCommitted = true;\n return;\n }\n\n const visibleCount = visibleTabCountForWidth(\n metrics.itemWidths,\n this.getLeftZoneAvailableWidth(),\n this.getOverflowTriggerReserveWidth(),\n metrics.itemGap,\n metrics.listExtraWidth,\n );\n const visibleTabs = trimTrailingDividers(this.resolvedTabs.slice(0, visibleCount));\n const overflowTabs = this.resolvedTabs.slice(visibleTabs.length);\n this.setOverflowTabLayout(visibleTabs, overflowTabs);\n this.tabLayoutCommitted = true;\n }\n\n private syncMenuAnchor() {\n if (!this.menuEl || !this.triggerEl || !this.hasOverflowTabs) return;\n this.menuEl.anchor = this.triggerEl;\n this.syncMenuSections();\n }\n\n private handleMenuSelect(e: Event) {\n const detail = (e as CustomEvent<MenuItemData>).detail;\n if (detail?.value !== undefined) {\n this.selectTab(String(detail.value));\n }\n this.menuOpen = false;\n this.menuInitialFocusVisible = false;\n }\n\n private handleMenuClose() {\n this.menuOpen = false;\n this.menuInitialFocusVisible = false;\n }\n\n private handleMenuAfterClose() {\n if (!this.menuOpen) this.menuTriggerActive = false;\n }\n\n /** Batch tabs/basePath/currentUrl updates so URL derivation never runs with a mixed section. */\n private incomingTabs(): BarNavTab[] {\n return this.tabsJson\n ? parseJsonArrayProp(this.tabsJson, [])\n : (this.tabs ?? []);\n }\n\n private applyHostProps() {\n const nextBasePath = this.basePath;\n const sectionChanged =\n nextBasePath !== '' &&\n this.committedSection !== '' &&\n nextBasePath !== this.committedSection;\n\n if (sectionChanged) {\n this.urlDerivedValue = '';\n this.committedSection = '';\n this.pendingSection = nextBasePath;\n this.resolvedTabs = [];\n this.resetTabOverflowLayout();\n return;\n }\n\n if (this.pendingSection) {\n if (!nextBasePath || nextBasePath !== this.pendingSection) {\n return;\n }\n if (!this.currentUrl.startsWith(this.pendingSection)) {\n return;\n }\n const tabs = this.incomingTabs();\n if (tabs.length === 0) {\n return;\n }\n this.pendingSection = '';\n }\n\n const tabs = this.incomingTabs();\n const tabsConfigChanged =\n this.digestTabsConfig(tabs) !== this.digestTabsConfig(this.resolvedTabs);\n\n this.resolvedTabs = tabs;\n this.syncValueFromUrl();\n this.committedSection = nextBasePath;\n if (tabsConfigChanged) {\n this.resetTabOverflowLayout();\n }\n this.scheduleOverflowCheck();\n }\n\n private syncMenuSections() {\n if (!this.menuEl) return;\n this.menuEl.sections = tabsToOverflowMenuSections(this.overflowTabs, this.effectiveValue);\n }\n\n private syncValueFromUrl() {\n const tabs = this.incomingTabs();\n\n if (!this.currentUrl || !this.basePath) {\n this.urlDerivedValue = '';\n this.hideTabsForDetailRoute = false;\n return;\n }\n\n if (!this.currentUrl.startsWith(this.basePath)) {\n this.urlDerivedValue = '';\n this.hideTabsForDetailRoute = false;\n return;\n }\n\n const { value, hideTabs } = deriveBarNavValueFromUrl(\n this.currentUrl,\n this.basePath,\n tabs,\n );\n this.urlDerivedValue = value;\n this.hideTabsForDetailRoute = hideTabs;\n }\n\n private selectTab(id: string) {\n this.value = id;\n this.dsTabChange.emit(id);\n }\n\n /** Move focus to the selected tab in the visible tab group (after expand). */\n private focusVisibleSelectedTab() {\n const tab = this.visibleTabListEl?.querySelector(\n `[data-tab-id=\"${this.effectiveValue}\"]`,\n ) as HTMLElement | null;\n tab?.focus({ preventScroll: true });\n }\n\n private focusVisibleTab(id: string) {\n this.focusedTabId = id;\n const tab = this.visibleTabListEl?.querySelector(\n `[data-tab-id=\"${id}\"]`,\n ) as HTMLElement | null;\n tab?.focus({ preventScroll: true });\n }\n\n private focusLastVisibleTab() {\n const last = [...this.renderedSelectableTabs].reverse().find(tab => !tab.isInactive);\n if (last) this.focusVisibleTab(last.id);\n }\n\n private findEnabledVisibleTab(from: number, step: 1 | -1): number | null {\n const tabs = this.renderedSelectableTabs;\n for (let index = from + step; index >= 0 && index < tabs.length; index += step) {\n if (!tabs[index]?.isInactive) return index;\n }\n return null;\n }\n\n private handleTabKeyDown(e: KeyboardEvent) {\n const tabs = this.renderedSelectableTabs;\n if (!tabs.length) return;\n const targetId = (e.target as HTMLElement | null)?.getAttribute('data-tab-id');\n const focusedId = targetId ?? this.focusedTabId;\n\n if (e.key === 'Enter' || e.key === ' ') {\n const focused = tabs.find(tab => tab.id === focusedId);\n if (!focused || focused.isInactive) return;\n e.preventDefault();\n this.selectTab(focused.id);\n return;\n }\n\n const currentIndex = tabs.findIndex(tab => tab.id === focusedId);\n if (currentIndex < 0) return;\n\n let nextIndex: number | null;\n if (e.key === 'ArrowRight') {\n nextIndex = this.findEnabledVisibleTab(currentIndex, 1);\n if (nextIndex === null && this.hasOverflowTabs) {\n e.preventDefault();\n this.overflowRovingFocused = true;\n void this.triggerEl?.setFocus?.();\n return;\n }\n } else if (e.key === 'ArrowLeft') {\n nextIndex = this.findEnabledVisibleTab(currentIndex, -1);\n } else if (e.key === 'Home') {\n nextIndex = tabs.findIndex(tab => !tab.isInactive);\n } else if (e.key === 'End') {\n const reversedIndex = [...tabs].reverse().findIndex(tab => !tab.isInactive);\n nextIndex = reversedIndex < 0 ? null : tabs.length - 1 - reversedIndex;\n } else {\n return;\n }\n\n if (nextIndex === null || nextIndex < 0 || nextIndex === currentIndex) return;\n e.preventDefault();\n this.focusVisibleTab(tabs[nextIndex].id);\n }\n\n private toggleTabMenu(options?: { focusVisible?: boolean }) {\n if (this.menuOpen) {\n this.menuOpen = false;\n this.menuInitialFocusVisible = false;\n return;\n }\n\n if (this.menuEl && this.triggerEl) {\n this.menuEl.anchor = this.triggerEl;\n this.menuEl.minWidth = '';\n this.syncMenuSections();\n }\n\n this.menuInitialFocusVisible = options?.focusVisible ?? false;\n this.menuTriggerActive = true;\n this.menuOpen = true;\n }\n\n private handleOverflowFocus = () => {\n this.overflowRovingFocused = true;\n };\n\n private handleOverflowBlur = () => {\n this.overflowRovingFocused = false;\n };\n\n private handleTriggerKeyDown(e: KeyboardEvent) {\n if (!this.hasOverflowTabs) return;\n\n if (!this.menuOpen) {\n if (e.key === 'ArrowLeft') {\n e.preventDefault();\n this.overflowRovingFocused = false;\n this.focusLastVisibleTab();\n return;\n }\n\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n this.toggleTabMenu({ focusVisible: true });\n }\n return;\n }\n\n if (e.key === 'Escape') {\n e.preventDefault();\n this.menuOpen = false;\n this.menuInitialFocusVisible = false;\n void this.triggerEl?.setFocus?.();\n }\n }\n\n private renderTabList(tabs: BarNavTab[], probe: boolean) {\n return (\n <div\n role=\"tablist\"\n class=\"bar-nav__tab-list\"\n aria-label={this.heading}\n ref={el => {\n if (probe) {\n const next = (el as HTMLElement) ?? null;\n if (next === this.probeTabListEl) return;\n this.probeTabListEl = next;\n if (next) {\n this.intrinsicWidthRetryCount = 0;\n this.tabLayoutPendingFrames = 0;\n this.scheduleOverflowCheck();\n }\n } else {\n this.visibleTabListEl = (el as HTMLElement) ?? null;\n }\n }}\n onKeyDown={probe ? undefined : (e: KeyboardEvent) => this.handleTabKeyDown(e)}\n onFocusin={probe ? undefined : () => {\n this.overflowRovingFocused = false;\n }}\n >\n {tabs.map((tab, index) => {\n if (isTabDivider(tab)) {\n return (\n <div class=\"bar-nav__tab-divider\" key={`divider-${index}`} aria-hidden=\"true\">\n <div class=\"bar-nav__tab-divider-line\" />\n </div>\n );\n }\n\n const isSelected = tab.id === this.effectiveValue;\n return (\n <button\n key={tab.id}\n type=\"button\"\n role=\"tab\"\n data-tab-id={tab.id}\n class={{\n 'bar-nav__tab': true,\n 'bar-nav__tab--selected': isSelected,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-control-inactive': !!tab.isInactive,\n }}\n aria-selected={isSelected ? 'true' : 'false'}\n aria-disabled={tab.isInactive ? 'true' : undefined}\n aria-controls={tab.panelId ?? undefined}\n disabled={tab.isInactive}\n tabIndex={\n !probe && !this.overflowRovingFocused && tab.id === this.focusedTabId\n ? 0\n : -1\n }\n onClick={() => !probe && !tab.isInactive && this.selectTab(tab.id)}\n onFocus={() => {\n if (!probe) this.focusedTabId = tab.id;\n }}\n >\n <span\n class={{\n 'bar-nav__tab-label': true,\n 'bar-nav__tab-label--dot': !!tab.dot,\n }}\n >\n <ds-text as=\"span\" variant=\"text-body-medium\" emphasis={isSelected} color=\"inherit\">\n {tab.label}\n </ds-text>\n {tab.dot && (\n <ds-badge\n class=\"bar-nav__tab-dot\"\n variant=\"dot\"\n hasRing={false}\n label=\"\"\n aria-hidden=\"true\"\n />\n )}\n </span>\n </button>\n );\n })}\n </div>\n );\n }\n\n render() {\n // Section gating only applies when `basePath` is set (SPA section sync).\n // Tabs without a base path must still render — otherwise a single-tab bar\n // (or any non-routed tabs) silently falls through to an empty header.\n const sectionReady =\n this.basePath === '' || this.committedSection === this.basePath;\n const hasTabs =\n sectionReady && this.resolvedTabs.length > 0 && !this.hideTabsForDetailRoute;\n const tabGroupKey = this.basePath || 'no-section';\n const renderedTabs = this.hasOverflowTabs ? this.visibleTabs : this.resolvedTabs;\n\n return (\n <Host>\n <header\n class={{\n 'bar-nav': true,\n 'ds-control--md': true,\n 'ds-chrome-row': true,\n 'ds-chrome-space--md': true,\n 'bar-nav--dashboard': this.navStyle === 'dashboard',\n 'bar-nav--settings': this.navStyle === 'settings',\n 'bar-nav--tabs-collapsed':\n hasTabs && this.hasOverflowTabs,\n }}\n ref={el => {\n this.headerEl = el as HTMLElement;\n if (el && !this.resizeObserver) {\n this.setupOverflowObserver();\n }\n }}\n >\n {hasTabs && (\n <div class=\"bar-nav__tabs-probe\" aria-hidden=\"true\" inert>\n <div key={`probe-${tabGroupKey}`}>\n {this.renderTabList(this.resolvedTabs, true)}\n </div>\n </div>\n )}\n\n {hasTabs && this.tabLayoutCommitted && renderedTabs.length > 0 && (\n <div class=\"bar-nav__left\">\n <div key={`visible-${tabGroupKey}`} class=\"bar-nav__tabs-visible\">\n {this.renderTabList(renderedTabs, false)}\n </div>\n </div>\n )}\n\n {hasTabs && !this.tabLayoutCommitted && (\n <div class=\"bar-nav__tabs-pending\" aria-hidden=\"true\" />\n )}\n\n {!hasTabs && this.heading && (\n <ds-text\n class=\"bar-nav__heading\"\n lineTruncation={1}\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis\n color=\"primary\"\n wrap=\"nowrap\"\n >\n {this.heading}\n </ds-text>\n )}\n\n {hasTabs && this.hasOverflowTabs && (\n <ds-button-unfilled variant=\"icon\"\n class={{\n 'bar-nav__overflow-trigger': true,\n }}\n icon=\"Ellipses\"\n activeFill={false}\n hasBorder={false}\n focusTabIndex={this.overflowRovingFocused ? 0 : -1}\n ref={(el?: HTMLDsButtonUnfilledElement) => {\n this.triggerEl = (el as (HTMLElement & { setFocus?: () => Promise<void> })) ?? null;\n }}\n haspopup=\"menu\"\n expanded={this.menuTriggerActive}\n aria-label={this.moreTabsLabel}\n onDsClick={() => this.toggleTabMenu({ focusVisible: false })}\n onFocusin={this.handleOverflowFocus}\n onFocusout={this.handleOverflowBlur}\n onKeyDown={(e: KeyboardEvent) => this.handleTriggerKeyDown(e)}\n />\n )}\n\n </header>\n\n {hasTabs && this.hasOverflowTabs && (\n <ds-menu\n ref={el => {\n this.menuEl = (el as HTMLDsMenuElement) ?? null;\n }}\n open={this.menuOpen}\n sections={tabsToOverflowMenuSections(this.overflowTabs, this.effectiveValue)}\n side=\"bottom\"\n align=\"end\"\n sideOffset=\"calc(var(--dimension-space-100) + var(--dimension-space-050))\"\n initialFocusVisible={this.menuInitialFocusVisible}\n onDsSelect={(e: Event) => this.handleMenuSelect(e)}\n onDsClose={() => this.handleMenuClose()}\n onDsAfterClose={() => this.handleMenuAfterClose()}\n />\n )}\n </Host>\n );\n }\n}\n",
304
+ "content": "import {\n Component,\n Prop,\n Event,\n EventEmitter,\n Element,\n Watch,\n State,\n h,\n Host,\n} from '@stencil/core';\nimport type { NavChromeStyle } from '../../shell/nav-chrome';\nimport type { MenuItemData } from '../Menu/menu-types';\nimport { getSelectableTabs, isTabDivider, type TabItemTab } from '../TabGroup/tab-item-utils';\nimport type { BarNavTab } from './bar-nav-types';\nimport {\n deriveBarNavValueFromUrl,\n shouldResyncBarNavProps,\n} from './bar-nav-utils';\nimport {\n tabsOverflowContainer,\n tabsToOverflowMenuSections,\n trimTrailingDividers,\n visibleTabCountForWidth,\n} from './bar-nav-tabs-menu-utils';\nimport {\n CHROME_TRANSITION_END,\n CHROME_TRANSITION_START,\n ChromeTransitionGate,\n createRafCoalescer,\n readChromeTransitionSource,\n readChromeTransitionPhase,\n} from '../../shell/chrome-transition';\n\n@Component({\n tag: 'ds-bar-nav',\n styleUrl: 'BarNav.css',\n scoped: true,\n})\nexport class BarNav {\n /** Style slot: `dashboard` or `settings`. Colors match for now. */\n @Prop({ attribute: 'nav-style', reflect: true }) navStyle: NavChromeStyle = 'dashboard';\n\n @Element() el!: HTMLElement;\n\n /**\n * Tab items for the left section.\n * Set via JS property: `el.tabs = [...]`. Replace the array reference to update.\n */\n @Prop() tabs: BarNavTab[] = [];\n\n /** ID of the currently active tab. Overridden when `currentUrl` + `basePath` are set. */\n @Prop({ mutable: true }) value: string = '';\n\n /**\n * Fallback heading shown when no tabs are provided.\n * When tabs are present the heading is hidden.\n */\n @Prop() heading: string | undefined;\n\n /** Section base path (e.g. `/dashboard/safety`). Used with `currentUrl` to derive `value`. */\n @Prop() basePath: string = '';\n\n /** Current route URL. When set with `basePath`, the active tab is derived automatically. */\n @Prop() currentUrl: string = '';\n @Prop() moreTabsLabel: string = 'More tabs';\n\n /** Emitted when the active tab changes. Detail = tab id. */\n @Event() dsTabChange!: EventEmitter<string>;\n\n @State() private resolvedTabs: BarNavTab[] = [];\n @State() private urlDerivedValue: string = '';\n @State() private hideTabsForDetailRoute = false;\n @State() private tabsCollapsed = false;\n @State() private menuOpen = false;\n @State() private menuTriggerActive = false;\n /** When false, defer showing expanded tabs or the collapsed trigger until intrinsic width is measured (avoids a one-frame flash of the full tab row on narrow viewports). */\n @State() private tabLayoutCommitted = false;\n @State() private visibleTabs: BarNavTab[] = [];\n @State() private overflowTabs: BarNavTab[] = [];\n @State() private menuInitialFocusVisible = false;\n @State() private overflowRovingFocused = false;\n @State() private focusedTabId = '';\n\n private static readonly HOST_PROP_SYNC_BUDGET = 8;\n private static readonly INTRINSIC_WIDTH_RETRY_MAX = 3;\n private static readonly TAB_LAYOUT_COMMIT_MAX_FRAMES = 16;\n private static readonly OVERFLOW_HYSTERESIS_PX = 8;\n\n private headerEl: HTMLElement | null = null;\n private triggerEl: (HTMLElement & { setFocus?: () => Promise<void> }) | null = null;\n private menuEl: HTMLDsMenuElement | null = null;\n private visibleTabListEl: HTMLElement | null = null;\n private probeTabListEl: HTMLElement | null = null;\n private resizeObserver: ResizeObserver | null = null;\n private intrinsicWidthRetryCount = 0;\n private tabLayoutPendingFrames = 0;\n private readonly panelNavTransition = new ChromeTransitionGate();\n private readonly panelToolsTransition = new ChromeTransitionGate();\n private readonly overflowCoalescer = createRafCoalescer(() => {\n this.updateTabsCollapsed();\n });\n private chromeTransitionShell: HTMLElement | null = null;\n /** Matches `basePath` once tabs + URL are reconciled for the active section. */\n @State() private committedSection = '';\n /** Section waiting for URL + tabs to catch up after a cross-area navigation. */\n @State() private pendingSection = '';\n\n private get effectiveValue(): string {\n if (this.currentUrl && this.basePath) {\n return this.urlDerivedValue;\n }\n return this.value;\n }\n\n private get renderedSelectableTabs(): TabItemTab[] {\n const tabs = this.hasOverflowTabs ? this.visibleTabs : this.resolvedTabs;\n return getSelectableTabs(tabs);\n }\n\n private get hasOverflowTabs(): boolean {\n return this.tabLayoutCommitted && this.tabsCollapsed && this.overflowTabs.length > 0;\n }\n\n private syncFocusedTabId(preferred: string, tabs: TabItemTab[]) {\n if (tabs.some(tab => tab.id === preferred && !tab.isInactive)) {\n this.focusedTabId = preferred;\n return;\n }\n const first = tabs.find(tab => !tab.isInactive);\n this.focusedTabId = first?.id ?? '';\n }\n\n private digestTabsConfig(tabs: BarNavTab[]): string {\n return tabs.map(t => (isTabDivider(t) ? '|' : t.id)).join(',');\n }\n\n private resetTabOverflowLayout() {\n this.tabLayoutCommitted = false;\n this.tabsCollapsed = false;\n this.menuOpen = false;\n this.menuTriggerActive = false;\n this.menuInitialFocusVisible = false;\n this.intrinsicWidthRetryCount = 0;\n this.tabLayoutPendingFrames = 0;\n this.visibleTabs = [];\n this.overflowTabs = [];\n }\n\n @Watch('tabs')\n @Watch('currentUrl')\n @Watch('basePath')\n onHostPropsChange() {\n this.applyHostProps();\n }\n\n @Watch('value')\n @Watch('urlDerivedValue')\n onSelectionChange() {\n this.syncFocusedTabId(this.effectiveValue, this.renderedSelectableTabs);\n this.syncMenuSections();\n this.scheduleOverflowCheck();\n }\n\n @Watch('menuOpen')\n onMenuOpenChange(open: boolean) {\n if (this.menuEl) {\n this.menuEl.open = open;\n }\n }\n\n @Watch('tabsCollapsed')\n onTabsCollapsedChange(collapsed: boolean, prevCollapsed: boolean | undefined) {\n if (!collapsed) {\n this.menuOpen = false;\n this.menuTriggerActive = false;\n this.menuInitialFocusVisible = false;\n this.menuEl = null;\n }\n this.scheduleOverflowCheck();\n if (collapsed) {\n requestAnimationFrame(() => {\n this.scheduleOverflowCheck();\n });\n }\n if (collapsed || prevCollapsed === undefined || prevCollapsed === collapsed) return;\n requestAnimationFrame(() => this.scheduleOverflowCheck());\n queueMicrotask(() => this.focusVisibleSelectedTab());\n }\n\n componentWillLoad() {\n this.applyHostProps();\n this.syncFocusedTabId(this.effectiveValue, getSelectableTabs(this.resolvedTabs));\n }\n\n componentDidLoad() {\n this.syncHostPropsIfNeeded();\n this.scheduleDeferredHostPropSync();\n this.setupOverflowObserver();\n this.bindChromeTransitionListeners();\n this.scheduleOverflowCheck();\n }\n\n componentDidRender() {\n if (this.chromeOverflowPaused()) {\n this.syncMenuAnchor();\n return;\n }\n this.scheduleOverflowCheck();\n this.syncMenuAnchor();\n }\n\n disconnectedCallback() {\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n this.overflowCoalescer.cancel();\n this.unbindChromeTransitionListeners();\n }\n\n private bindChromeTransitionListeners() {\n const shell = this.el.closest<HTMLElement>('ds-shell-app');\n if (!shell) return;\n this.chromeTransitionShell = shell;\n shell.addEventListener(CHROME_TRANSITION_START, this.onChromeTransitionStart);\n shell.addEventListener(CHROME_TRANSITION_END, this.onChromeTransitionEnd);\n }\n\n private unbindChromeTransitionListeners() {\n if (!this.chromeTransitionShell) return;\n this.chromeTransitionShell.removeEventListener(\n CHROME_TRANSITION_START,\n this.onChromeTransitionStart,\n );\n this.chromeTransitionShell.removeEventListener(\n CHROME_TRANSITION_END,\n this.onChromeTransitionEnd,\n );\n this.chromeTransitionShell = null;\n }\n\n private onChromeTransitionStart = (event: Event) => {\n const source = readChromeTransitionSource(event);\n if (source === 'panel-nav') {\n this.panelNavTransition.enter();\n return;\n }\n if (source === 'panel-tools') {\n const phase = readChromeTransitionPhase(event) ?? 'opening';\n if (phase === 'closing') {\n this.panelToolsTransition.enter();\n }\n }\n };\n\n private chromeOverflowPaused(): boolean {\n return this.panelNavTransition.isActive || this.panelToolsTransition.isActive;\n }\n\n private onChromeTransitionEnd = (event: Event) => {\n const source = readChromeTransitionSource(event);\n if (source === 'panel-nav') {\n this.panelNavTransition.exit();\n if (!this.panelNavTransition.isActive) {\n this.scheduleOverflowCheck();\n }\n return;\n }\n if (source === 'panel-tools') {\n this.panelToolsTransition.exit();\n this.scheduleOverflowCheck();\n }\n };\n\n /** Re-resolve props assigned by the host after componentWillLoad (Angular ngAfterViewInit). */\n private syncHostPropsIfNeeded() {\n if (shouldResyncBarNavProps(this.resolvedTabs, this.tabs)) {\n this.applyHostProps();\n } else if (this.currentUrl && this.basePath) {\n this.syncValueFromUrl();\n }\n }\n\n /** Poll across animation frames — host props may land without triggering @Watch. */\n private scheduleDeferredHostPropSync() {\n let remaining = BarNav.HOST_PROP_SYNC_BUDGET;\n const tick = () => {\n this.syncHostPropsIfNeeded();\n if (--remaining > 0) {\n requestAnimationFrame(tick);\n }\n };\n queueMicrotask(tick);\n }\n\n private setupOverflowObserver() {\n if (typeof ResizeObserver === 'undefined' || !this.headerEl) return;\n this.resizeObserver?.disconnect();\n this.resizeObserver = new ResizeObserver(() => {\n if (this.chromeOverflowPaused()) return;\n this.scheduleOverflowCheck();\n });\n this.resizeObserver.observe(this.headerEl);\n }\n\n private scheduleOverflowCheck() {\n if (this.chromeOverflowPaused()) return;\n this.overflowCoalescer.schedule();\n }\n\n /** Width available for the tab row or collapsed trigger. */\n private getLeftZoneAvailableWidth(): number {\n if (!this.headerEl) return 0;\n\n const headerStyle = getComputedStyle(this.headerEl);\n const horizontalPadding =\n (parseFloat(headerStyle.paddingLeft) || 0) + (parseFloat(headerStyle.paddingRight) || 0);\n\n return Math.max(0, this.headerEl.clientWidth - horizontalPadding);\n }\n\n private getTabsIntrinsicWidth(): number {\n return this.probeTabListEl?.scrollWidth ?? 0;\n }\n\n private getHeaderGap(): number {\n if (!this.headerEl) return 0;\n const style = getComputedStyle(this.headerEl);\n return parseFloat(style.columnGap || style.gap) || 0;\n }\n\n private getOverflowTriggerReserveWidth(): number {\n if (!this.triggerEl) {\n return 40;\n }\n return this.triggerEl.getBoundingClientRect().width + this.getHeaderGap();\n }\n\n private getProbeTabMetrics():\n | {\n itemWidths: number[];\n itemGap: number;\n listExtraWidth: number;\n }\n | null {\n const tabList = this.probeTabListEl;\n if (!tabList) return null;\n\n const children = Array.from(tabList.children) as HTMLElement[];\n const itemWidths = children.map(child => child.getBoundingClientRect().width);\n if (itemWidths.length === 0 || itemWidths.some(width => width === 0)) return null;\n\n const style = getComputedStyle(tabList);\n const itemGap = parseFloat(style.columnGap || style.gap) || 0;\n const childrenWidth = itemWidths.reduce((sum, width) => sum + width, 0);\n const gapWidth = itemGap * Math.max(0, itemWidths.length - 1);\n const listExtraWidth = Math.max(0, tabList.scrollWidth - childrenWidth - gapWidth);\n\n return { itemWidths, itemGap, listExtraWidth };\n }\n\n private setOverflowTabLayout(visibleTabs: BarNavTab[], overflowTabs: BarNavTab[]) {\n const nextCollapsed = overflowTabs.length > 0;\n const visibleChanged =\n this.digestTabsConfig(visibleTabs) !== this.digestTabsConfig(this.visibleTabs);\n const overflowChanged =\n this.digestTabsConfig(overflowTabs) !== this.digestTabsConfig(this.overflowTabs);\n\n if (visibleChanged) {\n this.visibleTabs = visibleTabs;\n }\n if (overflowChanged) {\n this.overflowTabs = overflowTabs;\n }\n if (nextCollapsed !== this.tabsCollapsed) {\n this.tabsCollapsed = nextCollapsed;\n if (!nextCollapsed) {\n this.menuOpen = false;\n this.menuTriggerActive = false;\n }\n }\n this.syncFocusedTabId(\n this.effectiveValue,\n getSelectableTabs(nextCollapsed ? visibleTabs : this.resolvedTabs),\n );\n }\n\n private scheduleIntrinsicWidthRetry() {\n if (this.intrinsicWidthRetryCount >= BarNav.INTRINSIC_WIDTH_RETRY_MAX) return;\n this.intrinsicWidthRetryCount++;\n requestAnimationFrame(() => this.scheduleOverflowCheck());\n }\n\n private forceTabLayoutCommit(fallbackCollapsed: boolean) {\n this.intrinsicWidthRetryCount = 0;\n this.tabLayoutPendingFrames = 0;\n this.setOverflowTabLayout(\n fallbackCollapsed ? [] : this.resolvedTabs,\n fallbackCollapsed ? this.resolvedTabs : [],\n );\n this.tabLayoutCommitted = true;\n }\n\n private updateTabsCollapsed() {\n if (!this.headerEl || this.resolvedTabs.length === 0 || this.hideTabsForDetailRoute) {\n if (this.tabsCollapsed) {\n this.setOverflowTabLayout([], []);\n }\n this.intrinsicWidthRetryCount = 0;\n this.tabLayoutPendingFrames = 0;\n this.tabLayoutCommitted = true;\n return;\n }\n\n const metrics = this.getProbeTabMetrics();\n const intrinsicWidth = this.getTabsIntrinsicWidth();\n if (!metrics || intrinsicWidth === 0) {\n this.tabLayoutPendingFrames += 1;\n if (this.intrinsicWidthRetryCount >= BarNav.INTRINSIC_WIDTH_RETRY_MAX) {\n this.forceTabLayoutCommit(false);\n } else if (this.tabLayoutPendingFrames >= BarNav.TAB_LAYOUT_COMMIT_MAX_FRAMES) {\n this.forceTabLayoutCommit(false);\n } else {\n this.scheduleIntrinsicWidthRetry();\n }\n return;\n }\n this.intrinsicWidthRetryCount = 0;\n this.tabLayoutPendingFrames = 0;\n\n const shouldCollapse = tabsOverflowContainer(\n intrinsicWidth,\n this.getLeftZoneAvailableWidth(),\n this.tabsCollapsed,\n BarNav.OVERFLOW_HYSTERESIS_PX,\n );\n\n if (!shouldCollapse) {\n this.setOverflowTabLayout(this.resolvedTabs, []);\n this.tabLayoutCommitted = true;\n return;\n }\n\n const visibleCount = visibleTabCountForWidth(\n metrics.itemWidths,\n this.getLeftZoneAvailableWidth(),\n this.getOverflowTriggerReserveWidth(),\n metrics.itemGap,\n metrics.listExtraWidth,\n );\n const visibleTabs = trimTrailingDividers(this.resolvedTabs.slice(0, visibleCount));\n const overflowTabs = this.resolvedTabs.slice(visibleTabs.length);\n this.setOverflowTabLayout(visibleTabs, overflowTabs);\n this.tabLayoutCommitted = true;\n }\n\n private syncMenuAnchor() {\n if (!this.menuEl || !this.triggerEl || !this.hasOverflowTabs) return;\n this.menuEl.anchor = this.triggerEl;\n this.syncMenuSections();\n }\n\n private handleMenuSelect(e: Event) {\n const detail = (e as CustomEvent<MenuItemData>).detail;\n if (detail?.value !== undefined) {\n this.selectTab(String(detail.value));\n }\n this.menuOpen = false;\n this.menuInitialFocusVisible = false;\n }\n\n private handleMenuClose() {\n this.menuOpen = false;\n this.menuInitialFocusVisible = false;\n }\n\n private handleMenuAfterClose() {\n if (!this.menuOpen) this.menuTriggerActive = false;\n }\n\n /** Batch tabs/basePath/currentUrl updates so URL derivation never runs with a mixed section. */\n private incomingTabs(): BarNavTab[] {\n return this.tabs ?? [];\n }\n\n private applyHostProps() {\n const nextBasePath = this.basePath;\n const sectionChanged =\n nextBasePath !== '' &&\n this.committedSection !== '' &&\n nextBasePath !== this.committedSection;\n\n if (sectionChanged) {\n this.urlDerivedValue = '';\n this.committedSection = '';\n this.pendingSection = nextBasePath;\n this.resolvedTabs = [];\n this.resetTabOverflowLayout();\n return;\n }\n\n if (this.pendingSection) {\n if (!nextBasePath || nextBasePath !== this.pendingSection) {\n return;\n }\n if (!this.currentUrl.startsWith(this.pendingSection)) {\n return;\n }\n const tabs = this.incomingTabs();\n if (tabs.length === 0) {\n return;\n }\n this.pendingSection = '';\n }\n\n const tabs = this.incomingTabs();\n const tabsConfigChanged =\n this.digestTabsConfig(tabs) !== this.digestTabsConfig(this.resolvedTabs);\n\n this.resolvedTabs = tabs;\n this.syncValueFromUrl();\n this.committedSection = nextBasePath;\n if (tabsConfigChanged) {\n this.resetTabOverflowLayout();\n }\n this.scheduleOverflowCheck();\n }\n\n private syncMenuSections() {\n if (!this.menuEl) return;\n this.menuEl.sections = tabsToOverflowMenuSections(this.overflowTabs, this.effectiveValue);\n }\n\n private syncValueFromUrl() {\n const tabs = this.incomingTabs();\n\n if (!this.currentUrl || !this.basePath) {\n this.urlDerivedValue = '';\n this.hideTabsForDetailRoute = false;\n return;\n }\n\n if (!this.currentUrl.startsWith(this.basePath)) {\n this.urlDerivedValue = '';\n this.hideTabsForDetailRoute = false;\n return;\n }\n\n const { value, hideTabs } = deriveBarNavValueFromUrl(\n this.currentUrl,\n this.basePath,\n tabs,\n );\n this.urlDerivedValue = value;\n this.hideTabsForDetailRoute = hideTabs;\n }\n\n private selectTab(id: string) {\n this.value = id;\n this.dsTabChange.emit(id);\n }\n\n /** Move focus to the selected tab in the visible tab group (after expand). */\n private focusVisibleSelectedTab() {\n const tab = this.visibleTabListEl?.querySelector(\n `[data-tab-id=\"${this.effectiveValue}\"]`,\n ) as HTMLElement | null;\n tab?.focus({ preventScroll: true });\n }\n\n private focusVisibleTab(id: string) {\n this.focusedTabId = id;\n const tab = this.visibleTabListEl?.querySelector(\n `[data-tab-id=\"${id}\"]`,\n ) as HTMLElement | null;\n tab?.focus({ preventScroll: true });\n }\n\n private focusLastVisibleTab() {\n const last = [...this.renderedSelectableTabs].reverse().find(tab => !tab.isInactive);\n if (last) this.focusVisibleTab(last.id);\n }\n\n private findEnabledVisibleTab(from: number, step: 1 | -1): number | null {\n const tabs = this.renderedSelectableTabs;\n for (let index = from + step; index >= 0 && index < tabs.length; index += step) {\n if (!tabs[index]?.isInactive) return index;\n }\n return null;\n }\n\n private handleTabKeyDown(e: KeyboardEvent) {\n const tabs = this.renderedSelectableTabs;\n if (!tabs.length) return;\n const targetId = (e.target as HTMLElement | null)?.getAttribute('data-tab-id');\n const focusedId = targetId ?? this.focusedTabId;\n\n if (e.key === 'Enter' || e.key === ' ') {\n const focused = tabs.find(tab => tab.id === focusedId);\n if (!focused || focused.isInactive) return;\n e.preventDefault();\n this.selectTab(focused.id);\n return;\n }\n\n const currentIndex = tabs.findIndex(tab => tab.id === focusedId);\n if (currentIndex < 0) return;\n\n let nextIndex: number | null;\n if (e.key === 'ArrowRight') {\n nextIndex = this.findEnabledVisibleTab(currentIndex, 1);\n if (nextIndex === null && this.hasOverflowTabs) {\n e.preventDefault();\n this.overflowRovingFocused = true;\n void this.triggerEl?.setFocus?.();\n return;\n }\n } else if (e.key === 'ArrowLeft') {\n nextIndex = this.findEnabledVisibleTab(currentIndex, -1);\n } else if (e.key === 'Home') {\n nextIndex = tabs.findIndex(tab => !tab.isInactive);\n } else if (e.key === 'End') {\n const reversedIndex = [...tabs].reverse().findIndex(tab => !tab.isInactive);\n nextIndex = reversedIndex < 0 ? null : tabs.length - 1 - reversedIndex;\n } else {\n return;\n }\n\n if (nextIndex === null || nextIndex < 0 || nextIndex === currentIndex) return;\n e.preventDefault();\n this.focusVisibleTab(tabs[nextIndex].id);\n }\n\n private toggleTabMenu(options?: { focusVisible?: boolean }) {\n if (this.menuOpen) {\n this.menuOpen = false;\n this.menuInitialFocusVisible = false;\n return;\n }\n\n if (this.menuEl && this.triggerEl) {\n this.menuEl.anchor = this.triggerEl;\n this.menuEl.minWidth = '';\n this.syncMenuSections();\n }\n\n this.menuInitialFocusVisible = options?.focusVisible ?? false;\n this.menuTriggerActive = true;\n this.menuOpen = true;\n }\n\n private handleOverflowFocus = () => {\n this.overflowRovingFocused = true;\n };\n\n private handleOverflowBlur = () => {\n this.overflowRovingFocused = false;\n };\n\n private handleTriggerKeyDown(e: KeyboardEvent) {\n if (!this.hasOverflowTabs) return;\n\n if (!this.menuOpen) {\n if (e.key === 'ArrowLeft') {\n e.preventDefault();\n this.overflowRovingFocused = false;\n this.focusLastVisibleTab();\n return;\n }\n\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n this.toggleTabMenu({ focusVisible: true });\n }\n return;\n }\n\n if (e.key === 'Escape') {\n e.preventDefault();\n this.menuOpen = false;\n this.menuInitialFocusVisible = false;\n void this.triggerEl?.setFocus?.();\n }\n }\n\n private renderTabList(tabs: BarNavTab[], probe: boolean) {\n return (\n <div\n role=\"tablist\"\n class=\"bar-nav__tab-list\"\n aria-label={this.heading}\n ref={el => {\n if (probe) {\n const next = (el as HTMLElement) ?? null;\n if (next === this.probeTabListEl) return;\n this.probeTabListEl = next;\n if (next) {\n this.intrinsicWidthRetryCount = 0;\n this.tabLayoutPendingFrames = 0;\n this.scheduleOverflowCheck();\n }\n } else {\n this.visibleTabListEl = (el as HTMLElement) ?? null;\n }\n }}\n onKeyDown={probe ? undefined : (e: KeyboardEvent) => this.handleTabKeyDown(e)}\n onFocusin={probe ? undefined : () => {\n this.overflowRovingFocused = false;\n }}\n >\n {tabs.map((tab, index) => {\n if (isTabDivider(tab)) {\n return (\n <div class=\"bar-nav__tab-divider\" key={`divider-${index}`} aria-hidden=\"true\">\n <div class=\"bar-nav__tab-divider-line\" />\n </div>\n );\n }\n\n const isSelected = tab.id === this.effectiveValue;\n return (\n <button\n key={tab.id}\n type=\"button\"\n role=\"tab\"\n data-tab-id={tab.id}\n class={{\n 'bar-nav__tab': true,\n 'bar-nav__tab--selected': isSelected,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-control-inactive': !!tab.isInactive,\n }}\n aria-selected={isSelected ? 'true' : 'false'}\n aria-disabled={tab.isInactive ? 'true' : undefined}\n aria-controls={tab.panelId ?? undefined}\n disabled={tab.isInactive}\n tabIndex={\n !probe && !this.overflowRovingFocused && tab.id === this.focusedTabId\n ? 0\n : -1\n }\n onClick={() => !probe && !tab.isInactive && this.selectTab(tab.id)}\n onFocus={() => {\n if (!probe) this.focusedTabId = tab.id;\n }}\n >\n <span\n class={{\n 'bar-nav__tab-label': true,\n 'bar-nav__tab-label--dot': !!tab.dot,\n }}\n >\n <ds-text as=\"span\" variant=\"text-body-medium\" emphasis={isSelected} color=\"inherit\">\n {tab.label}\n </ds-text>\n {tab.dot && (\n <ds-badge\n class=\"bar-nav__tab-dot\"\n variant=\"dot\"\n hasRing={false}\n label=\"\"\n aria-hidden=\"true\"\n />\n )}\n </span>\n </button>\n );\n })}\n </div>\n );\n }\n\n render() {\n // Section gating only applies when `basePath` is set (SPA section sync).\n // Tabs without a base path must still render — otherwise a single-tab bar\n // (or any non-routed tabs) silently falls through to an empty header.\n const sectionReady =\n this.basePath === '' || this.committedSection === this.basePath;\n const hasTabs =\n sectionReady && this.resolvedTabs.length > 0 && !this.hideTabsForDetailRoute;\n const tabGroupKey = this.basePath || 'no-section';\n const renderedTabs = this.hasOverflowTabs ? this.visibleTabs : this.resolvedTabs;\n\n return (\n <Host>\n <header\n class={{\n 'bar-nav': true,\n 'ds-control--md': true,\n 'ds-chrome-row': true,\n 'ds-chrome-space--md': true,\n 'bar-nav--dashboard': this.navStyle === 'dashboard',\n 'bar-nav--settings': this.navStyle === 'settings',\n 'bar-nav--tabs-collapsed':\n hasTabs && this.hasOverflowTabs,\n }}\n ref={el => {\n this.headerEl = el as HTMLElement;\n if (el && !this.resizeObserver) {\n this.setupOverflowObserver();\n }\n }}\n >\n {hasTabs && (\n <div class=\"bar-nav__tabs-probe\" aria-hidden=\"true\" inert>\n <div key={`probe-${tabGroupKey}`}>\n {this.renderTabList(this.resolvedTabs, true)}\n </div>\n </div>\n )}\n\n {hasTabs && this.tabLayoutCommitted && renderedTabs.length > 0 && (\n <div class=\"bar-nav__left\">\n <div key={`visible-${tabGroupKey}`} class=\"bar-nav__tabs-visible\">\n {this.renderTabList(renderedTabs, false)}\n </div>\n </div>\n )}\n\n {hasTabs && !this.tabLayoutCommitted && (\n <div class=\"bar-nav__tabs-pending\" aria-hidden=\"true\" />\n )}\n\n {!hasTabs && this.heading && (\n <ds-text\n class=\"bar-nav__heading\"\n lineTruncation={1}\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis\n color=\"primary\"\n wrap=\"nowrap\"\n >\n {this.heading}\n </ds-text>\n )}\n\n {hasTabs && this.hasOverflowTabs && (\n <ds-button-unfilled variant=\"icon\"\n class={{\n 'bar-nav__overflow-trigger': true,\n }}\n icon=\"Ellipses\"\n activeFill={false}\n hasBorder={false}\n focusTabIndex={this.overflowRovingFocused ? 0 : -1}\n ref={(el?: HTMLDsButtonUnfilledElement) => {\n this.triggerEl = (el as (HTMLElement & { setFocus?: () => Promise<void> })) ?? null;\n }}\n haspopup=\"menu\"\n expanded={this.menuTriggerActive}\n aria-label={this.moreTabsLabel}\n onDsClick={() => this.toggleTabMenu({ focusVisible: false })}\n onFocusin={this.handleOverflowFocus}\n onFocusout={this.handleOverflowBlur}\n onKeyDown={(e: KeyboardEvent) => this.handleTriggerKeyDown(e)}\n />\n )}\n\n </header>\n\n {hasTabs && this.hasOverflowTabs && (\n <ds-menu\n ref={el => {\n this.menuEl = (el as HTMLDsMenuElement) ?? null;\n }}\n open={this.menuOpen}\n sections={tabsToOverflowMenuSections(this.overflowTabs, this.effectiveValue)}\n side=\"bottom\"\n align=\"end\"\n sideOffset=\"calc(var(--dimension-space-100) + var(--dimension-space-050))\"\n initialFocusVisible={this.menuInitialFocusVisible}\n onDsSelect={(e: Event) => this.handleMenuSelect(e)}\n onDsClose={() => this.handleMenuClose()}\n onDsAfterClose={() => this.handleMenuAfterClose()}\n />\n )}\n </Host>\n );\n }\n}\n",
323
305
  "type": "registry:ui"
324
306
  },
325
307
  {
@@ -334,7 +316,7 @@
334
316
  },
335
317
  {
336
318
  "path": "src/wc/components/BarNav/bar-nav-utils.ts",
337
- "content": "import { getSelectableTabs } from '../TabGroup/tab-item-utils';\nimport type { BarNavTab } from './bar-nav-types';\n\nexport interface BarNavUrlState {\n /** Tab id derived from the URL, or empty when none applies. */\n value: string;\n /** When true, tabs should be hidden (e.g. non-tab child routes like detail pages). */\n hideTabs: boolean;\n}\n\n/** Derive BarNav tab selection from `currentUrl` relative to `basePath`.\n * Mirrors motive-webapp-lab `resolveBarNavState` tab-selection semantics. */\nexport function deriveBarNavValueFromUrl(\n path: string,\n basePath: string,\n tabs: BarNavTab[],\n): BarNavUrlState {\n if (!path || !basePath) {\n return { value: '', hideTabs: false };\n }\n\n if (path !== basePath && !path.startsWith(`${basePath}/`)) {\n return { value: '', hideTabs: false };\n }\n\n const remainder = path.slice(basePath.length);\n const seg = remainder.startsWith('/') ? remainder.slice(1).split('/')[0] : '';\n const selectableTabs = getSelectableTabs(tabs);\n\n if (!seg) {\n return { value: selectableTabs[0]?.id ?? '', hideTabs: false };\n }\n\n const tab = selectableTabs.find(t => t.id === seg);\n if (tab) {\n return { value: tab.id, hideTabs: false };\n }\n\n return { value: '', hideTabs: true };\n}\n\n/** True when host array/json props have data but resolved internal state is still empty. */\nexport function shouldResyncBarNavProps(\n resolvedTabs: BarNavTab[],\n tabs: BarNavTab[],\n tabsJson: string,\n): boolean {\n const tabsIncoming = tabsJson\n ? parseJsonArrayProp(tabsJson, [])\n : (tabs ?? []);\n\n return resolvedTabs.length === 0 && tabsIncoming.length > 0;\n}\n\n/** Parse tabs from either a JSON attribute string or a JS property array. */\nexport function parseJsonArrayProp<T>(value: string | T[] | undefined, fallback: T[]): T[] {\n if (Array.isArray(value)) return value;\n if (typeof value === 'string' && value !== '') {\n try {\n const parsed = JSON.parse(value);\n return Array.isArray(parsed) ? parsed : fallback;\n } catch {\n return fallback;\n }\n }\n return fallback;\n}\n",
319
+ "content": "import { getSelectableTabs } from '../TabGroup/tab-item-utils';\nimport type { BarNavTab } from './bar-nav-types';\n\nexport interface BarNavUrlState {\n /** Tab id derived from the URL, or empty when none applies. */\n value: string;\n /** When true, tabs should be hidden (e.g. non-tab child routes like detail pages). */\n hideTabs: boolean;\n}\n\n/** Derive BarNav tab selection from `currentUrl` relative to `basePath`.\n * Mirrors motive-webapp-lab `resolveBarNavState` tab-selection semantics. */\nexport function deriveBarNavValueFromUrl(\n path: string,\n basePath: string,\n tabs: BarNavTab[],\n): BarNavUrlState {\n if (!path || !basePath) {\n return { value: '', hideTabs: false };\n }\n\n if (path !== basePath && !path.startsWith(`${basePath}/`)) {\n return { value: '', hideTabs: false };\n }\n\n const remainder = path.slice(basePath.length);\n const seg = remainder.startsWith('/') ? remainder.slice(1).split('/')[0] : '';\n const selectableTabs = getSelectableTabs(tabs);\n\n if (!seg) {\n return { value: selectableTabs[0]?.id ?? '', hideTabs: false };\n }\n\n const tab = selectableTabs.find(t => t.id === seg);\n if (tab) {\n return { value: tab.id, hideTabs: false };\n }\n\n return { value: '', hideTabs: true };\n}\n\n/** True when host array props have data but resolved internal state is still empty. */\nexport function shouldResyncBarNavProps(\n resolvedTabs: BarNavTab[],\n tabs: BarNavTab[],\n): boolean {\n return resolvedTabs.length === 0 && (tabs?.length ?? 0) > 0;\n}\n",
338
320
  "type": "registry:ui"
339
321
  }
340
322
  ]
@@ -449,7 +449,7 @@
449
449
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: actions, breadcrumbs, primaryAction, sections.",
450
450
  "peerDependencies": {
451
451
  "required": [
452
- "@ds-mo/tokens ^5.0.0",
452
+ "@ds-mo/tokens ^5.1.0",
453
453
  "@ds-mo/icons ^6.4.0"
454
454
  ],
455
455
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -351,7 +351,7 @@
351
351
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: steps, submitAction.",
352
352
  "peerDependencies": {
353
353
  "required": [
354
- "@ds-mo/tokens ^5.0.0",
354
+ "@ds-mo/tokens ^5.1.0",
355
355
  "@ds-mo/icons ^6.4.0"
356
356
  ],
357
357
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -171,7 +171,7 @@
171
171
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: items.",
172
172
  "peerDependencies": {
173
173
  "required": [
174
- "@ds-mo/tokens ^5.0.0",
174
+ "@ds-mo/tokens ^5.1.0",
175
175
  "@ds-mo/icons ^6.4.0"
176
176
  ],
177
177
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -193,7 +193,7 @@
193
193
  },
194
194
  {
195
195
  "path": "src/wc/components/Breadcrumb/Breadcrumb.tsx",
196
- "content": "import { Component, Event, EventEmitter, h, Host, Prop, State, Watch } from '@stencil/core';\nimport type { BreadcrumbItem, BreadcrumbSelectDetail } from './breadcrumb-types';\n\n@Component({\n tag: 'ds-breadcrumb',\n styleUrl: 'Breadcrumb.css',\n scoped: true,\n})\nexport class Breadcrumb {\n /** Ordered path from the broadest ancestor to the nearest page. */\n @Prop() items: BreadcrumbItem[] = [];\n\n /** Accessible name for the breadcrumb navigation landmark. */\n @Prop() ariaLabel: string = 'Breadcrumb';\n\n /** Emitted when an interactive breadcrumb item is activated. Prevent to cancel native navigation. */\n @Event({ cancelable: true }) dsSelect!: EventEmitter<BreadcrumbSelectDetail>;\n\n @State() private labelWidths: number[] = [];\n @State() private collapsedLabels: boolean[] = [];\n\n private navElement?: HTMLElement;\n private listElement?: HTMLOListElement;\n private measurementElements: HTMLElement[] = [];\n private separatorElements: HTMLElement[] = [];\n private ellipsisMeasurement?: HTMLElement;\n private resizeObserver?: ResizeObserver;\n private observedElement?: HTMLElement;\n private measurementFrame?: number;\n\n componentDidLoad() {\n this.observeContainer();\n this.scheduleMeasurement();\n void document.fonts?.ready.then(() => this.scheduleMeasurement());\n }\n\n componentDidRender() {\n this.observeContainer();\n this.scheduleMeasurement();\n }\n\n disconnectedCallback() {\n this.resizeObserver?.disconnect();\n this.observedElement = undefined;\n if (this.measurementFrame !== undefined) cancelAnimationFrame(this.measurementFrame);\n }\n\n @Watch('items')\n handleItemsChange() {\n this.labelWidths = [];\n this.collapsedLabels = [];\n this.scheduleMeasurement();\n }\n\n private observeContainer() {\n if (!this.navElement || this.observedElement === this.navElement) return;\n this.resizeObserver ??= new ResizeObserver(() => this.scheduleMeasurement());\n this.resizeObserver.disconnect();\n this.resizeObserver.observe(this.navElement);\n this.observedElement = this.navElement;\n }\n\n private scheduleMeasurement() {\n if (this.measurementFrame !== undefined) return;\n this.measurementFrame = requestAnimationFrame(() => {\n this.measurementFrame = undefined;\n this.measureLabels();\n });\n }\n\n private measureLabels() {\n if (\n !this.navElement ||\n !this.listElement ||\n !this.ellipsisMeasurement ||\n this.items.length === 0\n ) return;\n\n const naturalWidths = this.items.map(\n (_, index) => this.measurementElements[index]?.getBoundingClientRect().width ?? 0\n );\n const ellipsisWidth = this.ellipsisMeasurement.getBoundingClientRect().width;\n if (naturalWidths.some(width => width === 0) || ellipsisWidth === 0) return;\n\n const separatorWidth = this.separatorElements.reduce((total, separator) => {\n const styles = getComputedStyle(separator);\n return (\n total +\n separator.getBoundingClientRect().width +\n Number.parseFloat(styles.marginInlineStart) +\n Number.parseFloat(styles.marginInlineEnd)\n );\n }, 0);\n const availableWidth = Math.max(0, this.navElement.clientWidth - separatorWidth);\n const minimumWidths = naturalWidths.map(width => Math.min(width, ellipsisWidth));\n const nextWidths = [...minimumWidths];\n let remainingWidth = Math.max(\n 0,\n availableWidth - minimumWidths.reduce((total, width) => total + width, 0)\n );\n\n // Protect the current/newest location. Extra width is granted newest-to-oldest,\n // so ancestors collapse in path order as the available space decreases.\n for (let index = naturalWidths.length - 1; index >= 0 && remainingWidth > 0; index -= 1) {\n const extraWidth = Math.min(\n remainingWidth,\n naturalWidths[index] - minimumWidths[index]\n );\n nextWidths[index] += extraWidth;\n remainingWidth -= extraWidth;\n }\n\n const widthsChanged =\n nextWidths.length !== this.labelWidths.length ||\n nextWidths.some(\n (width, index) => Math.abs(width - (this.labelWidths[index] ?? 0)) > 0.25\n );\n const nextCollapsedLabels = nextWidths.map(\n (width, index) =>\n naturalWidths[index] > minimumWidths[index] &&\n Math.abs(width - minimumWidths[index]) <= 0.25\n );\n const collapsedLabelsChanged =\n nextCollapsedLabels.length !== this.collapsedLabels.length ||\n nextCollapsedLabels.some(\n (isCollapsed, index) => isCollapsed !== this.collapsedLabels[index]\n );\n if (widthsChanged) this.labelWidths = nextWidths;\n if (collapsedLabelsChanged) this.collapsedLabels = nextCollapsedLabels;\n }\n\n private handleSelect(item: BreadcrumbItem, originalEvent: MouseEvent) {\n const event = this.dsSelect.emit({ item, originalEvent });\n if (event.defaultPrevented) originalEvent.preventDefault();\n }\n\n private renderItem(item: BreadcrumbItem, index: number) {\n const allocatedWidth = this.labelWidths[index];\n const isCollapsed = this.collapsedLabels[index] ?? false;\n const label = (\n [\n <ds-text\n class=\"breadcrumb__label\"\n as=\"span\"\n variant=\"text-caption\"\n color=\"inherit\"\n lineTruncation={isCollapsed ? 'none' : 1}\n aria-hidden={isCollapsed ? 'true' : undefined}\n style={\n allocatedWidth === undefined\n ? undefined\n : { '--breadcrumb-label-width': `${allocatedWidth}px` }\n }\n >\n {isCollapsed ? '…' : item.label}\n </ds-text>,\n isCollapsed ? <span class=\"breadcrumb__accessible-label ds-visually-hidden\">{item.label}</span> : null,\n ]\n );\n\n if (item.isCurrent) {\n return (\n <span class=\"breadcrumb__current\" aria-current=\"page\" aria-label={item.ariaLabel}>\n {label}\n </span>\n );\n }\n\n if (item.href) {\n return (\n <a\n class=\"breadcrumb__link ds-focus-ring\"\n href={item.href}\n aria-label={item.ariaLabel}\n onClick={(event: MouseEvent) => this.handleSelect(item, event)}\n >\n {label}\n </a>\n );\n }\n\n return (\n <button\n class=\"breadcrumb__link ds-focus-ring\"\n type=\"button\"\n aria-label={item.ariaLabel}\n onClick={(event: MouseEvent) => this.handleSelect(item, event)}\n >\n {label}\n </button>\n );\n }\n\n render() {\n if (this.items.length === 0) return null;\n\n this.measurementElements = [];\n this.separatorElements = [];\n\n return (\n <Host>\n <nav\n class=\"breadcrumb\"\n aria-label={this.ariaLabel}\n ref={element => {\n this.navElement = element;\n }}\n >\n <div class=\"breadcrumb__measurements\" aria-hidden=\"true\">\n {this.items.map((item, index) => (\n <ds-text\n class=\"breadcrumb__measurement\"\n as=\"span\"\n variant=\"text-caption\"\n color=\"inherit\"\n ref={element => {\n if (element) this.measurementElements[index] = element;\n }}\n >\n {item.label}\n </ds-text>\n ))}\n <ds-text\n class=\"breadcrumb__measurement\"\n as=\"span\"\n variant=\"text-caption\"\n color=\"inherit\"\n ref={element => {\n this.ellipsisMeasurement = element;\n }}\n >\n …\n </ds-text>\n </div>\n <ol\n class=\"breadcrumb__list\"\n ref={element => {\n this.listElement = element;\n }}\n >\n {this.items.map((item, index) => (\n <li class=\"breadcrumb__item\" key={item.id}>\n {index > 0 ? (\n <ds-text\n class=\"breadcrumb__separator\"\n as=\"span\"\n variant=\"text-caption\"\n color=\"tertiary\"\n aria-hidden=\"true\"\n ref={element => {\n if (element) this.separatorElements[index - 1] = element;\n }}\n >\n /\n </ds-text>\n ) : null}\n {this.renderItem(item, index)}\n </li>\n ))}\n </ol>\n </nav>\n </Host>\n );\n }\n}\n",
196
+ "content": "import { Component, Event, EventEmitter, h, Host, Prop, State, Watch } from '@stencil/core';\nimport { resolveSafeUrl } from '../../utils';\nimport type { BreadcrumbItem, BreadcrumbSelectDetail } from './breadcrumb-types';\n\n@Component({\n tag: 'ds-breadcrumb',\n styleUrl: 'Breadcrumb.css',\n scoped: true,\n})\nexport class Breadcrumb {\n /** Ordered path from the broadest ancestor to the nearest page. */\n @Prop() items: BreadcrumbItem[] = [];\n\n /** Accessible name for the breadcrumb navigation landmark. */\n @Prop() ariaLabel: string = 'Breadcrumb';\n\n /** Emitted when an interactive breadcrumb item is activated. Prevent to cancel native navigation. */\n @Event({ cancelable: true }) dsSelect!: EventEmitter<BreadcrumbSelectDetail>;\n\n @State() private labelWidths: number[] = [];\n @State() private collapsedLabels: boolean[] = [];\n\n private navElement?: HTMLElement;\n private listElement?: HTMLOListElement;\n private measurementElements: HTMLElement[] = [];\n private separatorElements: HTMLElement[] = [];\n private ellipsisMeasurement?: HTMLElement;\n private resizeObserver?: ResizeObserver;\n private observedElement?: HTMLElement;\n private measurementFrame?: number;\n\n componentDidLoad() {\n this.observeContainer();\n this.scheduleMeasurement();\n void document.fonts?.ready.then(() => this.scheduleMeasurement());\n }\n\n componentDidRender() {\n this.observeContainer();\n this.scheduleMeasurement();\n }\n\n disconnectedCallback() {\n this.resizeObserver?.disconnect();\n this.observedElement = undefined;\n if (this.measurementFrame !== undefined) cancelAnimationFrame(this.measurementFrame);\n }\n\n @Watch('items')\n handleItemsChange() {\n this.labelWidths = [];\n this.collapsedLabels = [];\n this.scheduleMeasurement();\n }\n\n private observeContainer() {\n if (!this.navElement || this.observedElement === this.navElement) return;\n this.resizeObserver ??= new ResizeObserver(() => this.scheduleMeasurement());\n this.resizeObserver.disconnect();\n this.resizeObserver.observe(this.navElement);\n this.observedElement = this.navElement;\n }\n\n private scheduleMeasurement() {\n if (this.measurementFrame !== undefined) return;\n this.measurementFrame = requestAnimationFrame(() => {\n this.measurementFrame = undefined;\n this.measureLabels();\n });\n }\n\n private measureLabels() {\n if (\n !this.navElement ||\n !this.listElement ||\n !this.ellipsisMeasurement ||\n this.items.length === 0\n ) return;\n\n const naturalWidths = this.items.map(\n (_, index) => this.measurementElements[index]?.getBoundingClientRect().width ?? 0\n );\n const ellipsisWidth = this.ellipsisMeasurement.getBoundingClientRect().width;\n if (naturalWidths.some(width => width === 0) || ellipsisWidth === 0) return;\n\n const separatorWidth = this.separatorElements.reduce((total, separator) => {\n const styles = getComputedStyle(separator);\n return (\n total +\n separator.getBoundingClientRect().width +\n Number.parseFloat(styles.marginInlineStart) +\n Number.parseFloat(styles.marginInlineEnd)\n );\n }, 0);\n const availableWidth = Math.max(0, this.navElement.clientWidth - separatorWidth);\n const minimumWidths = naturalWidths.map(width => Math.min(width, ellipsisWidth));\n const nextWidths = [...minimumWidths];\n let remainingWidth = Math.max(\n 0,\n availableWidth - minimumWidths.reduce((total, width) => total + width, 0)\n );\n\n // Protect the current/newest location. Extra width is granted newest-to-oldest,\n // so ancestors collapse in path order as the available space decreases.\n for (let index = naturalWidths.length - 1; index >= 0 && remainingWidth > 0; index -= 1) {\n const extraWidth = Math.min(\n remainingWidth,\n naturalWidths[index] - minimumWidths[index]\n );\n nextWidths[index] += extraWidth;\n remainingWidth -= extraWidth;\n }\n\n const widthsChanged =\n nextWidths.length !== this.labelWidths.length ||\n nextWidths.some(\n (width, index) => Math.abs(width - (this.labelWidths[index] ?? 0)) > 0.25\n );\n const nextCollapsedLabels = nextWidths.map(\n (width, index) =>\n naturalWidths[index] > minimumWidths[index] &&\n Math.abs(width - minimumWidths[index]) <= 0.25\n );\n const collapsedLabelsChanged =\n nextCollapsedLabels.length !== this.collapsedLabels.length ||\n nextCollapsedLabels.some(\n (isCollapsed, index) => isCollapsed !== this.collapsedLabels[index]\n );\n if (widthsChanged) this.labelWidths = nextWidths;\n if (collapsedLabelsChanged) this.collapsedLabels = nextCollapsedLabels;\n }\n\n private handleSelect(item: BreadcrumbItem, originalEvent: MouseEvent) {\n const event = this.dsSelect.emit({ item, originalEvent });\n if (event.defaultPrevented) originalEvent.preventDefault();\n }\n\n private renderItem(item: BreadcrumbItem, index: number) {\n const allocatedWidth = this.labelWidths[index];\n const isCollapsed = this.collapsedLabels[index] ?? false;\n const label = (\n [\n <ds-text\n class=\"breadcrumb__label\"\n as=\"span\"\n variant=\"text-caption\"\n color=\"inherit\"\n lineTruncation={isCollapsed ? 'none' : 1}\n aria-hidden={isCollapsed ? 'true' : undefined}\n style={\n allocatedWidth === undefined\n ? undefined\n : { '--breadcrumb-label-width': `${allocatedWidth}px` }\n }\n >\n {isCollapsed ? '…' : item.label}\n </ds-text>,\n isCollapsed ? <span class=\"breadcrumb__accessible-label ds-visually-hidden\">{item.label}</span> : null,\n ]\n );\n\n if (item.isCurrent) {\n return (\n <span class=\"breadcrumb__current\" aria-current=\"page\" aria-label={item.ariaLabel}>\n {label}\n </span>\n );\n }\n\n const href = resolveSafeUrl(item.href);\n if (href) {\n return (\n <a\n class=\"breadcrumb__link ds-focus-ring\"\n href={href}\n aria-label={item.ariaLabel}\n onClick={(event: MouseEvent) => this.handleSelect(item, event)}\n >\n {label}\n </a>\n );\n }\n\n return (\n <button\n class=\"breadcrumb__link ds-focus-ring\"\n type=\"button\"\n aria-label={item.ariaLabel}\n onClick={(event: MouseEvent) => this.handleSelect(item, event)}\n >\n {label}\n </button>\n );\n }\n\n render() {\n if (this.items.length === 0) return null;\n\n this.measurementElements = [];\n this.separatorElements = [];\n\n return (\n <Host>\n <nav\n class=\"breadcrumb\"\n aria-label={this.ariaLabel}\n ref={element => {\n this.navElement = element;\n }}\n >\n <div class=\"breadcrumb__measurements\" aria-hidden=\"true\">\n {this.items.map((item, index) => (\n <ds-text\n class=\"breadcrumb__measurement\"\n as=\"span\"\n variant=\"text-caption\"\n color=\"inherit\"\n ref={element => {\n if (element) this.measurementElements[index] = element;\n }}\n >\n {item.label}\n </ds-text>\n ))}\n <ds-text\n class=\"breadcrumb__measurement\"\n as=\"span\"\n variant=\"text-caption\"\n color=\"inherit\"\n ref={element => {\n this.ellipsisMeasurement = element;\n }}\n >\n …\n </ds-text>\n </div>\n <ol\n class=\"breadcrumb__list\"\n ref={element => {\n this.listElement = element;\n }}\n >\n {this.items.map((item, index) => (\n <li class=\"breadcrumb__item\" key={item.id}>\n {index > 0 ? (\n <ds-text\n class=\"breadcrumb__separator\"\n as=\"span\"\n variant=\"text-caption\"\n color=\"tertiary\"\n aria-hidden=\"true\"\n ref={element => {\n if (element) this.separatorElements[index - 1] = element;\n }}\n >\n /\n </ds-text>\n ) : null}\n {this.renderItem(item, index)}\n </li>\n ))}\n </ol>\n </nav>\n </Host>\n );\n }\n}\n",
197
197
  "type": "registry:ui"
198
198
  },
199
199
  {
@@ -72,7 +72,7 @@
72
72
  "Loading prevents duplicate activation without applying inactive opacity or removing the current keyboard focus.",
73
73
  "Icon and icon-label loading replace the icon in place. Label-only loading centers the loader while preserving the button's measured width.",
74
74
  "Physical press scaling is on by default. Disable it only when an owning composite must keep child or background geometry fixed; hover and pressed interaction washes remain available.",
75
- "ButtonFilled has no active or selected state; a filled action is a command, not a toggle or a persistent selection.",
75
+ "ButtonFilled has no active, pressed, or selected state; a filled action is a command, not a toggle or a persistent selection.",
76
76
  "An open popup is a transient pressed state instead. While expanded is true the pressed wash is held for the popup's complete rendered lifecycle and survives hover, so the trigger reads as held open without introducing a selected treatment.",
77
77
  "hasMenu supports only the label and icon-label variants. The chevron is what communicates that a menu will open, so an icon-only filled menu button would leave the affordance unstated."
78
78
  ],
@@ -221,14 +221,6 @@
221
221
  "mutable": false,
222
222
  "description": "Scale down during a physical pointer press.\nDisable when an owning composite requires fixed child or background geometry."
223
223
  },
224
- "pressed": {
225
- "type": "boolean | undefined",
226
- "resolvedType": "boolean | undefined",
227
- "attribute": "pressed",
228
- "required": false,
229
- "mutable": false,
230
- "description": "Pressed semantics for a toggle command that reports state elsewhere."
231
- },
232
224
  "rounded": {
233
225
  "type": "boolean",
234
226
  "resolvedType": "boolean",
@@ -415,14 +407,6 @@
415
407
  "mutable": false,
416
408
  "description": "Scale down during a physical pointer press.\nDisable when an owning composite requires fixed child or background geometry."
417
409
  },
418
- "pressed": {
419
- "type": "boolean | undefined",
420
- "resolvedType": "boolean | undefined",
421
- "attribute": "pressed",
422
- "required": false,
423
- "mutable": false,
424
- "description": "Pressed semantics for a toggle command that reports state elsewhere."
425
- },
426
410
  "rounded": {
427
411
  "type": "boolean",
428
412
  "resolvedType": "boolean",
@@ -507,7 +491,7 @@
507
491
  },
508
492
  "peerDependencies": {
509
493
  "required": [
510
- "@ds-mo/tokens ^5.0.0",
494
+ "@ds-mo/tokens ^5.1.0",
511
495
  "@ds-mo/icons ^6.4.0"
512
496
  ],
513
497
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
@@ -520,6 +504,7 @@
520
504
  "@ds-mo/icons"
521
505
  ],
522
506
  "registryDependencies": [
507
+ "badge",
523
508
  "icon",
524
509
  "loader",
525
510
  "text"
@@ -527,12 +512,12 @@
527
512
  "files": [
528
513
  {
529
514
  "path": "src/wc/components/ButtonFilled/ButtonFilled.css",
530
- "content": "@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n@import '../../utils/control-press.css';\n@import '../../utils/control-inactive.css';\n@import '../../utils/control-density.css';\n@import '../../utils/control-parts.css';\n@import '../../utils/control-width.css';\n\n:host {\n display: inline-flex;\n outline: none;\n --ds-button-filled-bg: var(--color-background-bold-brand);\n --ds-button-filled-fg: var(--color-foreground-faint-brand);\n --ds-button-filled-border: var(--color-border-secondary);\n}\n\n/* Icon-only: fixed square from density height (hug). Fill stretches the hit target. */\n:host(.button-filled-host--icon.ds-control-width--hug) {\n width: var(--ds-control-height, var(--dimension-size-400));\n height: var(--ds-control-height, var(--dimension-size-400));\n min-width: var(--ds-control-height, var(--dimension-size-400));\n min-height: var(--ds-control-height, var(--dimension-size-400));\n}\n\n:host(.button-filled-host--icon.ds-control-width--fill) {\n height: var(--ds-control-height, var(--dimension-size-400));\n min-height: var(--ds-control-height, var(--dimension-size-400));\n}\n\n:host(.ds-control-width--fill) .button-filled {\n width: 100%;\n}\n\n.button-filled {\n justify-content: center;\n border: none;\n border-radius: var(--ds-control-radius, var(--dimension-radius-025));\n background-color: var(--ds-button-filled-bg);\n color: var(--ds-button-filled-fg);\n cursor: default;\n -webkit-user-select: none;\n user-select: none;\n overflow: hidden;\n transition:\n background-color var(--effect-motion-short-2),\n color var(--effect-motion-short-2);\n /* Hover/press: `.ds-interaction-fill` + contrast → `--on-*` (token overlay, not color-mix). */\n}\n\n.button-filled--icon {\n width: 100%;\n height: 100%;\n padding: 0;\n}\n\n.button-filled--rounded {\n --ds-control-radius: var(--dimension-radius-half);\n}\n\n/*\n * An open popup is a transient pressed state, not a selected state. ButtonFilled\n * deliberately has no active state, so hold the pressed wash for the popup's\n * complete rendered lifecycle instead of promoting any selected treatment.\n */\n.button-filled.button-filled--expanded::after,\n.button-filled.button-filled--expanded:hover:not(:disabled)::after {\n background: var(--ds-interaction-pressed);\n}\n\n/* Inset stroke via interaction-fill ::after (above hover and pressed washes). */\n.button-filled--bordered {\n --ds-interaction-border-width: var(--dimension-stroke-width-012);\n --ds-interaction-border-color: var(--ds-button-filled-border);\n}\n\n/* Preserve the border while inactive, when interaction-fill is intentionally omitted. */\n.button-filled--bordered:not(.ds-interaction-fill) {\n box-shadow: inset 0 0 0 var(--ds-interaction-border-width) var(--ds-interaction-border-color);\n}\n\n/*\n * Parent-surface context affects only the optional inset border.\n * Intent + contrast continue to own fill, foreground, and interaction overlays.\n */\n.button-filled--background-medium {\n --ds-button-filled-border: var(--color-border-on-medium-background-secondary);\n}\n\n.button-filled--background-bold {\n --ds-button-filled-border: var(--color-border-on-bold-background-secondary);\n}\n\n.button-filled--background-strong {\n --ds-button-filled-border: var(--color-border-on-strong-background-secondary);\n}\n\n.button-filled--background-translucent {\n --ds-button-filled-border: var(--color-translucent-border-secondary);\n}\n\n.button-filled--background-inverted {\n --ds-button-filled-border: var(--color-inverted-border-secondary);\n}\n\n.button-filled--background-media {\n --ds-button-filled-border: var(--color-media-border-secondary);\n}\n\n.button-filled--background-always-dark {\n --ds-button-filled-border: var(--color-always-dark-border-secondary);\n}\n\n/* Inactive: `.ds-control-inactive` (50% opacity). */\n\n.button-filled__icon-wrap {\n position: relative;\n z-index: 2;\n}\n\n.button-filled__label {\n position: relative;\n z-index: 2;\n white-space: nowrap;\n flex-shrink: 0;\n min-width: 0;\n}\n\n.button-filled__label--loading {\n visibility: hidden;\n}\n\n.button-filled__chevron {\n position: relative;\n z-index: 2;\n}\n\n/* Hidden, not removed, so the label-only loader keeps the measured button width. */\n.button-filled__chevron--loading {\n visibility: hidden;\n}\n\n.button-filled > .button-filled__loader-overlay {\n position: absolute;\n inset: 0;\n z-index: 2;\n display: flex;\n align-items: center;\n justify-content: center;\n pointer-events: none;\n}\n\n/* ── Intent + contrast (bold default) ─────────────────────────────────────── */\n\n.button-filled--intent-neutral {\n --ds-button-filled-bg: var(--color-background-bold-neutral);\n --ds-button-filled-fg: var(--color-foreground-faint-neutral);\n}\n\n.button-filled--intent-brand {\n --ds-button-filled-bg: var(--color-background-bold-brand);\n --ds-button-filled-fg: var(--color-foreground-faint-brand);\n}\n\n.button-filled--intent-ai {\n --ds-button-filled-bg: var(--color-background-bold-ai);\n --ds-button-filled-fg: var(--color-foreground-faint-ai);\n}\n\n.button-filled--intent-negative {\n --ds-button-filled-bg: var(--color-background-bold-negative);\n --ds-button-filled-fg: var(--color-foreground-faint-negative);\n}\n\n.button-filled--intent-warning {\n --ds-button-filled-bg: var(--color-background-bold-warning);\n --ds-button-filled-fg: var(--color-foreground-faint-warning);\n}\n\n.button-filled--intent-caution {\n --ds-button-filled-bg: var(--color-background-bold-caution);\n --ds-button-filled-fg: var(--color-foreground-faint-caution);\n}\n\n.button-filled--intent-positive {\n --ds-button-filled-bg: var(--color-background-bold-positive);\n --ds-button-filled-fg: var(--color-foreground-faint-positive);\n}\n\n.button-filled--intent-guide {\n --ds-button-filled-bg: var(--color-background-bold-guide);\n --ds-button-filled-fg: var(--color-foreground-faint-guide);\n}\n\n.button-filled--intent-walkthrough {\n --ds-button-filled-bg: var(--color-background-bold-walkthrough);\n --ds-button-filled-fg: var(--color-foreground-faint-walkthrough);\n}\n\n/* strong → medium fg */\n.button-filled--contrast-strong.button-filled--intent-neutral {\n --ds-button-filled-bg: var(--color-background-strong-neutral);\n --ds-button-filled-fg: var(--color-foreground-medium-neutral);\n}\n\n.button-filled--contrast-strong.button-filled--intent-brand {\n --ds-button-filled-bg: var(--color-background-strong-brand);\n --ds-button-filled-fg: var(--color-foreground-medium-brand);\n}\n\n.button-filled--contrast-strong.button-filled--intent-ai {\n --ds-button-filled-bg: var(--color-background-strong-ai);\n --ds-button-filled-fg: var(--color-foreground-medium-ai);\n}\n\n.button-filled--contrast-strong.button-filled--intent-negative {\n --ds-button-filled-bg: var(--color-background-strong-negative);\n --ds-button-filled-fg: var(--color-foreground-medium-negative);\n}\n\n.button-filled--contrast-strong.button-filled--intent-warning {\n --ds-button-filled-bg: var(--color-background-strong-warning);\n --ds-button-filled-fg: var(--color-foreground-medium-warning);\n}\n\n.button-filled--contrast-strong.button-filled--intent-caution {\n --ds-button-filled-bg: var(--color-background-strong-caution);\n --ds-button-filled-fg: var(--color-foreground-medium-caution);\n}\n\n.button-filled--contrast-strong.button-filled--intent-positive {\n --ds-button-filled-bg: var(--color-background-strong-positive);\n --ds-button-filled-fg: var(--color-foreground-medium-positive);\n}\n\n.button-filled--contrast-strong.button-filled--intent-guide {\n --ds-button-filled-bg: var(--color-background-strong-guide);\n --ds-button-filled-fg: var(--color-foreground-medium-guide);\n}\n\n.button-filled--contrast-strong.button-filled--intent-walkthrough {\n --ds-button-filled-bg: var(--color-background-strong-walkthrough);\n --ds-button-filled-fg: var(--color-foreground-medium-walkthrough);\n}\n\n/* medium → strong fg */\n.button-filled--contrast-medium.button-filled--intent-neutral {\n --ds-button-filled-bg: var(--color-background-medium-neutral);\n --ds-button-filled-fg: var(--color-foreground-strong-neutral);\n}\n\n.button-filled--contrast-medium.button-filled--intent-brand {\n --ds-button-filled-bg: var(--color-background-medium-brand);\n --ds-button-filled-fg: var(--color-foreground-strong-brand);\n}\n\n.button-filled--contrast-medium.button-filled--intent-ai {\n --ds-button-filled-bg: var(--color-background-medium-ai);\n --ds-button-filled-fg: var(--color-foreground-strong-ai);\n}\n\n.button-filled--contrast-medium.button-filled--intent-negative {\n --ds-button-filled-bg: var(--color-background-medium-negative);\n --ds-button-filled-fg: var(--color-foreground-strong-negative);\n}\n\n.button-filled--contrast-medium.button-filled--intent-warning {\n --ds-button-filled-bg: var(--color-background-medium-warning);\n --ds-button-filled-fg: var(--color-foreground-strong-warning);\n}\n\n.button-filled--contrast-medium.button-filled--intent-caution {\n --ds-button-filled-bg: var(--color-background-medium-caution);\n --ds-button-filled-fg: var(--color-foreground-strong-caution);\n}\n\n.button-filled--contrast-medium.button-filled--intent-positive {\n --ds-button-filled-bg: var(--color-background-medium-positive);\n --ds-button-filled-fg: var(--color-foreground-strong-positive);\n}\n\n.button-filled--contrast-medium.button-filled--intent-guide {\n --ds-button-filled-bg: var(--color-background-medium-guide);\n --ds-button-filled-fg: var(--color-foreground-strong-guide);\n}\n\n.button-filled--contrast-medium.button-filled--intent-walkthrough {\n --ds-button-filled-bg: var(--color-background-medium-walkthrough);\n --ds-button-filled-fg: var(--color-foreground-strong-walkthrough);\n}\n\n/* faint → bold fg */\n.button-filled--contrast-faint.button-filled--intent-neutral {\n --ds-button-filled-bg: var(--color-background-faint-neutral);\n --ds-button-filled-fg: var(--color-foreground-bold-neutral);\n}\n\n.button-filled--contrast-faint.button-filled--intent-brand {\n --ds-button-filled-bg: var(--color-background-faint-brand);\n --ds-button-filled-fg: var(--color-foreground-bold-brand);\n}\n\n.button-filled--contrast-faint.button-filled--intent-ai {\n --ds-button-filled-bg: var(--color-background-faint-ai);\n --ds-button-filled-fg: var(--color-foreground-bold-ai);\n}\n\n.button-filled--contrast-faint.button-filled--intent-negative {\n --ds-button-filled-bg: var(--color-background-faint-negative);\n --ds-button-filled-fg: var(--color-foreground-bold-negative);\n}\n\n.button-filled--contrast-faint.button-filled--intent-warning {\n --ds-button-filled-bg: var(--color-background-faint-warning);\n --ds-button-filled-fg: var(--color-foreground-bold-warning);\n}\n\n.button-filled--contrast-faint.button-filled--intent-caution {\n --ds-button-filled-bg: var(--color-background-faint-caution);\n --ds-button-filled-fg: var(--color-foreground-bold-caution);\n}\n\n.button-filled--contrast-faint.button-filled--intent-positive {\n --ds-button-filled-bg: var(--color-background-faint-positive);\n --ds-button-filled-fg: var(--color-foreground-bold-positive);\n}\n\n.button-filled--contrast-faint.button-filled--intent-guide {\n --ds-button-filled-bg: var(--color-background-faint-guide);\n --ds-button-filled-fg: var(--color-foreground-bold-guide);\n}\n\n.button-filled--contrast-faint.button-filled--intent-walkthrough {\n --ds-button-filled-bg: var(--color-background-faint-walkthrough);\n --ds-button-filled-fg: var(--color-foreground-bold-walkthrough);\n}\n",
515
+ "content": "@import '../../utils/button-base.css';\n\n:host {\n --ds-button-filled-bg: var(--color-background-bold-brand);\n --ds-button-filled-fg: var(--color-foreground-faint-brand);\n --ds-button-filled-border: var(--color-border-secondary);\n}\n\n.button-filled {\n background-color: var(--ds-button-filled-bg);\n color: var(--ds-button-filled-fg);\n transition:\n background-color var(--effect-motion-short-2),\n color var(--effect-motion-short-2);\n /* Hover/press: `.ds-interaction-fill` + contrast → `--on-*` (token overlay, not color-mix). */\n}\n\n/*\n * An open popup is a transient pressed state, not a selected state. ButtonFilled\n * deliberately has no active state, so hold the pressed wash for the popup's\n * complete rendered lifecycle instead of promoting any selected treatment.\n */\n/* Inset stroke via interaction-fill ::after (above hover and pressed washes). */\n.button-filled--bordered {\n --ds-interaction-border-color: var(--ds-button-filled-border);\n}\n\n/*\n * Parent-surface context affects only the optional inset border.\n * Intent + contrast continue to own fill, foreground, and interaction overlays.\n */\n.button-filled--background-medium {\n --ds-button-filled-border: var(--color-border-on-medium-background-secondary);\n}\n\n.button-filled--background-bold {\n --ds-button-filled-border: var(--color-border-on-bold-background-secondary);\n}\n\n.button-filled--background-strong {\n --ds-button-filled-border: var(--color-border-on-strong-background-secondary);\n}\n\n.button-filled--background-translucent {\n --ds-button-filled-border: var(--color-translucent-border-secondary);\n}\n\n.button-filled--background-inverted {\n --ds-button-filled-border: var(--color-inverted-border-secondary);\n}\n\n.button-filled--background-media {\n --ds-button-filled-border: var(--color-media-border-secondary);\n}\n\n.button-filled--background-always-dark {\n --ds-button-filled-border: var(--color-always-dark-border-secondary);\n}\n\n/* Inactive: `.ds-control-inactive` (50% opacity). */\n\n/* ── Intent + contrast (bold default) ─────────────────────────────────────── */\n\n.button-filled--intent-neutral {\n --ds-button-filled-bg: var(--color-background-bold-neutral);\n --ds-button-filled-fg: var(--color-foreground-faint-neutral);\n}\n\n.button-filled--intent-brand {\n --ds-button-filled-bg: var(--color-background-bold-brand);\n --ds-button-filled-fg: var(--color-foreground-faint-brand);\n}\n\n.button-filled--intent-ai {\n --ds-button-filled-bg: var(--color-background-bold-ai);\n --ds-button-filled-fg: var(--color-foreground-faint-ai);\n}\n\n.button-filled--intent-negative {\n --ds-button-filled-bg: var(--color-background-bold-negative);\n --ds-button-filled-fg: var(--color-foreground-faint-negative);\n}\n\n.button-filled--intent-warning {\n --ds-button-filled-bg: var(--color-background-bold-warning);\n --ds-button-filled-fg: var(--color-foreground-faint-warning);\n}\n\n.button-filled--intent-caution {\n --ds-button-filled-bg: var(--color-background-bold-caution);\n --ds-button-filled-fg: var(--color-foreground-faint-caution);\n}\n\n.button-filled--intent-positive {\n --ds-button-filled-bg: var(--color-background-bold-positive);\n --ds-button-filled-fg: var(--color-foreground-faint-positive);\n}\n\n.button-filled--intent-guide {\n --ds-button-filled-bg: var(--color-background-bold-guide);\n --ds-button-filled-fg: var(--color-foreground-faint-guide);\n}\n\n.button-filled--intent-walkthrough {\n --ds-button-filled-bg: var(--color-background-bold-walkthrough);\n --ds-button-filled-fg: var(--color-foreground-faint-walkthrough);\n}\n\n/* strong → medium fg */\n.button-filled--contrast-strong.button-filled--intent-neutral {\n --ds-button-filled-bg: var(--color-background-strong-neutral);\n --ds-button-filled-fg: var(--color-foreground-medium-neutral);\n}\n\n.button-filled--contrast-strong.button-filled--intent-brand {\n --ds-button-filled-bg: var(--color-background-strong-brand);\n --ds-button-filled-fg: var(--color-foreground-medium-brand);\n}\n\n.button-filled--contrast-strong.button-filled--intent-ai {\n --ds-button-filled-bg: var(--color-background-strong-ai);\n --ds-button-filled-fg: var(--color-foreground-medium-ai);\n}\n\n.button-filled--contrast-strong.button-filled--intent-negative {\n --ds-button-filled-bg: var(--color-background-strong-negative);\n --ds-button-filled-fg: var(--color-foreground-medium-negative);\n}\n\n.button-filled--contrast-strong.button-filled--intent-warning {\n --ds-button-filled-bg: var(--color-background-strong-warning);\n --ds-button-filled-fg: var(--color-foreground-medium-warning);\n}\n\n.button-filled--contrast-strong.button-filled--intent-caution {\n --ds-button-filled-bg: var(--color-background-strong-caution);\n --ds-button-filled-fg: var(--color-foreground-medium-caution);\n}\n\n.button-filled--contrast-strong.button-filled--intent-positive {\n --ds-button-filled-bg: var(--color-background-strong-positive);\n --ds-button-filled-fg: var(--color-foreground-medium-positive);\n}\n\n.button-filled--contrast-strong.button-filled--intent-guide {\n --ds-button-filled-bg: var(--color-background-strong-guide);\n --ds-button-filled-fg: var(--color-foreground-medium-guide);\n}\n\n.button-filled--contrast-strong.button-filled--intent-walkthrough {\n --ds-button-filled-bg: var(--color-background-strong-walkthrough);\n --ds-button-filled-fg: var(--color-foreground-medium-walkthrough);\n}\n\n/* medium → strong fg */\n.button-filled--contrast-medium.button-filled--intent-neutral {\n --ds-button-filled-bg: var(--color-background-medium-neutral);\n --ds-button-filled-fg: var(--color-foreground-strong-neutral);\n}\n\n.button-filled--contrast-medium.button-filled--intent-brand {\n --ds-button-filled-bg: var(--color-background-medium-brand);\n --ds-button-filled-fg: var(--color-foreground-strong-brand);\n}\n\n.button-filled--contrast-medium.button-filled--intent-ai {\n --ds-button-filled-bg: var(--color-background-medium-ai);\n --ds-button-filled-fg: var(--color-foreground-strong-ai);\n}\n\n.button-filled--contrast-medium.button-filled--intent-negative {\n --ds-button-filled-bg: var(--color-background-medium-negative);\n --ds-button-filled-fg: var(--color-foreground-strong-negative);\n}\n\n.button-filled--contrast-medium.button-filled--intent-warning {\n --ds-button-filled-bg: var(--color-background-medium-warning);\n --ds-button-filled-fg: var(--color-foreground-strong-warning);\n}\n\n.button-filled--contrast-medium.button-filled--intent-caution {\n --ds-button-filled-bg: var(--color-background-medium-caution);\n --ds-button-filled-fg: var(--color-foreground-strong-caution);\n}\n\n.button-filled--contrast-medium.button-filled--intent-positive {\n --ds-button-filled-bg: var(--color-background-medium-positive);\n --ds-button-filled-fg: var(--color-foreground-strong-positive);\n}\n\n.button-filled--contrast-medium.button-filled--intent-guide {\n --ds-button-filled-bg: var(--color-background-medium-guide);\n --ds-button-filled-fg: var(--color-foreground-strong-guide);\n}\n\n.button-filled--contrast-medium.button-filled--intent-walkthrough {\n --ds-button-filled-bg: var(--color-background-medium-walkthrough);\n --ds-button-filled-fg: var(--color-foreground-strong-walkthrough);\n}\n\n/* faint → bold fg */\n.button-filled--contrast-faint.button-filled--intent-neutral {\n --ds-button-filled-bg: var(--color-background-faint-neutral);\n --ds-button-filled-fg: var(--color-foreground-bold-neutral);\n}\n\n.button-filled--contrast-faint.button-filled--intent-brand {\n --ds-button-filled-bg: var(--color-background-faint-brand);\n --ds-button-filled-fg: var(--color-foreground-bold-brand);\n}\n\n.button-filled--contrast-faint.button-filled--intent-ai {\n --ds-button-filled-bg: var(--color-background-faint-ai);\n --ds-button-filled-fg: var(--color-foreground-bold-ai);\n}\n\n.button-filled--contrast-faint.button-filled--intent-negative {\n --ds-button-filled-bg: var(--color-background-faint-negative);\n --ds-button-filled-fg: var(--color-foreground-bold-negative);\n}\n\n.button-filled--contrast-faint.button-filled--intent-warning {\n --ds-button-filled-bg: var(--color-background-faint-warning);\n --ds-button-filled-fg: var(--color-foreground-bold-warning);\n}\n\n.button-filled--contrast-faint.button-filled--intent-caution {\n --ds-button-filled-bg: var(--color-background-faint-caution);\n --ds-button-filled-fg: var(--color-foreground-bold-caution);\n}\n\n.button-filled--contrast-faint.button-filled--intent-positive {\n --ds-button-filled-bg: var(--color-background-faint-positive);\n --ds-button-filled-fg: var(--color-foreground-bold-positive);\n}\n\n.button-filled--contrast-faint.button-filled--intent-guide {\n --ds-button-filled-bg: var(--color-background-faint-guide);\n --ds-button-filled-fg: var(--color-foreground-bold-guide);\n}\n\n.button-filled--contrast-faint.button-filled--intent-walkthrough {\n --ds-button-filled-bg: var(--color-background-faint-walkthrough);\n --ds-button-filled-fg: var(--color-foreground-bold-walkthrough);\n}\n\n/*\n * Bold, strong, and medium fills already communicate semantic intent. Their\n * on-background foreground tokens preserve AA text contrast across themes,\n * including brand pairs whose chromatic counterparts sit on the 4.5:1 edge.\n */\n.button-filled--contrast-bold.button-filled {\n --ds-button-filled-fg: var(--color-foreground-on-bold-background-primary);\n}\n\n.button-filled--contrast-strong.button-filled {\n --ds-button-filled-fg: var(--color-foreground-on-strong-background-primary);\n}\n\n.button-filled--contrast-medium.button-filled {\n --ds-button-filled-fg: var(--color-foreground-on-medium-background-primary);\n}\n",
531
516
  "type": "registry:ui"
532
517
  },
533
518
  {
534
519
  "path": "src/wc/components/ButtonFilled/ButtonFilled.tsx",
535
- "content": "import { Component, Element, Event, EventEmitter, h, Host, Method, Prop } from '@stencil/core';\nimport { controlWidthClass, CONTROL_TEXT_VARIANT, type ControlWidth } from '../../utils';\nimport type { ChoiceBackground } from '../../utils/choice-list';\nimport { beginElevatedControlPress } from '../../utils/control-press';\n\nexport type ButtonFilledIntent =\n | 'neutral'\n | 'brand'\n | 'ai'\n | 'negative'\n | 'warning'\n | 'caution'\n | 'positive'\n | 'guide'\n | 'walkthrough';\n\nexport type ButtonFilledContrast = 'bold' | 'strong' | 'medium' | 'faint';\n\nexport type ButtonFilledBackground = ChoiceBackground;\n\nexport type ButtonFilledVariant = 'icon' | 'label' | 'icon-label';\n\nexport type ButtonFilledSize = 'lg' | 'md' | 'sm' | 'xs';\n\nexport type ButtonFilledWidth = ControlWidth;\n\nexport type ButtonFilledPopup = 'true' | 'menu' | 'listbox' | 'tree' | 'grid' | 'dialog';\n\n/**\n * `ds-icon` size prop matching control-density icon metrics\n * (lg→24 / md→20 / sm→16 / xs→12 via `--dimension-iconography-*`).\n */\nconst ICON_SIZE: Record<ButtonFilledSize, 'lg' | 'md' | 'sm' | 'xs'> = {\n lg: 'lg',\n md: 'md',\n sm: 'sm',\n xs: 'xs',\n};\n\n@Component({\n tag: 'ds-button-filled',\n styleUrl: 'ButtonFilled.css',\n scoped: true,\n})\nexport class ButtonFilled {\n @Element() el!: HTMLElement;\n\n /**\n * Content layout. Default is label-only; pass `icon` for icon-only chrome\n * (nav / tool rails) or `icon-label` for leading icon + text.\n */\n @Prop() variant: ButtonFilledVariant = 'label';\n\n /** Control density (height, padding, icon, type). */\n @Prop() size: ButtonFilledSize = 'md';\n\n /** Width fit — hug content (default) or fill the parent. */\n @Prop() width: ButtonFilledWidth = 'hug';\n\n /** Visible text for `label` / `icon-label` variants. */\n @Prop() label: string = '';\n\n /** Icon name passed to <ds-icon> for `icon` / `icon-label` variants. */\n @Prop() icon: string = '';\n\n /** Semantic colour intent. */\n @Prop() intent: ButtonFilledIntent = 'brand';\n\n /**\n * Background fill weight. Foreground uses the paired contrast token:\n * bold → faint, strong → medium, medium → strong, faint → bold.\n */\n @Prop() contrast: ButtonFilledContrast = 'bold';\n\n /** Show a 1px secondary inset border. */\n @Prop() hasBorder: boolean = false;\n\n /**\n * Actual parent surface context for the optional inset border color only.\n * Omit on primary and secondary surfaces.\n */\n @Prop() background: ButtonFilledBackground | undefined;\n\n /** Use the half-radius treatment instead of the default control radius. */\n @Prop() rounded: boolean = false;\n\n /**\n * Scale down during a physical pointer press.\n * Disable when an owning composite requires fixed child or background geometry.\n */\n @Prop() pressScale: boolean = true;\n\n /** Disables interaction. */\n @Prop() isInactive: boolean = false;\n\n /** Shows an inline loader and prevents interaction without applying inactive opacity. */\n @Prop() isLoading: boolean = false;\n\n /** Native button type. */\n @Prop() type: 'button' | 'submit' | 'reset' = 'button';\n\n /** Accessible name override. Required for icon-only buttons. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n\n /** ID of the popup this button controls. */\n @Prop() controls: string | undefined;\n\n /**\n * Controlled open state of the popup this button triggers. Holds the pressed\n * wash for the popup's rendered lifecycle. ButtonFilled has no selected state,\n * so an open popup never promotes to an active treatment.\n */\n @Prop() expanded: boolean | undefined;\n\n /** Popup type exposed to assistive technology. */\n @Prop() haspopup: ButtonFilledPopup | undefined;\n\n /** Pressed semantics for a toggle command that reports state elsewhere. */\n @Prop() pressed: boolean | undefined;\n\n /**\n * This action *has* a menu: implies `aria-haspopup=\"menu\"` and adds the trailing\n * chevron that carries the affordance.\n *\n * Only `label` and `icon-label` are supported. A filled button is never the\n * overflow / more-options control — that role belongs to ButtonUnfilled with an\n * `Ellipses` glyph, which conveys the menu without a chevron.\n *\n * Use `haspopup` directly for non-menu popups.\n */\n @Prop() hasMenu: boolean = false;\n\n @Event() dsClick!: EventEmitter<MouseEvent>;\n\n private buttonEl: HTMLButtonElement | null = null;\n\n @Method()\n async setFocus() {\n this.buttonEl?.focus();\n }\n\n private handleClick = (event: MouseEvent) => {\n if (this.isInactive || this.isLoading) {\n event.preventDefault();\n event.stopPropagation();\n return;\n }\n this.dsClick.emit(event);\n };\n\n private get showIcon(): boolean {\n return this.variant === 'icon' || this.variant === 'icon-label';\n }\n\n private get showLabel(): boolean {\n return this.variant === 'label' || this.variant === 'icon-label';\n }\n\n /** Icon-only triggers rely on their own glyph, so the chevron is label-bound. */\n private get showChevron(): boolean {\n return this.hasMenu && this.variant !== 'icon';\n }\n\n private get resolvedHaspopup(): ButtonFilledPopup | undefined {\n return this.haspopup ?? (this.hasMenu ? 'menu' : undefined);\n }\n\n private get accessibleName(): string | undefined {\n if (this.ariaLabel) return this.ariaLabel;\n if (this.isLoading && this.variant === 'label' && this.label) return this.label;\n return undefined;\n }\n\n render() {\n const textVariant = CONTROL_TEXT_VARIANT[this.size];\n const iconSize = ICON_SIZE[this.size];\n\n const cls: Record<string, boolean> = {\n 'button-filled': true,\n 'ds-focus-ring-inset': true,\n 'ds-control-press-scale': this.pressScale,\n 'ds-interaction-fill': !this.isInactive,\n /* Bold is the default filled contrast — on-bold interaction tokens. */\n 'ds-interaction-fill--on-bold': this.contrast === 'bold',\n 'ds-interaction-fill--on-strong': this.contrast === 'strong',\n 'ds-interaction-fill--on-medium': this.contrast === 'medium',\n /* faint → default app interaction tokens (no --on-*). */\n 'button-filled--bordered': this.hasBorder,\n 'button-filled--expanded': this.expanded === true && !this.isInactive,\n 'ds-control-inactive': this.isInactive,\n 'ds-control--lg': this.size === 'lg',\n 'ds-control--md': this.size === 'md',\n 'ds-control--sm': this.size === 'sm',\n 'ds-control--xs': this.size === 'xs',\n 'ds-control-frame': true,\n 'button-filled--icon': this.variant === 'icon',\n 'button-filled--label': this.variant === 'label',\n 'button-filled--icon-label': this.variant === 'icon-label',\n 'button-filled--rounded': this.rounded,\n [`button-filled--background-${this.background}`]: this.background !== undefined,\n [`button-filled--intent-${this.intent}`]: true,\n [`button-filled--contrast-${this.contrast}`]: this.contrast !== 'bold',\n };\n\n return (\n <Host\n class={{\n 'button-filled-host': true,\n 'button-filled-host--icon': this.variant === 'icon',\n 'ds-control--lg': this.size === 'lg',\n 'ds-control--md': this.size === 'md',\n 'ds-control--sm': this.size === 'sm',\n 'ds-control--xs': this.size === 'xs',\n ...controlWidthClass(this.width),\n }}\n tabIndex={-1}\n >\n <button\n ref={el => {\n this.buttonEl = el ?? null;\n }}\n type={this.type}\n class={cls}\n disabled={this.isInactive}\n aria-label={this.accessibleName}\n aria-busy={this.isLoading ? 'true' : undefined}\n aria-disabled={this.isLoading ? 'true' : undefined}\n aria-controls={this.controls}\n aria-expanded={this.expanded === undefined ? undefined : String(this.expanded)}\n aria-haspopup={this.resolvedHaspopup}\n aria-pressed={this.pressed === undefined ? undefined : String(this.pressed)}\n onPointerDown={event =>\n beginElevatedControlPress(\n event,\n this.pressScale && !this.isInactive && !this.isLoading,\n )\n }\n onClick={this.handleClick}\n >\n {this.showIcon && (\n <span class=\"button-filled__icon-wrap ds-control-icon-box ds-interaction-fill__content\">\n {this.isLoading\n ? <ds-loader size={iconSize} color=\"inherit\" />\n : <ds-icon name={this.icon} size={iconSize} color=\"inherit\" />\n }\n </span>\n )}\n {this.showLabel && (\n <ds-text\n class={{\n 'button-filled__label': true,\n 'ds-control-label-box': true,\n 'button-filled__label--loading': this.isLoading && this.variant === 'label',\n 'ds-interaction-fill__content': true,\n }}\n as=\"span\"\n variant={textVariant}\n emphasis\n color=\"inherit\"\n >\n {this.label}\n </ds-text>\n )}\n {this.showChevron && (\n <span\n class={{\n 'button-filled__chevron': true,\n 'ds-control-icon-box': true,\n 'ds-interaction-fill__content': true,\n 'button-filled__chevron--loading': this.isLoading && this.variant === 'label',\n }}\n aria-hidden=\"true\"\n >\n <ds-icon name=\"ChevronDown\" size={iconSize} color=\"inherit\" />\n </span>\n )}\n {this.isLoading && this.variant === 'label' && (\n <span class=\"button-filled__loader-overlay ds-interaction-fill__content\">\n <ds-loader size={iconSize} color=\"inherit\" />\n </span>\n )}\n </button>\n </Host>\n );\n }\n}\n",
520
+ "content": "import { Component, Element, Event, EventEmitter, h, Host, Method, Prop } from '@stencil/core';\nimport { controlWidthClass } from '../../utils';\nimport type { ChoiceBackground } from '../../utils/choice-list';\nimport { beginElevatedControlPress } from '../../utils/control-press';\nimport { renderButtonContent } from '../../utils/button-render';\nimport type {\n ButtonPopup,\n ButtonSize,\n ButtonVariant,\n ButtonWidth,\n} from '../../utils/button-types';\n\nexport type ButtonFilledIntent =\n | 'neutral'\n | 'brand'\n | 'ai'\n | 'negative'\n | 'warning'\n | 'caution'\n | 'positive'\n | 'guide'\n | 'walkthrough';\n\nexport type ButtonFilledContrast = 'bold' | 'strong' | 'medium' | 'faint';\n\nexport type ButtonFilledBackground = ChoiceBackground;\n\nexport type ButtonFilledVariant = ButtonVariant;\nexport type ButtonFilledSize = ButtonSize;\nexport type ButtonFilledWidth = ButtonWidth;\nexport type ButtonFilledPopup = ButtonPopup;\n\n@Component({\n tag: 'ds-button-filled',\n styleUrl: 'ButtonFilled.css',\n scoped: true,\n})\nexport class ButtonFilled {\n @Element() el!: HTMLElement;\n\n /**\n * Content layout. Default is label-only; pass `icon` for icon-only chrome\n * (nav / tool rails) or `icon-label` for leading icon + text.\n */\n @Prop() variant: ButtonFilledVariant = 'label';\n\n /** Control density (height, padding, icon, type). */\n @Prop() size: ButtonFilledSize = 'md';\n\n /** Width fit — hug content (default) or fill the parent. */\n @Prop() width: ButtonFilledWidth = 'hug';\n\n /** Visible text for `label` / `icon-label` variants. */\n @Prop() label: string = '';\n\n /** Icon name passed to <ds-icon> for `icon` / `icon-label` variants. */\n @Prop() icon: string = '';\n\n /** Semantic colour intent. */\n @Prop() intent: ButtonFilledIntent = 'brand';\n\n /**\n * Background fill weight. Foreground uses the paired contrast token:\n * bold → faint, strong → medium, medium → strong, faint → bold.\n */\n @Prop() contrast: ButtonFilledContrast = 'bold';\n\n /** Show a 1px secondary inset border. */\n @Prop() hasBorder: boolean = false;\n\n /**\n * Actual parent surface context for the optional inset border color only.\n * Omit on primary and secondary surfaces.\n */\n @Prop() background: ButtonFilledBackground | undefined;\n\n /** Use the half-radius treatment instead of the default control radius. */\n @Prop() rounded: boolean = false;\n\n /**\n * Scale down during a physical pointer press.\n * Disable when an owning composite requires fixed child or background geometry.\n */\n @Prop() pressScale: boolean = true;\n\n /** Disables interaction. */\n @Prop() isInactive: boolean = false;\n\n /** Shows an inline loader and prevents interaction without applying inactive opacity. */\n @Prop() isLoading: boolean = false;\n\n /** Native button type. */\n @Prop() type: 'button' | 'submit' | 'reset' = 'button';\n\n /** Accessible name override. Required for icon-only buttons. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n\n /** ID of the popup this button controls. */\n @Prop() controls: string | undefined;\n\n /**\n * Controlled open state of the popup this button triggers. Holds the pressed\n * wash for the popup's rendered lifecycle. ButtonFilled has no selected state,\n * so an open popup never promotes to an active treatment.\n */\n @Prop() expanded: boolean | undefined;\n\n /** Popup type exposed to assistive technology. */\n @Prop() haspopup: ButtonFilledPopup | undefined;\n\n /**\n * This action *has* a menu: implies `aria-haspopup=\"menu\"` and adds the trailing\n * chevron that carries the affordance.\n *\n * Only `label` and `icon-label` are supported. A filled button is never the\n * overflow / more-options control — that role belongs to ButtonUnfilled with an\n * `Ellipses` glyph, which conveys the menu without a chevron.\n *\n * Use `haspopup` directly for non-menu popups.\n */\n @Prop() hasMenu: boolean = false;\n\n @Event() dsClick!: EventEmitter<MouseEvent>;\n\n private buttonEl: HTMLButtonElement | null = null;\n\n @Method()\n async setFocus() {\n this.buttonEl?.focus();\n }\n\n private handleClick = (event: MouseEvent) => {\n if (this.isInactive || this.isLoading) {\n event.preventDefault();\n event.stopPropagation();\n return;\n }\n this.dsClick.emit(event);\n };\n\n private get resolvedHaspopup(): ButtonFilledPopup | undefined {\n return this.haspopup ?? (this.hasMenu ? 'menu' : undefined);\n }\n\n private get accessibleName(): string | undefined {\n if (this.ariaLabel) return this.ariaLabel;\n if (this.isLoading && this.variant === 'label' && this.label) return this.label;\n return undefined;\n }\n\n render() {\n const cls: Record<string, boolean> = {\n 'button-filled': true,\n 'ds-button': true,\n 'ds-focus-ring-inset': true,\n 'ds-control-press-scale': this.pressScale,\n 'ds-interaction-fill': !this.isInactive,\n /* Bold is the default filled contrast — on-bold interaction tokens. */\n 'ds-interaction-fill--on-bold': this.contrast === 'bold',\n 'ds-interaction-fill--on-strong': this.contrast === 'strong',\n 'ds-interaction-fill--on-medium': this.contrast === 'medium',\n /* faint → default app interaction tokens (no --on-*). */\n 'button-filled--bordered': this.hasBorder,\n 'ds-button--bordered': this.hasBorder,\n 'button-filled--expanded': this.expanded === true && !this.isInactive,\n 'ds-button--expanded': this.expanded === true && !this.isInactive,\n 'ds-control-inactive': this.isInactive,\n 'ds-control--lg': this.size === 'lg',\n 'ds-control--md': this.size === 'md',\n 'ds-control--sm': this.size === 'sm',\n 'ds-control--xs': this.size === 'xs',\n 'ds-control-frame': true,\n 'button-filled--icon': this.variant === 'icon',\n 'ds-button--icon': this.variant === 'icon',\n 'button-filled--label': this.variant === 'label',\n 'button-filled--icon-label': this.variant === 'icon-label',\n 'button-filled--rounded': this.rounded,\n 'ds-button--rounded': this.rounded,\n [`button-filled--background-${this.background}`]: this.background !== undefined,\n [`button-filled--intent-${this.intent}`]: true,\n [`button-filled--contrast-${this.contrast}`]: true,\n };\n\n return (\n <Host\n class={{\n 'button-filled-host': true,\n 'button-filled-host--icon': this.variant === 'icon',\n 'ds-button-host--icon': this.variant === 'icon',\n 'ds-control--lg': this.size === 'lg',\n 'ds-control--md': this.size === 'md',\n 'ds-control--sm': this.size === 'sm',\n 'ds-control--xs': this.size === 'xs',\n ...controlWidthClass(this.width),\n }}\n tabIndex={-1}\n >\n <button\n ref={el => {\n this.buttonEl = el ?? null;\n }}\n type={this.type}\n class={cls}\n disabled={this.isInactive}\n aria-label={this.accessibleName}\n aria-busy={this.isLoading ? 'true' : undefined}\n aria-disabled={this.isLoading ? 'true' : undefined}\n aria-controls={this.controls}\n aria-expanded={this.expanded === undefined ? undefined : String(this.expanded)}\n aria-haspopup={this.resolvedHaspopup}\n onPointerDown={event =>\n beginElevatedControlPress(\n event,\n this.pressScale && !this.isInactive && !this.isLoading,\n )\n }\n onClick={this.handleClick}\n >\n {renderButtonContent({\n namespace: 'button-filled',\n variant: this.variant,\n size: this.size,\n label: this.label,\n icon: this.icon,\n hasMenu: this.hasMenu,\n isLoading: this.isLoading,\n })}\n </button>\n </Host>\n );\n }\n}\n",
536
521
  "type": "registry:ui"
537
522
  }
538
523
  ]