@ds-mo/ui 14.27.1 → 14.29.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 (338) hide show
  1. package/dist/.package-ready.json +1 -1
  2. package/dist/agent-patterns.json +28 -18
  3. package/dist/agent.json +436 -33
  4. package/dist/angular/ds-button-filled.d.ts +1 -1
  5. package/dist/angular/ds-button-filled.js +3 -3
  6. package/dist/angular/ds-button-unfilled.d.ts +1 -1
  7. package/dist/angular/ds-button-unfilled.js +3 -3
  8. package/dist/angular/ds-card-navigation.d.ts +20 -0
  9. package/dist/angular/ds-card-navigation.js +42 -0
  10. package/dist/angular/ds-card-settings-scope.d.ts +19 -0
  11. package/dist/angular/ds-card-settings-scope.js +42 -0
  12. package/dist/angular/ds-menu.d.ts +1 -1
  13. package/dist/angular/ds-menu.js +3 -3
  14. package/dist/angular/ds-mobile-bar-nav.d.ts +1 -1
  15. package/dist/angular/ds-mobile-bar-nav.js +3 -3
  16. package/dist/angular/ds-mobile-header.d.ts +1 -1
  17. package/dist/angular/ds-mobile-header.js +3 -3
  18. package/dist/angular/ds-mobile-section-switcher.d.ts +1 -1
  19. package/dist/angular/ds-mobile-section-switcher.js +3 -3
  20. package/dist/angular/ds-mobile-sheet-nav.d.ts +7 -1
  21. package/dist/angular/ds-mobile-sheet-nav.js +7 -4
  22. package/dist/angular/ds-panel-nav.d.ts +1 -1
  23. package/dist/angular/ds-panel-nav.js +3 -3
  24. package/dist/angular/ds-panel-tool-header.d.ts +1 -1
  25. package/dist/angular/ds-panel-tool-header.js +3 -3
  26. package/dist/angular/ds-panel-tool-search.d.ts +1 -1
  27. package/dist/angular/ds-panel-tool-search.js +3 -3
  28. package/dist/angular/ds-table.d.ts +1 -1
  29. package/dist/angular/ds-table.js +3 -3
  30. package/dist/angular/ds-tag.d.ts +1 -1
  31. package/dist/angular/ds-tag.js +3 -3
  32. package/dist/angular/index.d.ts +1 -1
  33. package/dist/angular/index.js +2 -0
  34. package/dist/angular/proxies.d.ts +2 -0
  35. package/dist/angular/proxies.js +2 -0
  36. package/dist/components/ds-agent-activity.js +1 -1
  37. package/dist/components/ds-agent-questionnaire.js +1 -1
  38. package/dist/components/ds-agent-response.js +1 -1
  39. package/dist/components/ds-agent-source-list.js +1 -1
  40. package/dist/components/ds-agent-tool-call.js +1 -1
  41. package/dist/components/ds-attachment-list.js +1 -1
  42. package/dist/components/ds-badge.js +1 -1
  43. package/dist/components/ds-banner.js +1 -1
  44. package/dist/components/ds-bar-action.js +1 -1
  45. package/dist/components/ds-bar-nav.js +1 -1
  46. package/dist/components/ds-bar-title.js +1 -1
  47. package/dist/components/ds-bar-workflow.js +1 -1
  48. package/dist/components/ds-breadcrumb.js +1 -1
  49. package/dist/components/ds-button-filled.js +1 -1
  50. package/dist/components/ds-button-unfilled.js +1 -1
  51. package/dist/components/ds-card-chart.js +1 -1
  52. package/dist/components/ds-card-navigation.d.ts +11 -0
  53. package/dist/components/ds-card-navigation.js +2 -0
  54. package/dist/components/ds-card-navigation.js.map +1 -0
  55. package/dist/components/ds-card-overview.js +1 -1
  56. package/dist/components/ds-card-overview.js.map +1 -1
  57. package/dist/components/ds-card-setting.js +1 -1
  58. package/dist/components/ds-card-settings-scope.d.ts +11 -0
  59. package/dist/components/ds-card-settings-scope.js +2 -0
  60. package/dist/components/ds-card-settings-scope.js.map +1 -0
  61. package/dist/components/ds-chart-legend.js +1 -1
  62. package/dist/components/ds-chart-legend.js.map +1 -1
  63. package/dist/components/ds-chart.js +1 -1
  64. package/dist/components/ds-checkbox.js +1 -1
  65. package/dist/components/ds-chip.js +1 -1
  66. package/dist/components/ds-code-block.js +1 -1
  67. package/dist/components/ds-conversation-list-item.js +1 -1
  68. package/dist/components/ds-conversation-list-item.js.map +1 -1
  69. package/dist/components/ds-conversation-list-section.js +1 -1
  70. package/dist/components/ds-conversation-list.js +1 -1
  71. package/dist/components/ds-divider.js +1 -1
  72. package/dist/components/ds-empty-state.js +1 -1
  73. package/dist/components/ds-field.js +1 -1
  74. package/dist/components/ds-filter-menu.js +1 -1
  75. package/dist/components/ds-input.js +1 -1
  76. package/dist/components/ds-markdown.js +1 -1
  77. package/dist/components/ds-menu.js +1 -1
  78. package/dist/components/ds-message-actions.js +1 -1
  79. package/dist/components/ds-message-bubble.js +1 -1
  80. package/dist/components/ds-message-composer.js +1 -1
  81. package/dist/components/ds-message-scroller.js +1 -1
  82. package/dist/components/ds-message.js +1 -1
  83. package/dist/components/ds-mobile-bar-nav.js +1 -1
  84. package/dist/components/ds-mobile-header.js +1 -1
  85. package/dist/components/ds-mobile-section-switcher.js +1 -1
  86. package/dist/components/ds-mobile-sheet-nav.js +1 -1
  87. package/dist/components/ds-modal.js +1 -1
  88. package/dist/components/ds-pagination.js +1 -1
  89. package/dist/components/ds-panel-nav.js +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-tool-search.js.map +1 -1
  95. package/dist/components/ds-panel-tools.js +1 -1
  96. package/dist/components/ds-paper-texture.js +1 -1
  97. package/dist/components/ds-radio.js +1 -1
  98. package/dist/components/ds-score.js +1 -1
  99. package/dist/components/ds-scroll-overlay.js +1 -1
  100. package/dist/components/ds-select.js +1 -1
  101. package/dist/components/ds-shell-app.js +1 -1
  102. package/dist/components/ds-shell-app.js.map +1 -1
  103. package/dist/components/ds-shell-page.js +1 -1
  104. package/dist/components/ds-shell-tools.js +1 -1
  105. package/dist/components/ds-skeleton.js +1 -1
  106. package/dist/components/ds-slider.js +1 -1
  107. package/dist/components/ds-swatch-picker.js +1 -1
  108. package/dist/components/ds-switch.js +1 -1
  109. package/dist/components/ds-tab-group.js +1 -1
  110. package/dist/components/ds-table-filter.js +1 -1
  111. package/dist/components/ds-table-group.js +1 -1
  112. package/dist/components/ds-table-group.js.map +1 -1
  113. package/dist/components/ds-table-saved-views.js +1 -1
  114. package/dist/components/ds-table-saved-views.js.map +1 -1
  115. package/dist/components/ds-table-search.js +1 -1
  116. package/dist/components/ds-table-search.js.map +1 -1
  117. package/dist/components/ds-table-sort.js +1 -1
  118. package/dist/components/ds-table-sort.js.map +1 -1
  119. package/dist/components/ds-table-toolbar.js +1 -1
  120. package/dist/components/ds-table.js +1 -1
  121. package/dist/components/ds-table.js.map +1 -1
  122. package/dist/components/ds-tag.js +1 -1
  123. package/dist/components/ds-text.js +1 -1
  124. package/dist/components/ds-textarea.js +1 -1
  125. package/dist/components/ds-textarea.js.map +1 -1
  126. package/dist/components/ds-toast.js +1 -1
  127. package/dist/components/ds-tooltip-chart.js +1 -1
  128. package/dist/components/ds-tooltip.js +1 -1
  129. package/dist/components/ds-typing-indicator.js +1 -1
  130. package/dist/components/p--yvQzZoO.js +2 -0
  131. package/dist/components/p--yvQzZoO.js.map +1 -0
  132. package/dist/components/{p-Is1lFmwp.js → p-1gt8QlyW.js} +2 -2
  133. package/dist/components/p-B5AZd41L.js +2 -0
  134. package/dist/components/p-B5AZd41L.js.map +1 -0
  135. package/dist/components/p-B5eMVBlz.js +2 -0
  136. package/dist/components/p-B5eMVBlz.js.map +1 -0
  137. package/dist/components/{p-Q7AOgrg5.js → p-BCFpqnAg.js} +2 -2
  138. package/dist/components/p-BFHWoNH3.js +2 -0
  139. package/dist/components/{p-BVz-r3g0.js.map → p-BFHWoNH3.js.map} +1 -1
  140. package/dist/components/p-BGnRFtBa.js +2 -0
  141. package/dist/components/p-BGnRFtBa.js.map +1 -0
  142. package/dist/components/p-BIDGQ4tc.js +2 -0
  143. package/dist/components/p-BIDGQ4tc.js.map +1 -0
  144. package/dist/components/{p-Ctnm9NJP.js → p-BLTHIBsE.js} +2 -2
  145. package/dist/components/{p-C68oSTb1.js → p-BXT2opmZ.js} +2 -2
  146. package/dist/components/p-BZDQLjE0.js +2 -0
  147. package/dist/components/p-BZDQLjE0.js.map +1 -0
  148. package/dist/components/{p-Q0dkeoJ6.js → p-BdC7wxhu.js} +2 -2
  149. package/dist/components/{p-Q0dkeoJ6.js.map → p-BdC7wxhu.js.map} +1 -1
  150. package/dist/components/p-BfVlFinS.js +2 -0
  151. package/dist/components/{p-CQjUOHiN.js.map → p-BfVlFinS.js.map} +1 -1
  152. package/dist/components/{p-DGkn8FwV.js → p-Bl5wd3v3.js} +2 -2
  153. package/dist/components/p-BmRHJ_jo.js +2 -0
  154. package/dist/components/p-BmRHJ_jo.js.map +1 -0
  155. package/dist/components/{p-Bb9dA7sl.js → p-BpB2EF9g.js} +2 -2
  156. package/dist/components/{p-2wIPUcSL.js → p-BwAWZAHk.js} +2 -2
  157. package/dist/components/{p-B3sRZBZk.js → p-By5TDnbF.js} +2 -2
  158. package/dist/components/{p-BVpn_e-r.js → p-CDXcINTO.js} +2 -2
  159. package/dist/components/p-CDXcINTO.js.map +1 -0
  160. package/dist/components/{p-B3Z5sQin.js → p-CE8gsv7V.js} +2 -2
  161. package/dist/components/p-CLcwh1kq.js +2 -0
  162. package/dist/components/p-CLcwh1kq.js.map +1 -0
  163. package/dist/components/{p-DW8ssbfI.js → p-CN8PQLZu.js} +2 -2
  164. package/dist/components/p-CcK3UiTu.js +2 -0
  165. package/dist/components/{p-1604KY3B.js.map → p-CcK3UiTu.js.map} +1 -1
  166. package/dist/components/{p-CaF5WCci.js → p-Cocq-utR.js} +2 -2
  167. package/dist/components/p-Cr5vn5JO.js +2 -0
  168. package/dist/components/{p-to8BxxgT.js.map → p-Cr5vn5JO.js.map} +1 -1
  169. package/dist/components/{p-Cf5Ai4xx.js → p-Crd3PEUx.js} +2 -2
  170. package/dist/components/{p-Cf5Ai4xx.js.map → p-Crd3PEUx.js.map} +1 -1
  171. package/dist/components/{p-BZiGtBBk.js → p-Ct90k-nz.js} +2 -2
  172. package/dist/components/p-Cx2dWjaW.js +2 -0
  173. package/dist/components/p-Cx2dWjaW.js.map +1 -0
  174. package/dist/components/p-CyoLh8su.js +2 -0
  175. package/dist/components/p-CyoLh8su.js.map +1 -0
  176. package/dist/components/{p-BXDLR072.js → p-CysCSinT.js} +2 -2
  177. package/dist/components/{p-BXDLR072.js.map → p-CysCSinT.js.map} +1 -1
  178. package/dist/components/p-CzbsTZt7.js.map +1 -1
  179. package/dist/components/p-D1pPq-y0.js +2 -0
  180. package/dist/components/p-D1pPq-y0.js.map +1 -0
  181. package/dist/components/{p-VT-5XDkB.js → p-DDOuPq-N.js} +2 -2
  182. package/dist/components/{p-DkxOEFHN.js → p-DF8W85Tz.js} +2 -2
  183. package/dist/components/{p-DDqRCDXh.js → p-DSJSnQ1U.js} +2 -2
  184. package/dist/components/{p-Cr6pDY-e.js → p-DYK0slTb.js} +2 -2
  185. package/dist/components/{p-D9wZlB_N.js → p-D_QtZZR8.js} +2 -2
  186. package/dist/components/{p-Bv6FSDPR.js → p-DbDYEEWq.js} +2 -2
  187. package/dist/components/{p-BEXkJ9lg.js → p-DnQN0pLp.js} +2 -2
  188. package/dist/components/{p-DIqAdmui.js → p-FiM3rF7B.js} +2 -2
  189. package/dist/components/p-GbtafFds.js +2 -0
  190. package/dist/components/p-GbtafFds.js.map +1 -0
  191. package/dist/components/{p-WFo1x6dN.js → p-Hzz2j1Je.js} +2 -2
  192. package/dist/components/{p-B9esvbYk.js → p-K39w53Bw.js} +2 -2
  193. package/dist/components/p-K39w53Bw.js.map +1 -0
  194. package/dist/components/{p-DQQsS9Rx.js → p-aMyflqva.js} +2 -2
  195. package/dist/components/{p-DZcIt_JJ.js → p-dgep63_i.js} +2 -2
  196. package/dist/components/p-frKsmPQn.js +2 -0
  197. package/dist/components/p-frKsmPQn.js.map +1 -0
  198. package/dist/components/{p-BMzn2Cmw.js → p-rSC7ZUpy.js} +2 -2
  199. package/dist/docs/components.json +1025 -44
  200. package/dist/lib/components/ButtonFilled/ButtonFilled.d.ts +11 -3
  201. package/dist/lib/components/PanelTools/panel-tools-types.d.ts +2 -0
  202. package/dist/lib/components/ShellApp/shell-app-types.d.ts +2 -0
  203. package/dist/lib/shell/index.js.map +1 -1
  204. package/dist/lib/utils/choice-list.d.ts +2 -0
  205. package/dist/mcp-data/registry/bar-nav.json +7 -2
  206. package/dist/mcp-data/registry/bar-title.json +8 -3
  207. package/dist/mcp-data/registry/button-filled.json +26 -5
  208. package/dist/mcp-data/registry/button-unfilled.json +27 -6
  209. package/dist/mcp-data/registry/card-navigation.json +255 -0
  210. package/dist/mcp-data/registry/card-settings-scope.json +314 -0
  211. package/dist/mcp-data/registry/filter-menu.json +7 -2
  212. package/dist/mcp-data/registry/menu.json +21 -2
  213. package/dist/mcp-data/registry/mobile-bar-nav.json +20 -1
  214. package/dist/mcp-data/registry/mobile-header.json +23 -3
  215. package/dist/mcp-data/registry/mobile-section-switcher.json +50 -6
  216. package/dist/mcp-data/registry/mobile-sheet-nav.json +40 -3
  217. package/dist/mcp-data/registry/panel-nav.json +27 -6
  218. package/dist/mcp-data/registry/panel-tool-header.json +23 -2
  219. package/dist/mcp-data/registry/panel-tool-search.json +23 -2
  220. package/dist/mcp-data/registry/panel-tools.json +7 -2
  221. package/dist/mcp-data/registry/registry.json +441 -32
  222. package/dist/mcp-data/registry/select.json +8 -3
  223. package/dist/mcp-data/registry/shell-app.json +14 -8
  224. package/dist/mcp-data/registry/shell-tools.json +7 -2
  225. package/dist/mcp-data/registry/table-group.json +7 -2
  226. package/dist/mcp-data/registry/table-saved-views.json +7 -2
  227. package/dist/mcp-data/registry/table-search.json +7 -2
  228. package/dist/mcp-data/registry/table-sort.json +7 -2
  229. package/dist/mcp-data/registry/table.json +24 -3
  230. package/dist/mcp-data/registry/tag.json +24 -3
  231. package/dist/react/components.d.ts +2 -0
  232. package/dist/react/components.js +2 -0
  233. package/dist/react/ds-card-navigation.d.ts +12 -0
  234. package/dist/react/ds-card-navigation.js +12 -0
  235. package/dist/react/ds-card-settings-scope.d.ts +12 -0
  236. package/dist/react/ds-card-settings-scope.js +12 -0
  237. package/dist/react/ds-mobile-sheet-nav.d.ts +2 -1
  238. package/dist/react/ds-mobile-sheet-nav.js +1 -0
  239. package/dist/styles/table.css +1 -0
  240. package/dist/types/components/BarTitle/BarTitle.d.ts +3 -0
  241. package/dist/types/components/ButtonFilled/ButtonFilled.d.ts +11 -3
  242. package/dist/types/components/ButtonUnfilled/ButtonUnfilled.d.ts +11 -5
  243. package/dist/types/components/CardNavigation/CardNavigation.d.ts +31 -0
  244. package/dist/types/components/CardNavigation/index.d.ts +2 -0
  245. package/dist/types/components/CardSettingsScope/CardSettingsScope.d.ts +33 -0
  246. package/dist/types/components/CardSettingsScope/index.d.ts +2 -0
  247. package/dist/types/components/Menu/Menu.d.ts +4 -0
  248. package/dist/types/components/MobileBarNav/MobileBarNav.d.ts +2 -0
  249. package/dist/types/components/MobileHeader/MobileHeader.d.ts +3 -0
  250. package/dist/types/components/MobileSectionSwitcher/MobileSectionSwitcher.d.ts +19 -0
  251. package/dist/types/components/MobileSheetNav/MobileSheetNav.d.ts +15 -2
  252. package/dist/types/components/PanelNav/PanelNav.d.ts +4 -1
  253. package/dist/types/components/PanelToolHeader/PanelToolHeader.d.ts +2 -0
  254. package/dist/types/components/PanelToolSearch/PanelToolSearch.d.ts +2 -0
  255. package/dist/types/components/PanelTools/panel-tools-types.d.ts +2 -0
  256. package/dist/types/components/ShellApp/ShellApp.d.ts +5 -2
  257. package/dist/types/components/ShellApp/shell-app-types.d.ts +2 -0
  258. package/dist/types/components/Table/Table.d.ts +10 -0
  259. package/dist/types/components/Table/table-row-view.d.ts +6 -1
  260. package/dist/types/components/TableSort/TableSort.d.ts +1 -0
  261. package/dist/types/components/Tag/Tag.d.ts +3 -0
  262. package/dist/types/components.d.ts +369 -8
  263. package/dist/types/utils/choice-list.d.ts +2 -0
  264. package/dist/vue/components.d.ts +2 -0
  265. package/dist/vue/components.js +2 -0
  266. package/dist/vue/ds-button-filled.js +1 -0
  267. package/dist/vue/ds-button-unfilled.js +1 -0
  268. package/dist/vue/ds-card-navigation.d.ts +3 -0
  269. package/dist/vue/ds-card-navigation.js +15 -0
  270. package/dist/vue/ds-card-settings-scope.d.ts +3 -0
  271. package/dist/vue/ds-card-settings-scope.js +20 -0
  272. package/dist/vue/ds-menu.js +1 -0
  273. package/dist/vue/ds-mobile-bar-nav.js +1 -0
  274. package/dist/vue/ds-mobile-header.js +1 -0
  275. package/dist/vue/ds-mobile-section-switcher.js +2 -0
  276. package/dist/vue/ds-mobile-sheet-nav.js +3 -0
  277. package/dist/vue/ds-panel-nav.js +1 -0
  278. package/dist/vue/ds-panel-tool-header.js +1 -0
  279. package/dist/vue/ds-panel-tool-search.js +1 -0
  280. package/dist/vue/ds-table.js +1 -0
  281. package/dist/vue/ds-tag.js +1 -0
  282. package/package.json +1 -1
  283. package/dist/components/p-1604KY3B.js +0 -2
  284. package/dist/components/p-537t6oRD.js +0 -2
  285. package/dist/components/p-537t6oRD.js.map +0 -1
  286. package/dist/components/p-6_CNVbqa.js +0 -2
  287. package/dist/components/p-6_CNVbqa.js.map +0 -1
  288. package/dist/components/p-B9esvbYk.js.map +0 -1
  289. package/dist/components/p-BIGpc__9.js +0 -2
  290. package/dist/components/p-BIGpc__9.js.map +0 -1
  291. package/dist/components/p-BRLDf-NK.js +0 -2
  292. package/dist/components/p-BRLDf-NK.js.map +0 -1
  293. package/dist/components/p-BUh2GkPJ.js +0 -2
  294. package/dist/components/p-BUh2GkPJ.js.map +0 -1
  295. package/dist/components/p-BVpn_e-r.js.map +0 -1
  296. package/dist/components/p-BVz-r3g0.js +0 -2
  297. package/dist/components/p-Bjt_8sLe.js +0 -2
  298. package/dist/components/p-Bjt_8sLe.js.map +0 -1
  299. package/dist/components/p-BssZ4SbT.js +0 -2
  300. package/dist/components/p-BssZ4SbT.js.map +0 -1
  301. package/dist/components/p-CGQnHNuW.js +0 -2
  302. package/dist/components/p-CGQnHNuW.js.map +0 -1
  303. package/dist/components/p-CQjUOHiN.js +0 -2
  304. package/dist/components/p-Cy1oWIaV.js +0 -2
  305. package/dist/components/p-Cy1oWIaV.js.map +0 -1
  306. package/dist/components/p-DQsFmleZ.js +0 -2
  307. package/dist/components/p-DQsFmleZ.js.map +0 -1
  308. package/dist/components/p-DlbhmXjH.js +0 -2
  309. package/dist/components/p-DlbhmXjH.js.map +0 -1
  310. package/dist/components/p-GEi3hDJQ.js +0 -2
  311. package/dist/components/p-GEi3hDJQ.js.map +0 -1
  312. package/dist/components/p-LMmrjmI_.js +0 -2
  313. package/dist/components/p-LMmrjmI_.js.map +0 -1
  314. package/dist/components/p-to8BxxgT.js +0 -2
  315. /package/dist/components/{p-Is1lFmwp.js.map → p-1gt8QlyW.js.map} +0 -0
  316. /package/dist/components/{p-Q7AOgrg5.js.map → p-BCFpqnAg.js.map} +0 -0
  317. /package/dist/components/{p-Ctnm9NJP.js.map → p-BLTHIBsE.js.map} +0 -0
  318. /package/dist/components/{p-C68oSTb1.js.map → p-BXT2opmZ.js.map} +0 -0
  319. /package/dist/components/{p-DGkn8FwV.js.map → p-Bl5wd3v3.js.map} +0 -0
  320. /package/dist/components/{p-Bb9dA7sl.js.map → p-BpB2EF9g.js.map} +0 -0
  321. /package/dist/components/{p-2wIPUcSL.js.map → p-BwAWZAHk.js.map} +0 -0
  322. /package/dist/components/{p-B3sRZBZk.js.map → p-By5TDnbF.js.map} +0 -0
  323. /package/dist/components/{p-B3Z5sQin.js.map → p-CE8gsv7V.js.map} +0 -0
  324. /package/dist/components/{p-DW8ssbfI.js.map → p-CN8PQLZu.js.map} +0 -0
  325. /package/dist/components/{p-CaF5WCci.js.map → p-Cocq-utR.js.map} +0 -0
  326. /package/dist/components/{p-BZiGtBBk.js.map → p-Ct90k-nz.js.map} +0 -0
  327. /package/dist/components/{p-VT-5XDkB.js.map → p-DDOuPq-N.js.map} +0 -0
  328. /package/dist/components/{p-DkxOEFHN.js.map → p-DF8W85Tz.js.map} +0 -0
  329. /package/dist/components/{p-DDqRCDXh.js.map → p-DSJSnQ1U.js.map} +0 -0
  330. /package/dist/components/{p-Cr6pDY-e.js.map → p-DYK0slTb.js.map} +0 -0
  331. /package/dist/components/{p-D9wZlB_N.js.map → p-D_QtZZR8.js.map} +0 -0
  332. /package/dist/components/{p-Bv6FSDPR.js.map → p-DbDYEEWq.js.map} +0 -0
  333. /package/dist/components/{p-BEXkJ9lg.js.map → p-DnQN0pLp.js.map} +0 -0
  334. /package/dist/components/{p-DIqAdmui.js.map → p-FiM3rF7B.js.map} +0 -0
  335. /package/dist/components/{p-WFo1x6dN.js.map → p-Hzz2j1Je.js.map} +0 -0
  336. /package/dist/components/{p-DQQsS9Rx.js.map → p-aMyflqva.js.map} +0 -0
  337. /package/dist/components/{p-DZcIt_JJ.js.map → p-dgep63_i.js.map} +0 -0
  338. /package/dist/components/{p-BMzn2Cmw.js.map → p-rSC7ZUpy.js.map} +0 -0
@@ -62,11 +62,18 @@ export declare class ButtonFilled {
62
62
  /** ID of the popup this button controls. */
63
63
  controls: string | undefined;
64
64
  /**
65
- * Controlled open state of the popup this button triggers. Holds the pressed
66
- * wash for the popup's rendered lifecycle. ButtonFilled has no selected state,
67
- * so an open popup never promotes to an active treatment.
65
+ * Controlled disclosure state forwarded to aria-expanded.
66
+ * Using expanded alone for surface-open styling is deprecated; set surfaceOpen
67
+ * explicitly to separate the rendered surface lifecycle from disclosure state.
68
68
  */
69
69
  expanded: boolean | undefined;
70
+ /**
71
+ * The associated menu, picker, or panel is visible, including its exit motion.
72
+ * Holds only the pressed wash; does not set ARIA, selection, or press scaling.
73
+ * In split mode this applies only to the menu segment. Explicit false disables
74
+ * the legacy expanded-derived wash; omission preserves that compatibility path.
75
+ */
76
+ surfaceOpen: boolean | undefined;
70
77
  /** Popup type exposed to assistive technology. */
71
78
  haspopup: ButtonFilledPopup | undefined;
72
79
  /**
@@ -95,6 +102,7 @@ export declare class ButtonFilled {
95
102
  private handleClick;
96
103
  private handleMenuClick;
97
104
  private get resolvedHaspopup();
105
+ private get resolvedSurfaceOpen();
98
106
  private get accessibleName();
99
107
  private get doubleInset();
100
108
  private buttonClass;
@@ -113,6 +113,8 @@ export interface PanelToolsHeaderAction {
113
113
  triggerId?: string;
114
114
  controls?: string;
115
115
  expanded?: boolean;
116
+ /** Associated surface remains visible, including exit motion. */
117
+ surfaceOpen?: boolean;
116
118
  haspopup?: 'menu' | 'dialog' | 'listbox' | 'tree' | 'grid';
117
119
  pressed?: boolean;
118
120
  isInactive?: boolean;
@@ -32,6 +32,8 @@ export interface ShellNavigationConfig {
32
32
  userName?: string;
33
33
  userInitial?: string;
34
34
  accountMenuExpanded?: boolean;
35
+ /** Associated account surface remains visible, including exit motion. */
36
+ accountMenuSurfaceOpen?: boolean;
35
37
  dashboardLabel?: string;
36
38
  settingsLabel?: string;
37
39
  accountLabel?: string;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/wc/components/PanelTools/panel-tools-types.ts", "../../../src/wc/shell/nav-chrome.ts", "../../../src/wc/components/Menu/menu-placement.ts", "../../../src/wc/shell/shell-gradient-presets.ts", "../../../src/wc/shell/shell-responsive.ts"],
4
- "sourcesContent": ["/** Application-owned tool id for the shell rail and responsive tool views. */\nexport type PanelToolsToolId = string;\n\n/** Canonical Motive shell recipe used when item metadata is omitted. */\nexport type CanonicalPanelToolsToolId =\n | 'search'\n | 'messages'\n | 'agents'\n | 'stacks'\n | 'activity'\n | 'help';\n\nexport const PANEL_TOOLS_TOOL_IDS: CanonicalPanelToolsToolId[] = [\n 'agents',\n 'messages',\n 'activity',\n 'search',\n 'stacks',\n 'help',\n];\n\n/** Rail header slot \u2014 mirrors panel-nav M logo row. */\nexport const PANEL_TOOLS_PRIMARY_TOOL_ID: CanonicalPanelToolsToolId = 'agents';\n\n/** Rail footer slot \u2014 flush to the bottom of the tools column. */\nexport const PANEL_TOOLS_FOOTER_TOOL_ID: CanonicalPanelToolsToolId = 'help';\n\nexport const PANEL_TOOLS_LABELS: Record<CanonicalPanelToolsToolId, string> = {\n search: 'Search',\n messages: 'Messages',\n stacks: 'Stacks',\n activity: 'Activity',\n agents: 'Agents',\n help: 'Help & Support',\n};\n\nexport const PANEL_TOOLS_SHORTCUTS: Partial<Record<CanonicalPanelToolsToolId, string>> = {\n search: 'K',\n agents: 'A',\n messages: 'M',\n stacks: 'S',\n activity: 'N',\n help: '/',\n};\n\nexport type PanelToolsRailPlacement = 'header' | 'body' | 'footer';\n\nexport interface PanelToolsItem {\n id: PanelToolsToolId;\n /** Icon name for <ds-icon>. */\n icon: string;\n /** Visible and accessible fallback label for custom tool ids. */\n label?: string;\n /** Explicit rail region; canonical Agents/Help placement is the fallback. */\n railPlacement?: PanelToolsRailPlacement;\n /** Stable ordering within the selected rail region. */\n order?: number;\n /** Optional shortcut label displayed by the tooltip. */\n shortcutKey?: string;\n /** Mobile destination recipe; canonical ids retain their explicit defaults. */\n mobileDestination?: 'search' | 'inbox' | 'activity' | 'messages' | 'agents' | 'help';\n /** Whether this rail button is currently pressed/active. */\n selected?: boolean;\n /** Show a notification dot. */\n dot?: boolean;\n isInactive?: boolean;\n ariaLabel?: string;\n}\n\nexport type PanelToolsRailAccessoryVisual =\n | { type: 'icon'; icon: string }\n | { type: 'initial'; initial: string }\n | { type: 'image'; src: string };\n\nexport interface PanelToolsRailAccessoryAction {\n /** Stable application-owned action id emitted with the accessory id. */\n id: string;\n /** Overrides the accessory label for the primary action. */\n ariaLabel?: string;\n isInactive?: boolean;\n}\n\nexport interface PanelToolsRailAccessorySecondaryAction {\n id: string;\n icon: string;\n ariaLabel: string;\n isInactive?: boolean;\n}\n\nexport type PanelToolsRailTransientTone = 'active' | 'positive';\n\ninterface PanelToolsRailAccessoryBase {\n /** Stable application-owned identity. Duplicate or blank ids are omitted. */\n id: string;\n /** Explicit rail region; accessories never infer placement from their id. */\n railPlacement: PanelToolsRailPlacement;\n /** Stable ordering within the selected rail region. */\n order: number;\n}\n\n/** Decorative boundary between application-owned rail accessory groups. */\nexport interface PanelToolsRailDividerAccessory extends PanelToolsRailAccessoryBase {\n type: 'divider';\n}\n\n/**\n * A compact one-action shortcut that matches standard tool-button geometry.\n * PanelTools owns its initial orb, notification dot, and transient interaction paint.\n */\nexport interface PanelToolsRailShortcutAccessory extends PanelToolsRailAccessoryBase {\n type: 'shortcut';\n /** Accessible shortcut name and action-label fallback. */\n ariaLabel: string;\n /** One or two displayed graphemes. Longer values are truncated visually. */\n initials: string;\n /** Supplemental notification dot. Include its meaning in the accessible label when needed. */\n dot?: boolean;\n action: PanelToolsRailAccessoryAction;\n}\n\n/**\n * A direct application intent that never selects a tool or changes drawer state.\n * PanelTools owns the two-control-height surface and its focus behavior.\n */\nexport interface PanelToolsRailTransientAccessory extends PanelToolsRailAccessoryBase {\n type: 'transient';\n /** Accessible accessory name and primary-action fallback label. */\n ariaLabel: string;\n visual: PanelToolsRailAccessoryVisual;\n /** State announced with the primary action and shown in its tooltip. */\n statusText: string;\n /** Bold surface treatment. Active uses brand; positive uses positive semantic color. */\n statusTone: PanelToolsRailTransientTone;\n primaryAction: PanelToolsRailAccessoryAction;\n secondaryAction?: PanelToolsRailAccessorySecondaryAction;\n}\n\nexport type PanelToolsRailAccessory =\n | PanelToolsRailDividerAccessory\n | PanelToolsRailShortcutAccessory\n | PanelToolsRailTransientAccessory;\n\nexport interface PanelToolsRailAccessoryActionDetail {\n accessoryId: string;\n actionId: string;\n /** Rendered action control, suitable for anchoring an application-owned overlay. */\n anchor: HTMLElement;\n}\n\n/** Canonical shell recipe used by Lab and by managed ShellApp examples. */\nexport const PANEL_TOOLS_DEFAULT_ITEMS: PanelToolsItem[] = [\n {\n id: 'agents',\n icon: 'AI',\n label: PANEL_TOOLS_LABELS.agents,\n railPlacement: 'header',\n order: 0,\n shortcutKey: PANEL_TOOLS_SHORTCUTS.agents,\n mobileDestination: 'agents',\n },\n {\n id: 'messages',\n icon: 'MessageBubbleStack',\n label: PANEL_TOOLS_LABELS.messages,\n order: 0,\n shortcutKey: PANEL_TOOLS_SHORTCUTS.messages,\n mobileDestination: 'messages',\n },\n {\n id: 'activity',\n icon: 'Bell',\n label: PANEL_TOOLS_LABELS.activity,\n order: 1,\n shortcutKey: PANEL_TOOLS_SHORTCUTS.activity,\n mobileDestination: 'activity',\n },\n {\n id: 'search',\n icon: 'MagnifyingGlass',\n label: PANEL_TOOLS_LABELS.search,\n order: 2,\n shortcutKey: PANEL_TOOLS_SHORTCUTS.search,\n mobileDestination: 'search',\n },\n {\n id: 'help',\n icon: 'CircleQuestion',\n label: PANEL_TOOLS_LABELS.help,\n railPlacement: 'footer',\n order: 0,\n shortcutKey: PANEL_TOOLS_SHORTCUTS.help,\n mobileDestination: 'help',\n },\n];\n\nexport interface PanelToolsHeaderAction {\n id: string;\n icon: string;\n ariaLabel: string;\n triggerId?: string;\n controls?: string;\n expanded?: boolean;\n haspopup?: 'menu' | 'dialog' | 'listbox' | 'tree' | 'grid';\n pressed?: boolean;\n isInactive?: boolean;\n}\n\nexport interface PanelToolsHeaderActionDetail {\n tool: PanelToolsToolId;\n id: string;\n /** Rendered header action host. Pass this to an external overlay's element anchor prop. */\n anchor?: HTMLElement;\n}\n\nexport interface PanelToolHeaderActionDetail {\n id: string;\n originalEvent: MouseEvent;\n /** Rendered action host, suitable for anchoring an external overlay across a shadow boundary. */\n anchor: HTMLElement;\n}\n\nexport interface PanelToolsHeaderConfig {\n title?: string;\n showBack?: boolean;\n backIcon?: string;\n backAriaLabel?: string;\n actions?: PanelToolsHeaderAction[];\n}\n\n/** Per-tool header state. The application replaces the object when a tool changes depth or actions. */\nexport type PanelToolsHeaders = Partial<Record<PanelToolsToolId, PanelToolsHeaderConfig>>;\n", "/** Nav chrome style slot \u2014 dashboard vs settings. Colors unified for now; texture hooks use the class. */\nexport type NavChromeStyle = 'dashboard' | 'settings';\n\n/** Document hint for first paint before framework props land. */\nexport const NAV_STYLE_HINT_ATTR = 'data-nav-style';\n\nexport function readNavStyleAttr(attr: string | null): NavChromeStyle | undefined {\n if (attr === 'dashboard' || attr === 'settings') return attr;\n return undefined;\n}\n\nexport function setNavStyleHint(style: NavChromeStyle): void {\n if (typeof document === 'undefined') return;\n document.documentElement.setAttribute(NAV_STYLE_HINT_ATTR, style);\n}\n\nexport function clearNavStyleHint(): void {\n if (typeof document === 'undefined') return;\n document.documentElement.removeAttribute(NAV_STYLE_HINT_ATTR);\n}\n\nexport function resolveNavChromeStyle(\n styleProp: NavChromeStyle,\n hostAttr: string | null,\n docHint?: string | null\n): NavChromeStyle {\n const fromHost = readNavStyleAttr(hostAttr);\n if (fromHost) return fromHost;\n\n const hint =\n docHint !== undefined\n ? docHint\n : typeof document !== 'undefined'\n ? document.documentElement.getAttribute(NAV_STYLE_HINT_ATTR)\n : null;\n const fromHint = readNavStyleAttr(hint);\n if (fromHint) return fromHint;\n\n return styleProp;\n}\n\nexport function shouldResyncNavChromeStyle(\n renderedStyle: NavChromeStyle,\n style: NavChromeStyle\n): boolean {\n return renderedStyle !== style;\n}\n", "import type { ChoicePopupAnchorAlignment } from '../../utils/choice-popup-alignment';\nimport type { MenuAlign, MenuSide } from './menu-position';\n\n/** Placement props bindable on `ds-menu` (side, align, offsets). */\nexport interface MenuPlacement {\n side: MenuSide;\n align: MenuAlign;\n anchorAlignment: ChoicePopupAnchorAlignment;\n sideOffset: number | string;\n alignOffset: number | string;\n}\n\n/**\n * Canonical `ds-menu` placement for the **panel-nav footer user menu**.\n * Anchor from `dsNavUserAction` detail `{ anchor }` (id `ds-panel-nav-user-menu-anchor`).\n *\n * Not for BarNav overflow menus \u2014 those use `side=\"bottom\"` / `align=\"end\"` internally.\n * `align: 'end'` bottom-aligns the last choice cell on the anchor; the side offset nudges into the content area:\n * - `sideOffset` space-100 + space-050 (12px): 4px past the panel-nav outer edge (anchor is ~8px inset).\n * - `anchorAlignment: 'choice-cell'`: the shared section inset puts the last row edge on the footer user row.\n */\nexport const PANEL_NAV_USER_MENU_PLACEMENT = {\n side: 'right',\n align: 'end',\n anchorAlignment: 'choice-cell',\n sideOffset: 'calc(var(--dimension-space-100) + var(--dimension-space-050))',\n alignOffset: 0,\n} as const satisfies MenuPlacement;\n\n/**\n * Canonical placement for PanelNav's collapsed child-route flyout.\n * The parent control is 8px inside the panel edge, so a 12px side offset leaves\n * a 4px visual gap outside the panel. Start choice-cell alignment places the\n * first flyout row on the same block axis as its parent control.\n */\nexport const PANEL_NAV_CHILD_MENU_PLACEMENT = {\n side: 'right',\n align: 'start',\n anchorAlignment: 'choice-cell',\n sideOffset: 'calc(var(--dimension-space-100) + var(--dimension-space-050))',\n alignOffset: 0,\n} as const satisfies MenuPlacement;\n\n/**\n * Canonical placement for a menu opened by a PanelTools header action.\n * Matches BarNav overflow: the 32px trigger is 8px inside the 48px header, so\n * a 12px side offset places the popup 4px below the header edge. Choice-cell\n * alignment keeps the final menu item edge aligned with the trigger.\n */\nexport const PANEL_TOOLS_HEADER_MENU_PLACEMENT = {\n side: 'bottom',\n align: 'end',\n anchorAlignment: 'choice-cell',\n sideOffset: 'calc(var(--dimension-space-100) + var(--dimension-space-050))',\n alignOffset: 0,\n} as const satisfies MenuPlacement;\n", "/** Built-in shell chrome wash presets. */\nexport type ShellGradientPreset = 'none' | 'cool' | 'neutral' | 'warm' | 'fresh';\n\nexport const SHELL_GRADIENT_PRESETS: ShellGradientPreset[] = [\n 'none',\n 'cool',\n 'neutral',\n 'warm',\n 'fresh',\n];\n\n/** Wash presets shown after the `none` option in pickers. */\nexport const SHELL_GRADIENT_WASH_PRESETS: Exclude<ShellGradientPreset, 'none'>[] = [\n 'cool',\n 'neutral',\n 'warm',\n 'fresh',\n];\n\n/** Default wash when `gradient-preset` is omitted. */\nexport const DEFAULT_SHELL_GRADIENT_PRESET: ShellGradientPreset = 'neutral';\n\nexport const SHELL_GRADIENT_PRESET_LABELS: Record<ShellGradientPreset, string> = {\n none: 'None',\n cool: 'Cool',\n neutral: 'Neutral',\n warm: 'Warm',\n fresh: 'Fresh',\n};\n\nexport interface ShellGradientStop {\n color: string;\n position: number;\n}\n\nexport interface ShellGradientRecipe {\n opacity: number;\n stops: readonly ShellGradientStop[];\n}\n\nexport interface ShellGradientPickerPreview {\n backgroundColor: string;\n backgroundImage?: string;\n opacity?: number;\n}\n\nexport interface ShellGradientPickerOption {\n value: ShellGradientPreset;\n label: string;\n preview: ShellGradientPickerPreview;\n}\n\nexport interface ShellGradientPickerSection {\n options: ShellGradientPickerOption[];\n}\n\nconst SHELL_GRADIENT_RECIPES: Record<Exclude<ShellGradientPreset, 'none'>, ShellGradientRecipe> = {\n cool: {\n opacity: 0.1,\n stops: [\n { color: 'var(--color-background-transparent)', position: 0 },\n { color: 'var(--color-color-intent-blue-strong-background)', position: 100 },\n ],\n },\n neutral: {\n opacity: 0.1,\n stops: [\n { color: 'var(--color-background-transparent)', position: 0 },\n { color: 'var(--color-color-intent-grey-strong-background)', position: 100 },\n ],\n },\n warm: {\n opacity: 0.1,\n stops: [\n { color: 'var(--color-background-transparent)', position: 0 },\n { color: 'var(--color-color-intent-yellow-strong-background)', position: 100 },\n ],\n },\n fresh: {\n opacity: 0.4,\n stops: [\n { color: 'var(--color-color-intent-cyan-faint-background)', position: 0 },\n { color: 'var(--color-background-transparent)', position: 47 },\n { color: 'var(--color-background-transparent)', position: 58 },\n { color: 'var(--color-color-intent-red-faint-background)', position: 83 },\n { color: 'var(--color-color-intent-pink-faint-background)', position: 100 },\n ],\n },\n};\n\nconst GRADIENT_GEOMETRY = '100% 100% at 0% 0%';\n\nexport function shellGradientPresetStopToken(preset: ShellGradientPreset): string | null {\n if (preset === 'none') return null;\n const stops = SHELL_GRADIENT_RECIPES[preset].stops;\n return stops[stops.length - 1]?.color ?? null;\n}\n\nexport function shellGradientPresetRecipe(preset: ShellGradientPreset): ShellGradientRecipe | null {\n const normalized = normalizeShellGradientPreset(preset);\n return normalized === 'none' ? null : SHELL_GRADIENT_RECIPES[normalized];\n}\n\nexport function shellGradientPresetOpacity(preset: ShellGradientPreset): string {\n return String(shellGradientPresetRecipe(preset)?.opacity ?? 0);\n}\n\nexport function isShellGradientPreset(value: string): value is ShellGradientPreset {\n return (SHELL_GRADIENT_PRESETS as string[]).includes(value);\n}\n\n/**\n * Coerce unknown/absent presets to the default. Runtime values escape the\n * type: removing the reflected `gradient-preset` attribute (e.g. an Angular\n * `[attr.gradient-preset]=\"null\"` binding) drives the prop to null past its\n * field default \u2014 without normalization the stop lookup interpolates the\n * literal string \"undefined\" into CSS and the wash silently disappears.\n */\nexport function normalizeShellGradientPreset(\n value: string | null | undefined\n): ShellGradientPreset {\n return value != null && isShellGradientPreset(value) ? value : DEFAULT_SHELL_GRADIENT_PRESET;\n}\n\n/** Radial wash for a preset \u2014 transparent at top-left into the intent stop. `none` returns no image. */\nexport function buildShellRadialGradientForPreset(preset: ShellGradientPreset): string {\n const normalized = normalizeShellGradientPreset(preset);\n if (normalized === 'none') return 'none';\n\n return buildShellRadialGradientFromStops(SHELL_GRADIENT_RECIPES[normalized].stops);\n}\n\n/** Build the production shell geometry from an ordered set of design-time stops. */\nexport function buildShellRadialGradientFromStops(stops: readonly ShellGradientStop[]): string {\n const serialized = stops.map(stop => `${stop.color} ${stop.position}%`).join(', ');\n return `radial-gradient(${GRADIENT_GEOMETRY}, ${serialized})`;\n}\n\n/** Generic SwatchPicker sections for the fixed shell appearance presets. */\nexport function shellGradientPickerSections(): ShellGradientPickerSection[] {\n return [\n {\n options: [\n {\n value: 'none',\n label: SHELL_GRADIENT_PRESET_LABELS.none,\n preview: {\n backgroundColor: 'var(--color-background-secondary)',\n },\n },\n ],\n },\n {\n options: SHELL_GRADIENT_WASH_PRESETS.map(preset => ({\n value: preset,\n label: SHELL_GRADIENT_PRESET_LABELS[preset],\n preview: {\n backgroundColor: 'var(--color-background-secondary)',\n backgroundImage: buildShellRadialGradientForPreset(preset),\n opacity: Number(shellGradientPresetOpacity(preset)),\n },\n })),\n },\n ];\n}\n", "import type { PanelToolsToolId } from '../components/PanelTools/panel-tools-types';\n\nexport type ShellResponsiveMode = 'desktop' | 'tablet' | 'mobile';\nexport type MobileDestination =\n | 'area'\n | 'search'\n | 'inbox'\n | 'activity'\n | 'messages'\n | 'agents'\n | 'help';\nexport type ShellInboxToolId = PanelToolsToolId;\n\nexport const SHELL_DESKTOP_BREAKPOINT = 1200;\nexport const SHELL_MOBILE_BREAKPOINT = 768;\nexport const SHELL_DEFAULT_INBOX_TOOL: ShellInboxToolId = 'activity';\n\nexport function resolveShellResponsiveMode(width: number): ShellResponsiveMode {\n if (width >= SHELL_DESKTOP_BREAKPOINT) return 'desktop';\n if (width >= SHELL_MOBILE_BREAKPOINT) return 'tablet';\n return 'mobile';\n}\n\nexport function resolveManagedShellPageCapacity(\n mode: ShellResponsiveMode\n): 'roomy' | 'compact' | 'constrained' {\n return mode === 'desktop' ? 'roomy' : 'compact';\n}\n\nexport function shellMobileDestinationForTool(\n open: boolean,\n tool: PanelToolsToolId | ''\n): MobileDestination {\n if (!open || !tool) return 'area';\n if (tool === 'help') return 'help';\n if (tool === 'search') return 'search';\n if (tool === 'messages') return 'messages';\n if (tool === 'agents') return 'agents';\n return 'inbox';\n}\n\n/** Explicit item metadata wins over the legacy canonical Stacks/Activity grouping. */\nexport function itemUsesShellInbox(item: {\n id: PanelToolsToolId;\n mobileDestination?: MobileDestination;\n}): boolean {\n return (\n item.mobileDestination === 'inbox' ||\n (item.mobileDestination === undefined && isShellInboxTool(item.id))\n );\n}\n\nexport function isShellInboxTool(tool: string): tool is ShellInboxToolId {\n return tool === 'stacks' || tool === 'activity';\n}\n\nexport function resolveAvailableInboxTool(\n preferred: string,\n available: readonly PanelToolsToolId[]\n): ShellInboxToolId | '' {\n const inbox = available.filter(isShellInboxTool);\n if (isShellInboxTool(preferred) && inbox.includes(preferred)) return preferred;\n if (inbox.includes(SHELL_DEFAULT_INBOX_TOOL)) return SHELL_DEFAULT_INBOX_TOOL;\n return inbox[0] ?? '';\n}\n\n/** Active-destination reselects are inert unless they also dismiss the Menu pane. */\nexport function shouldEmitMobileDestinationChange(\n active: MobileDestination,\n next: MobileDestination,\n navigationOpen: boolean\n): boolean {\n return navigationOpen || active !== next;\n}\n"],
4
+ "sourcesContent": ["/** Application-owned tool id for the shell rail and responsive tool views. */\nexport type PanelToolsToolId = string;\n\n/** Canonical Motive shell recipe used when item metadata is omitted. */\nexport type CanonicalPanelToolsToolId =\n | 'search'\n | 'messages'\n | 'agents'\n | 'stacks'\n | 'activity'\n | 'help';\n\nexport const PANEL_TOOLS_TOOL_IDS: CanonicalPanelToolsToolId[] = [\n 'agents',\n 'messages',\n 'activity',\n 'search',\n 'stacks',\n 'help',\n];\n\n/** Rail header slot \u2014 mirrors panel-nav M logo row. */\nexport const PANEL_TOOLS_PRIMARY_TOOL_ID: CanonicalPanelToolsToolId = 'agents';\n\n/** Rail footer slot \u2014 flush to the bottom of the tools column. */\nexport const PANEL_TOOLS_FOOTER_TOOL_ID: CanonicalPanelToolsToolId = 'help';\n\nexport const PANEL_TOOLS_LABELS: Record<CanonicalPanelToolsToolId, string> = {\n search: 'Search',\n messages: 'Messages',\n stacks: 'Stacks',\n activity: 'Activity',\n agents: 'Agents',\n help: 'Help & Support',\n};\n\nexport const PANEL_TOOLS_SHORTCUTS: Partial<Record<CanonicalPanelToolsToolId, string>> = {\n search: 'K',\n agents: 'A',\n messages: 'M',\n stacks: 'S',\n activity: 'N',\n help: '/',\n};\n\nexport type PanelToolsRailPlacement = 'header' | 'body' | 'footer';\n\nexport interface PanelToolsItem {\n id: PanelToolsToolId;\n /** Icon name for <ds-icon>. */\n icon: string;\n /** Visible and accessible fallback label for custom tool ids. */\n label?: string;\n /** Explicit rail region; canonical Agents/Help placement is the fallback. */\n railPlacement?: PanelToolsRailPlacement;\n /** Stable ordering within the selected rail region. */\n order?: number;\n /** Optional shortcut label displayed by the tooltip. */\n shortcutKey?: string;\n /** Mobile destination recipe; canonical ids retain their explicit defaults. */\n mobileDestination?: 'search' | 'inbox' | 'activity' | 'messages' | 'agents' | 'help';\n /** Whether this rail button is currently pressed/active. */\n selected?: boolean;\n /** Show a notification dot. */\n dot?: boolean;\n isInactive?: boolean;\n ariaLabel?: string;\n}\n\nexport type PanelToolsRailAccessoryVisual =\n | { type: 'icon'; icon: string }\n | { type: 'initial'; initial: string }\n | { type: 'image'; src: string };\n\nexport interface PanelToolsRailAccessoryAction {\n /** Stable application-owned action id emitted with the accessory id. */\n id: string;\n /** Overrides the accessory label for the primary action. */\n ariaLabel?: string;\n isInactive?: boolean;\n}\n\nexport interface PanelToolsRailAccessorySecondaryAction {\n id: string;\n icon: string;\n ariaLabel: string;\n isInactive?: boolean;\n}\n\nexport type PanelToolsRailTransientTone = 'active' | 'positive';\n\ninterface PanelToolsRailAccessoryBase {\n /** Stable application-owned identity. Duplicate or blank ids are omitted. */\n id: string;\n /** Explicit rail region; accessories never infer placement from their id. */\n railPlacement: PanelToolsRailPlacement;\n /** Stable ordering within the selected rail region. */\n order: number;\n}\n\n/** Decorative boundary between application-owned rail accessory groups. */\nexport interface PanelToolsRailDividerAccessory extends PanelToolsRailAccessoryBase {\n type: 'divider';\n}\n\n/**\n * A compact one-action shortcut that matches standard tool-button geometry.\n * PanelTools owns its initial orb, notification dot, and transient interaction paint.\n */\nexport interface PanelToolsRailShortcutAccessory extends PanelToolsRailAccessoryBase {\n type: 'shortcut';\n /** Accessible shortcut name and action-label fallback. */\n ariaLabel: string;\n /** One or two displayed graphemes. Longer values are truncated visually. */\n initials: string;\n /** Supplemental notification dot. Include its meaning in the accessible label when needed. */\n dot?: boolean;\n action: PanelToolsRailAccessoryAction;\n}\n\n/**\n * A direct application intent that never selects a tool or changes drawer state.\n * PanelTools owns the two-control-height surface and its focus behavior.\n */\nexport interface PanelToolsRailTransientAccessory extends PanelToolsRailAccessoryBase {\n type: 'transient';\n /** Accessible accessory name and primary-action fallback label. */\n ariaLabel: string;\n visual: PanelToolsRailAccessoryVisual;\n /** State announced with the primary action and shown in its tooltip. */\n statusText: string;\n /** Bold surface treatment. Active uses brand; positive uses positive semantic color. */\n statusTone: PanelToolsRailTransientTone;\n primaryAction: PanelToolsRailAccessoryAction;\n secondaryAction?: PanelToolsRailAccessorySecondaryAction;\n}\n\nexport type PanelToolsRailAccessory =\n | PanelToolsRailDividerAccessory\n | PanelToolsRailShortcutAccessory\n | PanelToolsRailTransientAccessory;\n\nexport interface PanelToolsRailAccessoryActionDetail {\n accessoryId: string;\n actionId: string;\n /** Rendered action control, suitable for anchoring an application-owned overlay. */\n anchor: HTMLElement;\n}\n\n/** Canonical shell recipe used by Lab and by managed ShellApp examples. */\nexport const PANEL_TOOLS_DEFAULT_ITEMS: PanelToolsItem[] = [\n {\n id: 'agents',\n icon: 'AI',\n label: PANEL_TOOLS_LABELS.agents,\n railPlacement: 'header',\n order: 0,\n shortcutKey: PANEL_TOOLS_SHORTCUTS.agents,\n mobileDestination: 'agents',\n },\n {\n id: 'messages',\n icon: 'MessageBubbleStack',\n label: PANEL_TOOLS_LABELS.messages,\n order: 0,\n shortcutKey: PANEL_TOOLS_SHORTCUTS.messages,\n mobileDestination: 'messages',\n },\n {\n id: 'activity',\n icon: 'Bell',\n label: PANEL_TOOLS_LABELS.activity,\n order: 1,\n shortcutKey: PANEL_TOOLS_SHORTCUTS.activity,\n mobileDestination: 'activity',\n },\n {\n id: 'search',\n icon: 'MagnifyingGlass',\n label: PANEL_TOOLS_LABELS.search,\n order: 2,\n shortcutKey: PANEL_TOOLS_SHORTCUTS.search,\n mobileDestination: 'search',\n },\n {\n id: 'help',\n icon: 'CircleQuestion',\n label: PANEL_TOOLS_LABELS.help,\n railPlacement: 'footer',\n order: 0,\n shortcutKey: PANEL_TOOLS_SHORTCUTS.help,\n mobileDestination: 'help',\n },\n];\n\nexport interface PanelToolsHeaderAction {\n id: string;\n icon: string;\n ariaLabel: string;\n triggerId?: string;\n controls?: string;\n expanded?: boolean;\n /** Associated surface remains visible, including exit motion. */\n surfaceOpen?: boolean;\n haspopup?: 'menu' | 'dialog' | 'listbox' | 'tree' | 'grid';\n pressed?: boolean;\n isInactive?: boolean;\n}\n\nexport interface PanelToolsHeaderActionDetail {\n tool: PanelToolsToolId;\n id: string;\n /** Rendered header action host. Pass this to an external overlay's element anchor prop. */\n anchor?: HTMLElement;\n}\n\nexport interface PanelToolHeaderActionDetail {\n id: string;\n originalEvent: MouseEvent;\n /** Rendered action host, suitable for anchoring an external overlay across a shadow boundary. */\n anchor: HTMLElement;\n}\n\nexport interface PanelToolsHeaderConfig {\n title?: string;\n showBack?: boolean;\n backIcon?: string;\n backAriaLabel?: string;\n actions?: PanelToolsHeaderAction[];\n}\n\n/** Per-tool header state. The application replaces the object when a tool changes depth or actions. */\nexport type PanelToolsHeaders = Partial<Record<PanelToolsToolId, PanelToolsHeaderConfig>>;\n", "/** Nav chrome style slot \u2014 dashboard vs settings. Colors unified for now; texture hooks use the class. */\nexport type NavChromeStyle = 'dashboard' | 'settings';\n\n/** Document hint for first paint before framework props land. */\nexport const NAV_STYLE_HINT_ATTR = 'data-nav-style';\n\nexport function readNavStyleAttr(attr: string | null): NavChromeStyle | undefined {\n if (attr === 'dashboard' || attr === 'settings') return attr;\n return undefined;\n}\n\nexport function setNavStyleHint(style: NavChromeStyle): void {\n if (typeof document === 'undefined') return;\n document.documentElement.setAttribute(NAV_STYLE_HINT_ATTR, style);\n}\n\nexport function clearNavStyleHint(): void {\n if (typeof document === 'undefined') return;\n document.documentElement.removeAttribute(NAV_STYLE_HINT_ATTR);\n}\n\nexport function resolveNavChromeStyle(\n styleProp: NavChromeStyle,\n hostAttr: string | null,\n docHint?: string | null\n): NavChromeStyle {\n const fromHost = readNavStyleAttr(hostAttr);\n if (fromHost) return fromHost;\n\n const hint =\n docHint !== undefined\n ? docHint\n : typeof document !== 'undefined'\n ? document.documentElement.getAttribute(NAV_STYLE_HINT_ATTR)\n : null;\n const fromHint = readNavStyleAttr(hint);\n if (fromHint) return fromHint;\n\n return styleProp;\n}\n\nexport function shouldResyncNavChromeStyle(\n renderedStyle: NavChromeStyle,\n style: NavChromeStyle\n): boolean {\n return renderedStyle !== style;\n}\n", "import type { ChoicePopupAnchorAlignment } from '../../utils/choice-popup-alignment';\nimport type { MenuAlign, MenuSide } from './menu-position';\n\n/** Placement props bindable on `ds-menu` (side, align, offsets). */\nexport interface MenuPlacement {\n side: MenuSide;\n align: MenuAlign;\n anchorAlignment: ChoicePopupAnchorAlignment;\n sideOffset: number | string;\n alignOffset: number | string;\n}\n\n/**\n * Canonical `ds-menu` placement for the **panel-nav footer user menu**.\n * Anchor from `dsNavUserAction` detail `{ anchor }` (id `ds-panel-nav-user-menu-anchor`).\n *\n * Not for BarNav overflow menus \u2014 those use `side=\"bottom\"` / `align=\"end\"` internally.\n * `align: 'end'` bottom-aligns the last choice cell on the anchor; the side offset nudges into the content area:\n * - `sideOffset` space-100 + space-050 (12px): 4px past the panel-nav outer edge (anchor is ~8px inset).\n * - `anchorAlignment: 'choice-cell'`: the shared section inset puts the last row edge on the footer user row.\n */\nexport const PANEL_NAV_USER_MENU_PLACEMENT = {\n side: 'right',\n align: 'end',\n anchorAlignment: 'choice-cell',\n sideOffset: 'calc(var(--dimension-space-100) + var(--dimension-space-050))',\n alignOffset: 0,\n} as const satisfies MenuPlacement;\n\n/**\n * Canonical placement for PanelNav's collapsed child-route flyout.\n * The parent control is 8px inside the panel edge, so a 12px side offset leaves\n * a 4px visual gap outside the panel. Start choice-cell alignment places the\n * first flyout row on the same block axis as its parent control.\n */\nexport const PANEL_NAV_CHILD_MENU_PLACEMENT = {\n side: 'right',\n align: 'start',\n anchorAlignment: 'choice-cell',\n sideOffset: 'calc(var(--dimension-space-100) + var(--dimension-space-050))',\n alignOffset: 0,\n} as const satisfies MenuPlacement;\n\n/**\n * Canonical placement for a menu opened by a PanelTools header action.\n * Matches BarNav overflow: the 32px trigger is 8px inside the 48px header, so\n * a 12px side offset places the popup 4px below the header edge. Choice-cell\n * alignment keeps the final menu item edge aligned with the trigger.\n */\nexport const PANEL_TOOLS_HEADER_MENU_PLACEMENT = {\n side: 'bottom',\n align: 'end',\n anchorAlignment: 'choice-cell',\n sideOffset: 'calc(var(--dimension-space-100) + var(--dimension-space-050))',\n alignOffset: 0,\n} as const satisfies MenuPlacement;\n", "/** Built-in shell chrome wash presets. */\nexport type ShellGradientPreset = 'none' | 'cool' | 'neutral' | 'warm' | 'fresh';\n\nexport const SHELL_GRADIENT_PRESETS: ShellGradientPreset[] = [\n 'none',\n 'cool',\n 'neutral',\n 'warm',\n 'fresh',\n];\n\n/** Wash presets shown after the `none` option in pickers. */\nexport const SHELL_GRADIENT_WASH_PRESETS: Exclude<ShellGradientPreset, 'none'>[] = [\n 'cool',\n 'neutral',\n 'warm',\n 'fresh',\n];\n\n/** Default wash when `gradient-preset` is omitted. */\nexport const DEFAULT_SHELL_GRADIENT_PRESET: ShellGradientPreset = 'neutral';\n\nexport const SHELL_GRADIENT_PRESET_LABELS: Record<ShellGradientPreset, string> = {\n none: 'None',\n cool: 'Cool',\n neutral: 'Neutral',\n warm: 'Warm',\n fresh: 'Fresh',\n};\n\nexport interface ShellGradientStop {\n color: string;\n position: number;\n}\n\nexport interface ShellGradientRecipe {\n opacity: number;\n stops: readonly ShellGradientStop[];\n}\n\nexport interface ShellGradientPickerPreview {\n backgroundColor: string;\n backgroundImage?: string;\n opacity?: number;\n}\n\nexport interface ShellGradientPickerOption {\n value: ShellGradientPreset;\n label: string;\n preview: ShellGradientPickerPreview;\n}\n\nexport interface ShellGradientPickerSection {\n options: ShellGradientPickerOption[];\n}\n\nconst SHELL_GRADIENT_RECIPES: Record<Exclude<ShellGradientPreset, 'none'>, ShellGradientRecipe> = {\n cool: {\n opacity: 0.1,\n stops: [\n { color: 'var(--color-background-transparent)', position: 0 },\n { color: 'var(--color-color-intent-blue-strong-background)', position: 100 },\n ],\n },\n neutral: {\n opacity: 0.1,\n stops: [\n { color: 'var(--color-background-transparent)', position: 0 },\n { color: 'var(--color-color-intent-grey-strong-background)', position: 100 },\n ],\n },\n warm: {\n opacity: 0.1,\n stops: [\n { color: 'var(--color-background-transparent)', position: 0 },\n { color: 'var(--color-color-intent-yellow-strong-background)', position: 100 },\n ],\n },\n fresh: {\n opacity: 0.4,\n stops: [\n { color: 'var(--color-color-intent-cyan-faint-background)', position: 0 },\n { color: 'var(--color-background-transparent)', position: 47 },\n { color: 'var(--color-background-transparent)', position: 58 },\n { color: 'var(--color-color-intent-red-faint-background)', position: 83 },\n { color: 'var(--color-color-intent-pink-faint-background)', position: 100 },\n ],\n },\n};\n\nconst GRADIENT_GEOMETRY = '100% 100% at 0% 0%';\n\nexport function shellGradientPresetStopToken(preset: ShellGradientPreset): string | null {\n if (preset === 'none') return null;\n const stops = SHELL_GRADIENT_RECIPES[preset].stops;\n return stops[stops.length - 1]?.color ?? null;\n}\n\nexport function shellGradientPresetRecipe(preset: ShellGradientPreset): ShellGradientRecipe | null {\n const normalized = normalizeShellGradientPreset(preset);\n return normalized === 'none' ? null : SHELL_GRADIENT_RECIPES[normalized];\n}\n\nexport function shellGradientPresetOpacity(preset: ShellGradientPreset): string {\n return String(shellGradientPresetRecipe(preset)?.opacity ?? 0);\n}\n\nexport function isShellGradientPreset(value: string): value is ShellGradientPreset {\n return (SHELL_GRADIENT_PRESETS as string[]).includes(value);\n}\n\n/**\n * Coerce unknown/absent presets to the default. Runtime values escape the\n * type: removing the reflected `gradient-preset` attribute (e.g. an Angular\n * `[attr.gradient-preset]=\"null\"` binding) drives the prop to null past its\n * field default \u2014 without normalization the stop lookup interpolates the\n * literal string \"undefined\" into CSS and the wash silently disappears.\n */\nexport function normalizeShellGradientPreset(\n value: string | null | undefined\n): ShellGradientPreset {\n return value != null && isShellGradientPreset(value) ? value : DEFAULT_SHELL_GRADIENT_PRESET;\n}\n\n/** Radial wash for a preset \u2014 transparent at top-left into the intent stop. `none` returns no image. */\nexport function buildShellRadialGradientForPreset(preset: ShellGradientPreset): string {\n const normalized = normalizeShellGradientPreset(preset);\n if (normalized === 'none') return 'none';\n\n return buildShellRadialGradientFromStops(SHELL_GRADIENT_RECIPES[normalized].stops);\n}\n\n/** Build the production shell geometry from an ordered set of design-time stops. */\nexport function buildShellRadialGradientFromStops(stops: readonly ShellGradientStop[]): string {\n const serialized = stops.map(stop => `${stop.color} ${stop.position}%`).join(', ');\n return `radial-gradient(${GRADIENT_GEOMETRY}, ${serialized})`;\n}\n\n/** Generic SwatchPicker sections for the fixed shell appearance presets. */\nexport function shellGradientPickerSections(): ShellGradientPickerSection[] {\n return [\n {\n options: [\n {\n value: 'none',\n label: SHELL_GRADIENT_PRESET_LABELS.none,\n preview: {\n backgroundColor: 'var(--color-background-secondary)',\n },\n },\n ],\n },\n {\n options: SHELL_GRADIENT_WASH_PRESETS.map(preset => ({\n value: preset,\n label: SHELL_GRADIENT_PRESET_LABELS[preset],\n preview: {\n backgroundColor: 'var(--color-background-secondary)',\n backgroundImage: buildShellRadialGradientForPreset(preset),\n opacity: Number(shellGradientPresetOpacity(preset)),\n },\n })),\n },\n ];\n}\n", "import type { PanelToolsToolId } from '../components/PanelTools/panel-tools-types';\n\nexport type ShellResponsiveMode = 'desktop' | 'tablet' | 'mobile';\nexport type MobileDestination =\n | 'area'\n | 'search'\n | 'inbox'\n | 'activity'\n | 'messages'\n | 'agents'\n | 'help';\nexport type ShellInboxToolId = PanelToolsToolId;\n\nexport const SHELL_DESKTOP_BREAKPOINT = 1200;\nexport const SHELL_MOBILE_BREAKPOINT = 768;\nexport const SHELL_DEFAULT_INBOX_TOOL: ShellInboxToolId = 'activity';\n\nexport function resolveShellResponsiveMode(width: number): ShellResponsiveMode {\n if (width >= SHELL_DESKTOP_BREAKPOINT) return 'desktop';\n if (width >= SHELL_MOBILE_BREAKPOINT) return 'tablet';\n return 'mobile';\n}\n\nexport function resolveManagedShellPageCapacity(\n mode: ShellResponsiveMode\n): 'roomy' | 'compact' | 'constrained' {\n return mode === 'desktop' ? 'roomy' : 'compact';\n}\n\nexport function shellMobileDestinationForTool(\n open: boolean,\n tool: PanelToolsToolId | ''\n): MobileDestination {\n if (!open || !tool) return 'area';\n if (tool === 'help') return 'help';\n if (tool === 'search') return 'search';\n if (tool === 'messages') return 'messages';\n if (tool === 'agents') return 'agents';\n return 'inbox';\n}\n\n/** Explicit item metadata wins over the legacy canonical Stacks/Activity grouping. */\nexport function itemUsesShellInbox(item: {\n id: PanelToolsToolId;\n mobileDestination?: MobileDestination;\n}): boolean {\n return (\n item.mobileDestination === 'inbox' ||\n (item.mobileDestination === undefined && isShellInboxTool(item.id))\n );\n}\n\nexport function isShellInboxTool(tool: string): tool is ShellInboxToolId {\n return tool === 'stacks' || tool === 'activity';\n}\n\nexport function resolveAvailableInboxTool(\n preferred: string,\n available: readonly PanelToolsToolId[]\n): ShellInboxToolId | '' {\n const inbox = available.filter(isShellInboxTool);\n if (isShellInboxTool(preferred) && inbox.includes(preferred)) return preferred;\n if (inbox.includes(SHELL_DEFAULT_INBOX_TOOL)) return SHELL_DEFAULT_INBOX_TOOL;\n return inbox[0] ?? '';\n}\n\n/** Active-destination reselects are inert unless they also dismiss the Menu pane. */\nexport function shouldEmitMobileDestinationChange(\n active: MobileDestination,\n next: MobileDestination,\n navigationOpen: boolean\n): boolean {\n return navigationOpen || active !== next;\n}\n"],
5
5
  "mappings": ";AA2BO,IAAM,qBAAgE;AAAA,EAC3E,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,MAAM;AACR;AAEO,IAAM,wBAA4E;AAAA,EACvF,QAAQ;AAAA,EACR,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,UAAU;AAAA,EACV,MAAM;AACR;AA2GO,IAAM,4BAA8C;AAAA,EACzD;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,OAAO,mBAAmB;AAAA,IAC1B,eAAe;AAAA,IACf,OAAO;AAAA,IACP,aAAa,sBAAsB;AAAA,IACnC,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,OAAO,mBAAmB;AAAA,IAC1B,OAAO;AAAA,IACP,aAAa,sBAAsB;AAAA,IACnC,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,OAAO,mBAAmB;AAAA,IAC1B,OAAO;AAAA,IACP,aAAa,sBAAsB;AAAA,IACnC,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,OAAO,mBAAmB;AAAA,IAC1B,OAAO;AAAA,IACP,aAAa,sBAAsB;AAAA,IACnC,mBAAmB;AAAA,EACrB;AAAA,EACA;AAAA,IACE,IAAI;AAAA,IACJ,MAAM;AAAA,IACN,OAAO,mBAAmB;AAAA,IAC1B,eAAe;AAAA,IACf,OAAO;AAAA,IACP,aAAa,sBAAsB;AAAA,IACnC,mBAAmB;AAAA,EACrB;AACF;;;AC7LO,IAAM,sBAAsB;AAE5B,SAAS,iBAAiB,MAAiD;AAChF,MAAI,SAAS,eAAe,SAAS,WAAY,QAAO;AACxD,SAAO;AACT;AAEO,SAAS,gBAAgB,OAA6B;AAC3D,MAAI,OAAO,aAAa,YAAa;AACrC,WAAS,gBAAgB,aAAa,qBAAqB,KAAK;AAClE;AAEO,SAAS,oBAA0B;AACxC,MAAI,OAAO,aAAa,YAAa;AACrC,WAAS,gBAAgB,gBAAgB,mBAAmB;AAC9D;;;ACEO,IAAM,gCAAgC;AAAA,EAC3C,MAAM;AAAA,EACN,OAAO;AAAA,EACP,iBAAiB;AAAA,EACjB,YAAY;AAAA,EACZ,aAAa;AACf;AAsBO,IAAM,oCAAoC;AAAA,EAC/C,MAAM;AAAA,EACN,OAAO;AAAA,EACP,iBAAiB;AAAA,EACjB,YAAY;AAAA,EACZ,aAAa;AACf;;;ACpDO,IAAM,yBAAgD;AAAA,EAC3D;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,IAAM,8BAAsE;AAAA,EACjF;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAGO,IAAM,gCAAqD;AAE3D,IAAM,+BAAoE;AAAA,EAC/E,MAAM;AAAA,EACN,MAAM;AAAA,EACN,SAAS;AAAA,EACT,MAAM;AAAA,EACN,OAAO;AACT;AA4BA,IAAM,yBAA4F;AAAA,EAChG,MAAM;AAAA,IACJ,SAAS;AAAA,IACT,OAAO;AAAA,MACL,EAAE,OAAO,uCAAuC,UAAU,EAAE;AAAA,MAC5D,EAAE,OAAO,oDAAoD,UAAU,IAAI;AAAA,IAC7E;AAAA,EACF;AAAA,EACA,SAAS;AAAA,IACP,SAAS;AAAA,IACT,OAAO;AAAA,MACL,EAAE,OAAO,uCAAuC,UAAU,EAAE;AAAA,MAC5D,EAAE,OAAO,oDAAoD,UAAU,IAAI;AAAA,IAC7E;AAAA,EACF;AAAA,EACA,MAAM;AAAA,IACJ,SAAS;AAAA,IACT,OAAO;AAAA,MACL,EAAE,OAAO,uCAAuC,UAAU,EAAE;AAAA,MAC5D,EAAE,OAAO,sDAAsD,UAAU,IAAI;AAAA,IAC/E;AAAA,EACF;AAAA,EACA,OAAO;AAAA,IACL,SAAS;AAAA,IACT,OAAO;AAAA,MACL,EAAE,OAAO,mDAAmD,UAAU,EAAE;AAAA,MACxE,EAAE,OAAO,uCAAuC,UAAU,GAAG;AAAA,MAC7D,EAAE,OAAO,uCAAuC,UAAU,GAAG;AAAA,MAC7D,EAAE,OAAO,kDAAkD,UAAU,GAAG;AAAA,MACxE,EAAE,OAAO,mDAAmD,UAAU,IAAI;AAAA,IAC5E;AAAA,EACF;AACF;AAEA,IAAM,oBAAoB;AAQnB,SAAS,0BAA0B,QAAyD;AACjG,QAAM,aAAa,6BAA6B,MAAM;AACtD,SAAO,eAAe,SAAS,OAAO,uBAAuB,UAAU;AACzE;AAEO,SAAS,2BAA2B,QAAqC;AAC9E,SAAO,OAAO,0BAA0B,MAAM,GAAG,WAAW,CAAC;AAC/D;AAEO,SAAS,sBAAsB,OAA6C;AACjF,SAAQ,uBAAoC,SAAS,KAAK;AAC5D;AASO,SAAS,6BACd,OACqB;AACrB,SAAO,SAAS,QAAQ,sBAAsB,KAAK,IAAI,QAAQ;AACjE;AAGO,SAAS,kCAAkC,QAAqC;AACrF,QAAM,aAAa,6BAA6B,MAAM;AACtD,MAAI,eAAe,OAAQ,QAAO;AAElC,SAAO,kCAAkC,uBAAuB,UAAU,EAAE,KAAK;AACnF;AAGO,SAAS,kCAAkC,OAA6C;AAC7F,QAAM,aAAa,MAAM,IAAI,UAAQ,GAAG,KAAK,KAAK,IAAI,KAAK,QAAQ,GAAG,EAAE,KAAK,IAAI;AACjF,SAAO,mBAAmB,iBAAiB,KAAK,UAAU;AAC5D;AAGO,SAAS,8BAA4D;AAC1E,SAAO;AAAA,IACL;AAAA,MACE,SAAS;AAAA,QACP;AAAA,UACE,OAAO;AAAA,UACP,OAAO,6BAA6B;AAAA,UACpC,SAAS;AAAA,YACP,iBAAiB;AAAA,UACnB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,IACA;AAAA,MACE,SAAS,4BAA4B,IAAI,aAAW;AAAA,QAClD,OAAO;AAAA,QACP,OAAO,6BAA6B,MAAM;AAAA,QAC1C,SAAS;AAAA,UACP,iBAAiB;AAAA,UACjB,iBAAiB,kCAAkC,MAAM;AAAA,UACzD,SAAS,OAAO,2BAA2B,MAAM,CAAC;AAAA,QACpD;AAAA,MACF,EAAE;AAAA,IACJ;AAAA,EACF;AACF;;;ACvJO,IAAM,2BAA2B;AACjC,IAAM,0BAA0B;AAChC,IAAM,2BAA6C;AAEnD,SAAS,2BAA2B,OAAoC;AAC7E,MAAI,SAAS,yBAA0B,QAAO;AAC9C,MAAI,SAAS,wBAAyB,QAAO;AAC7C,SAAO;AACT;AAQO,SAAS,8BACd,MACA,MACmB;AACnB,MAAI,CAAC,QAAQ,CAAC,KAAM,QAAO;AAC3B,MAAI,SAAS,OAAQ,QAAO;AAC5B,MAAI,SAAS,SAAU,QAAO;AAC9B,MAAI,SAAS,WAAY,QAAO;AAChC,MAAI,SAAS,SAAU,QAAO;AAC9B,SAAO;AACT;AAaO,SAAS,iBAAiB,MAAwC;AACvE,SAAO,SAAS,YAAY,SAAS;AACvC;AAEO,SAAS,0BACd,WACA,WACuB;AACvB,QAAM,QAAQ,UAAU,OAAO,gBAAgB;AAC/C,MAAI,iBAAiB,SAAS,KAAK,MAAM,SAAS,SAAS,EAAG,QAAO;AACrE,MAAI,MAAM,SAAS,wBAAwB,EAAG,QAAO;AACrD,SAAO,MAAM,CAAC,KAAK;AACrB;AAGO,SAAS,kCACd,QACA,MACA,gBACS;AACT,SAAO,kBAAkB,WAAW;AACtC;",
6
6
  "names": []
7
7
  }
@@ -22,6 +22,8 @@ export interface ChoiceOptionAction {
22
22
  controls?: string;
23
23
  /** Keep the action visible and expose its controlled popup state. */
24
24
  expanded?: boolean;
25
+ /** Keep the action visible and held while its associated surface is rendered. */
26
+ surfaceOpen?: boolean;
25
27
  }
26
28
  export interface ChoiceSection {
27
29
  header?: string;
@@ -67,7 +67,8 @@
67
67
  "On non-tab detail routes, show the parent PanelNav destination label in BarNav while the detail-page title remains in page content.",
68
68
  "Dividers and groups visually organize peer route sections only and do not create hierarchy.",
69
69
  "The application filters unauthorized sections before passing tabs to BarNav.",
70
- "Dashboard and Settings route state updates PanelNav groups, BarNav tabs, heading, basePath, currentUrl, and navStyle as one coordinated change."
70
+ "Dashboard and Settings route state updates PanelNav groups, BarNav tabs, heading, basePath, currentUrl, and navStyle as one coordinated change.",
71
+ "The overflow trigger automatically retains the shared surface-open wash through menu exit, separately from the selected route."
71
72
  ],
72
73
  "responsiveBehavior": [
73
74
  "Keep the BarNav region present so shell height and main-content origin remain stable across routes.",
@@ -100,6 +101,10 @@
100
101
  {
101
102
  "label": "Application shell composition",
102
103
  "path": "agent/patterns/application-shell/pattern.agent.json"
104
+ },
105
+ {
106
+ "label": "Surface ownership and trigger state",
107
+ "path": "src/docs/SurfaceOwnership.mdx"
103
108
  }
104
109
  ]
105
110
  },
@@ -306,7 +311,7 @@
306
311
  },
307
312
  {
308
313
  "path": "src/wc/components/BarNav/BarNav.tsx",
309
- "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 { deriveBarNavValueFromUrl, shouldResyncBarNavProps } 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 itemWidths: number[];\n itemGap: number;\n listExtraWidth: number;\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 !== '' && this.committedSection !== '' && 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(this.currentUrl, this.basePath, tabs);\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(`[data-tab-id=\"${id}\"]`) 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={\n probe\n ? undefined\n : () => {\n this.overflowRovingFocused = false;\n }\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 ? 0 : -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 'ds-control-label-box': true,\n 'ds-control-label-dot': !!tab.dot,\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 ds-control-label-dot__badge\"\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 = this.basePath === '' || this.committedSection === this.basePath;\n const hasTabs = 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': 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}`}>{this.renderTabList(this.resolvedTabs, true)}</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\n variant=\"icon\"\n class={{\n 'bar-nav__overflow-trigger': true,\n }}\n icon=\"ChevronDown\"\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 </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",
314
+ "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 { deriveBarNavValueFromUrl, shouldResyncBarNavProps } 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 itemWidths: number[];\n itemGap: number;\n listExtraWidth: number;\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 !== '' && this.committedSection !== '' && 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(this.currentUrl, this.basePath, tabs);\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(`[data-tab-id=\"${id}\"]`) 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={\n probe\n ? undefined\n : () => {\n this.overflowRovingFocused = false;\n }\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 ? 0 : -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 'ds-control-label-box': true,\n 'ds-control-label-dot': !!tab.dot,\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 ds-control-label-dot__badge\"\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 = this.basePath === '' || this.committedSection === this.basePath;\n const hasTabs = 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': 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}`}>{this.renderTabList(this.resolvedTabs, true)}</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\n variant=\"icon\"\n class={{\n 'bar-nav__overflow-trigger': true,\n }}\n icon=\"ChevronDown\"\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.menuOpen}\n surfaceOpen={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 </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",
310
315
  "type": "registry:ui"
311
316
  },
312
317
  {
@@ -74,7 +74,8 @@
74
74
  "expanded renders the larger heading, optional description, inset divider, and a full-width caption breadcrumb row above the title. The breadcrumb begins after 32px top padding; the following title and right-side actions share one top alignment. Compact renders 48px chrome with the existing icon Back action; constrained uses compact chrome and moves an auto-collapse primary action into overflow.",
75
75
  "The one visible ordered action collapses by default in constrained presentation. Use never for an exceptional action whose hierarchy must remain visible; explicit overflow actions never leave the shared menu.",
76
76
  "showDivider=false removes the bottom divider when the surrounding page composition supplies the boundary; showCompactDivider can independently restore or remove that boundary in compact and constrained variants.",
77
- "placement=shell-bar is reserved for ShellApp panel section navigation. It remains compact, omits description and breadcrumb presentation, and preserves applicable Back, section, primary, and overflow controls."
77
+ "placement=shell-bar is reserved for ShellApp panel section navigation. It remains compact, omits description and breadcrumb presentation, and preserves applicable Back, section, primary, and overflow controls.",
78
+ "Section, action, split-menu, and overflow triggers automatically retain the shared surface-open wash through exit. The expanded header presentation is layout, not surface ownership, and does not itself acquire a pressed wash."
78
79
  ],
79
80
  "responsiveBehavior": [
80
81
  "BarTitle does not observe viewport, shell, scroll, or container state. Its owner selects an explicit variant.",
@@ -111,6 +112,10 @@
111
112
  {
112
113
  "label": "Cross-browser shell-page and header coverage",
113
114
  "path": "tests/e2e/bar-title.spec.ts"
115
+ },
116
+ {
117
+ "label": "Surface ownership and trigger state",
118
+ "path": "src/docs/SurfaceOwnership.mdx"
114
119
  }
115
120
  ]
116
121
  },
@@ -552,12 +557,12 @@
552
557
  "files": [
553
558
  {
554
559
  "path": "src/wc/components/BarTitle/BarTitle.css",
555
- "content": "@import '../../utils/control-density.css';\n@import '../../utils/chrome-header.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n\n:host {\n display: block;\n min-width: 0;\n box-sizing: border-box;\n background: var(--_shell-page-header-background, var(--color-background-primary));\n}\n\n:host([placement='shell-bar']) {\n --_shell-page-header-background: var(--color-background-secondary);\n}\n\n:host-context(ds-shell-app:not([gradient-preset='none']))[placement='shell-bar'],\n:host-context(ds-shell-app:not([gradient-preset='none']))[placement='shell-bar'] .bar-title,\n:host-context(ds-shell-app.shell-app--paper-texture)[placement='shell-bar'],\n:host-context(ds-shell-app.shell-app--paper-texture)[placement='shell-bar'] .bar-title {\n background: transparent;\n}\n\n:host([data-shell-page-syncing]) {\n visibility: hidden;\n}\n\n.bar-title {\n position: relative;\n min-width: 0;\n box-sizing: border-box;\n background: var(--_shell-page-header-background, var(--color-background-primary));\n}\n\n.bar-title::after {\n content: '';\n position: absolute;\n inset: auto var(--ds-bar-title-divider-inset, var(--dimension-space-400)) 0;\n height: var(--dimension-stroke-width-012);\n background: var(--color-border-tertiary);\n pointer-events: none;\n}\n\n.bar-title--divider-hidden::after {\n display: none;\n}\n\n.bar-title__inner {\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: stretch;\n gap: var(--dimension-space-100);\n padding: var(--dimension-space-400);\n box-sizing: border-box;\n}\n\n.bar-title__row {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--dimension-space-100);\n min-height: var(--dimension-size-400);\n min-width: 0;\n}\n\n:host(.bar-title-host--expanded) .bar-title__row {\n gap: var(--dimension-space-400);\n}\n\n.bar-title__leading {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n gap: var(--dimension-space-100);\n min-width: 0;\n}\n\n.bar-title__title-row {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-100);\n min-width: 0;\n min-height: var(--dimension-size-400);\n}\n\n.bar-title__identity {\n display: flex;\n flex: 0 1 auto;\n align-items: center;\n gap: var(--dimension-space-050);\n min-width: 0;\n}\n\n.bar-title__back,\n.bar-title__section-chevron {\n flex: 0 0 auto;\n}\n\n.bar-title__breadcrumb {\n display: block;\n width: 100%;\n min-width: 0;\n}\n\n.bar-title__heading {\n display: block;\n width: auto;\n min-width: 0;\n user-select: none;\n}\n\n.bar-title__divider {\n width: var(--dimension-stroke-width-012);\n height: var(--dimension-size-250);\n margin-inline-start: var(--dimension-space-100);\n flex: 0 0 var(--dimension-stroke-width-012);\n background: var(--color-border-tertiary);\n}\n\n.bar-title__section-selector {\n display: flex;\n flex: 0 1 auto;\n min-width: 0;\n max-width: var(--dimension-panel-width-sm);\n}\n\n.bar-title__section-trigger {\n --ds-control-radius: var(--dimension-radius-half);\n\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--ds-control-gap);\n height: var(--ds-control-height);\n min-width: 0;\n max-width: 100%;\n padding-inline: var(--ds-control-padding-inline);\n border: none;\n border-radius: var(--ds-control-radius);\n box-sizing: border-box;\n background: transparent;\n color: var(--color-foreground-secondary);\n cursor: default;\n font-family: inherit;\n user-select: none;\n}\n\n.bar-title__section-trigger--expanded {\n color: var(--color-foreground-primary);\n --ds-interaction-hover: var(--ds-interaction-pressed);\n}\n\n.bar-title__section-trigger--expanded::after {\n background: var(--ds-interaction-pressed);\n}\n\n.bar-title__section-label {\n min-width: 0;\n padding-inline: var(--ds-control-label-inset);\n}\n\n.bar-title__actions {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-100);\n flex: 0 0 auto;\n}\n\n.bar-title__description {\n display: block;\n min-width: 0;\n max-width: 100%;\n}\n\n:host(.bar-title-host--compact) .bar-title {\n height: var(--dimension-size-600);\n}\n\n:host(.bar-title-host--compact) .bar-title::after {\n inset-inline: 0;\n}\n\n:host(.bar-title-host--compact) .bar-title__inner {\n flex: 1 1 auto;\n height: 100%;\n min-width: 0;\n padding: 0;\n gap: 0;\n}\n\n:host(.bar-title-host--compact) .bar-title__row {\n height: 100%;\n align-items: center;\n}\n\n:host(.bar-title-host--compact) .bar-title__identity {\n height: var(--dimension-size-400);\n}\n\n:host(.bar-title-host--compact) .bar-title__divider {\n margin-inline-start: 0;\n}\n\n:host(.bar-title-host--compact) .bar-title__heading {\n display: inline-flex;\n flex: 1 1 auto;\n align-items: center;\n height: var(--dimension-size-400);\n padding-inline: calc(var(--ds-control-padding-inline) + var(--ds-control-label-inset));\n box-sizing: border-box;\n}\n",
560
+ "content": "@import '../../utils/control-density.css';\n@import '../../utils/chrome-header.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n\n:host {\n display: block;\n min-width: 0;\n box-sizing: border-box;\n background: var(--_shell-page-header-background, var(--color-background-primary));\n}\n\n:host([placement='shell-bar']) {\n --_shell-page-header-background: var(--color-background-secondary);\n}\n\n:host-context(ds-shell-app:not([gradient-preset='none']))[placement='shell-bar'],\n:host-context(ds-shell-app:not([gradient-preset='none']))[placement='shell-bar'] .bar-title,\n:host-context(ds-shell-app.shell-app--paper-texture)[placement='shell-bar'],\n:host-context(ds-shell-app.shell-app--paper-texture)[placement='shell-bar'] .bar-title {\n background: transparent;\n}\n\n:host([data-shell-page-syncing]) {\n visibility: hidden;\n}\n\n.bar-title {\n position: relative;\n min-width: 0;\n box-sizing: border-box;\n background: var(--_shell-page-header-background, var(--color-background-primary));\n}\n\n.bar-title::after {\n content: '';\n position: absolute;\n inset: auto var(--ds-bar-title-divider-inset, var(--dimension-space-400)) 0;\n height: var(--dimension-stroke-width-012);\n background: var(--color-border-tertiary);\n pointer-events: none;\n}\n\n.bar-title--divider-hidden::after {\n display: none;\n}\n\n.bar-title__inner {\n position: relative;\n display: flex;\n flex-direction: column;\n align-items: stretch;\n gap: var(--dimension-space-100);\n padding: var(--dimension-space-400);\n box-sizing: border-box;\n}\n\n.bar-title__row {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: var(--dimension-space-100);\n min-height: var(--dimension-size-400);\n min-width: 0;\n}\n\n:host(.bar-title-host--expanded) .bar-title__row {\n gap: var(--dimension-space-400);\n}\n\n.bar-title__leading {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n gap: var(--dimension-space-100);\n min-width: 0;\n}\n\n.bar-title__title-row {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-100);\n min-width: 0;\n min-height: var(--dimension-size-400);\n}\n\n.bar-title__identity {\n display: flex;\n flex: 0 1 auto;\n align-items: center;\n gap: var(--dimension-space-050);\n min-width: 0;\n}\n\n.bar-title__back,\n.bar-title__section-chevron {\n flex: 0 0 auto;\n}\n\n.bar-title__breadcrumb {\n display: block;\n width: 100%;\n min-width: 0;\n}\n\n.bar-title__heading {\n display: block;\n width: auto;\n min-width: 0;\n user-select: none;\n}\n\n.bar-title__divider {\n width: var(--dimension-stroke-width-012);\n height: var(--dimension-size-250);\n margin-inline-start: var(--dimension-space-100);\n flex: 0 0 var(--dimension-stroke-width-012);\n background: var(--color-border-tertiary);\n}\n\n.bar-title__section-selector {\n display: flex;\n flex: 0 1 auto;\n min-width: 0;\n max-width: var(--dimension-panel-width-sm);\n}\n\n.bar-title__section-trigger {\n --ds-control-radius: var(--dimension-radius-half);\n\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: var(--ds-control-gap);\n height: var(--ds-control-height);\n min-width: 0;\n max-width: 100%;\n padding-inline: var(--ds-control-padding-inline);\n border: none;\n border-radius: var(--ds-control-radius);\n box-sizing: border-box;\n background: transparent;\n color: var(--color-foreground-secondary);\n cursor: default;\n font-family: inherit;\n user-select: none;\n}\n\n.bar-title__section-label {\n min-width: 0;\n padding-inline: var(--ds-control-label-inset);\n}\n\n.bar-title__actions {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-100);\n flex: 0 0 auto;\n}\n\n.bar-title__description {\n display: block;\n min-width: 0;\n max-width: 100%;\n}\n\n:host(.bar-title-host--compact) .bar-title {\n height: var(--dimension-size-600);\n}\n\n:host(.bar-title-host--compact) .bar-title::after {\n inset-inline: 0;\n}\n\n:host(.bar-title-host--compact) .bar-title__inner {\n flex: 1 1 auto;\n height: 100%;\n min-width: 0;\n padding: 0;\n gap: 0;\n}\n\n:host(.bar-title-host--compact) .bar-title__row {\n height: 100%;\n align-items: center;\n}\n\n:host(.bar-title-host--compact) .bar-title__identity {\n height: var(--dimension-size-400);\n}\n\n:host(.bar-title-host--compact) .bar-title__divider {\n margin-inline-start: 0;\n}\n\n:host(.bar-title-host--compact) .bar-title__heading {\n display: inline-flex;\n flex: 1 1 auto;\n align-items: center;\n height: var(--dimension-size-400);\n padding-inline: calc(var(--ds-control-padding-inline) + var(--ds-control-label-inset));\n box-sizing: border-box;\n}\n",
556
561
  "type": "registry:ui"
557
562
  },
558
563
  {
559
564
  "path": "src/wc/components/BarTitle/BarTitle.tsx",
560
- "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport type { BreadcrumbItem, BreadcrumbSelectDetail } from '../Breadcrumb/breadcrumb-types';\nimport type { MenuItemData, MenuSection } from '../Menu/menu-types';\nimport {\n isBarTitleDivider,\n type BarTitleActionConfigItem,\n type BarTitleActionItem,\n type BarTitleMenuAction,\n type BarTitlePrimaryAction,\n type BarTitleSection,\n type BarTitleSectionItem,\n type BarTitlePlacement,\n type BarTitleSplitAction,\n type BarTitleVariant,\n} from './bar-title-types';\nimport {\n barTitleActionIdIssues,\n barTitleChoiceSections,\n findBarTitleAction,\n overflowBarTitleActionSections,\n resolveBarTitleActionItems,\n visibleBarTitleActions,\n} from './bar-title-actions';\n\nlet nextBarTitleId = 0;\n\ntype FocusableButton = HTMLElement & {\n setFocus?: (segment?: 'primary' | 'menu') => Promise<void>;\n};\n\n@Component({\n tag: 'ds-bar-title',\n styleUrl: 'BarTitle.css',\n scoped: true,\n})\nexport class BarTitle {\n @Element() el!: HTMLElement;\n\n /** The page's single visible h1. */\n @Prop() heading!: string;\n\n /** Optional supporting copy shown in the expanded presentation. */\n @Prop() description: string = '';\n\n /** Show the leading page-level Back action. */\n @Prop({ attribute: 'show-back' }) showBack: boolean = false;\n\n /** Accessible name for the leading Back action. */\n @Prop() backAriaLabel: string = 'Back';\n\n /** Visible parent-page label used by the expanded breadcrumb when breadcrumbs is empty. */\n @Prop() backLabel: string = 'Back';\n\n /** Optional expanded ancestor path. Compact variants continue to use the Back action. */\n @Prop() breadcrumbs: BreadcrumbItem[] = [];\n\n /** Accessible name for the expanded breadcrumb navigation landmark. */\n @Prop() breadcrumbAriaLabel: string = 'Breadcrumb';\n\n /** Optional page sections exposed through the active-section menu. */\n @Prop() sections: BarTitleSectionItem[] = [];\n\n /** Id of the active page section. */\n @Prop() value: string = '';\n\n /** Accessible name for the page-section menu. */\n @Prop() sectionsAriaLabel: string = 'Change page section';\n\n /** The one highest-emphasis page action. */\n @Prop() primaryAction: BarTitlePrimaryAction | null = null;\n\n /** Secondary page actions shown in the overflow menu. Dividers create groups. */\n @Prop() actions: BarTitleActionItem[] = [];\n\n /**\n * Ordered page-header actions. When supplied, this replaces the legacy\n * primaryAction/actions presentation while preserving the same dsAction event.\n */\n @Prop() actionItems?: BarTitleActionConfigItem[];\n\n /** Accessible name for the page-actions menu. */\n @Prop() actionsAriaLabel: string = 'More page actions';\n\n /** Explicit visual/capacity variant. ShellPage owns automatic selection. */\n @Prop() variant: BarTitleVariant = 'expanded';\n\n /** Page-header placement, or compact application-shell bar placement. */\n @Prop({ reflect: true }) placement: BarTitlePlacement = 'page';\n\n /** Draw the page-title divider beneath the header. */\n @Prop() showDivider: boolean = true;\n\n /** Override divider visibility for compact and constrained variants. */\n @Prop() showCompactDivider?: boolean;\n\n /** Emitted when the leading Back action is activated. */\n @Event() dsBack!: EventEmitter<MouseEvent>;\n\n /** Emitted when an authored expanded breadcrumb item is activated. */\n @Event({ cancelable: true }) dsBreadcrumbSelect!: EventEmitter<BreadcrumbSelectDetail>;\n\n /** Emitted with the newly selected page-section id. */\n @Event() dsSectionChange!: EventEmitter<string>;\n\n /** Emitted with the activated primary or overflow action id. */\n @Event() dsAction!: EventEmitter<string>;\n\n @State() private sectionMenuOpen = false;\n @State() private openActionMenuId = '';\n @State() private sectionMenuInitialFocusVisible = false;\n @State() private actionMenuInitialFocusVisible = false;\n\n private readonly instanceId = nextBarTitleId++;\n private readonly sectionMenuTriggerId = `bar-title-section-trigger-${this.instanceId}`;\n private readonly sectionMenuId = `bar-title-section-menu-${this.instanceId}`;\n private readonly actionMenuTriggerId = `bar-title-action-trigger-${this.instanceId}`;\n private readonly actionMenuId = `bar-title-action-menu-${this.instanceId}`;\n private sectionTriggerEl: HTMLButtonElement | null = null;\n private actionTriggerEl: FocusableButton | null = null;\n private actionTriggerEls = new Map<string, FocusableButton>();\n\n componentWillLoad() {\n if (this.el.closest('ds-shell-page')) {\n this.el.setAttribute('data-shell-page-syncing', '');\n }\n this.reportActionIdIssues();\n }\n\n @Watch('sections')\n handleSectionsChange() {\n if (!this.hasSectionSelector) this.closeSectionMenu();\n }\n\n @Watch('actions')\n @Watch('primaryAction')\n @Watch('actionItems')\n handleActionsChange() {\n this.reportActionIdIssues();\n if (!this.availableActionMenuIds.has(this.openActionMenuId)) this.closeActionMenu();\n }\n\n private reportActionIdIssues() {\n for (const issue of barTitleActionIdIssues(this.resolvedActionItems)) {\n console.warn(`[ds-bar-title] ${issue}`);\n }\n }\n\n private get compact(): boolean {\n return this.effectiveVariant !== 'expanded';\n }\n\n private get effectiveVariant(): BarTitleVariant {\n return this.placement === 'shell-bar' ? 'compact' : this.variant;\n }\n\n private get dividerVisible(): boolean {\n return this.compact ? (this.showCompactDivider ?? this.showDivider) : this.showDivider;\n }\n\n private get expandedBreadcrumbItems(): BreadcrumbItem[] {\n if (this.breadcrumbs.length > 0) return this.breadcrumbs;\n if (!this.showBack) return [];\n return [\n {\n id: 'back',\n label: this.backLabel,\n ariaLabel: this.backAriaLabel,\n },\n ];\n }\n\n /** Tooltip copy is fixed and generic; backAriaLabel/actionsAriaLabel stay page-specific. */\n private get backTooltipLabel(): string {\n return 'Go back';\n }\n\n private get primaryCollapsed(): boolean {\n return (\n this.actionItems === undefined &&\n this.effectiveVariant === 'constrained' &&\n this.primaryAction !== null &&\n (this.primaryAction.collapse ?? 'auto') === 'auto'\n );\n }\n\n private get selectableSections(): BarTitleSection[] {\n return this.sections.filter((item): item is BarTitleSection => !isBarTitleDivider(item));\n }\n\n private get hasSectionSelector(): boolean {\n return this.selectableSections.length > 1;\n }\n\n private get effectiveValue(): string {\n return this.selectableSections.some(section => section.id === this.value)\n ? this.value\n : (this.selectableSections[0]?.id ?? '');\n }\n\n private get selectedSectionLabel(): string {\n return this.selectableSections.find(section => section.id === this.effectiveValue)?.label ?? '';\n }\n\n private get sectionTriggerAriaLabel(): string {\n return `${this.sectionsAriaLabel}. Current section: ${this.selectedSectionLabel}`;\n }\n\n private groupsFromSections(): MenuSection[] {\n const groups: MenuSection[] = [];\n let items: MenuItemData[] = [];\n const commit = () => {\n if (items.length > 0) groups.push({ items });\n items = [];\n };\n\n for (const item of this.sections) {\n if (isBarTitleDivider(item)) {\n commit();\n } else {\n items.push({\n label: item.label,\n value: item.id,\n isSelected: item.id === this.effectiveValue,\n isInactive: item.isInactive,\n });\n }\n }\n commit();\n return groups;\n }\n\n private get sectionMenuSections(): MenuSection[] {\n return this.groupsFromSections();\n }\n\n private get resolvedActionItems(): BarTitleActionConfigItem[] {\n return resolveBarTitleActionItems(this.actionItems, this.primaryAction, this.actions);\n }\n\n private get visibleActions(): BarTitleActionConfigItem[] {\n return visibleBarTitleActions(this.resolvedActionItems, this.effectiveVariant);\n }\n\n private get actionMenuSections(): MenuSection[] {\n return overflowBarTitleActionSections(this.resolvedActionItems, this.effectiveVariant);\n }\n\n private get showActionMenuTrigger(): boolean {\n return this.actionMenuSections.length > 0;\n }\n\n private get availableActionMenuIds(): Set<string> {\n const ids = new Set<string>();\n if (this.showActionMenuTrigger) ids.add('__overflow');\n for (const item of this.visibleActions) {\n if (!isBarTitleDivider(item) && (item.type === 'menu' || item.type === 'split')) {\n ids.add(item.id);\n }\n }\n return ids;\n }\n\n private actionMenuDomId(id: string): string {\n const index = this.resolvedActionItems.findIndex(\n item => !isBarTitleDivider(item) && item.id === id\n );\n return `bar-title-action-menu-${this.instanceId}-${index}`;\n }\n\n private actionMenuAnchor(id: string): HTMLElement | undefined {\n if (id === '__overflow') return this.actionTriggerEl ?? undefined;\n const trigger = this.actionTriggerEls.get(id);\n if (!trigger) return undefined;\n const splitMenu = trigger.querySelector<HTMLElement>('.ds-button-split__menu');\n return splitMenu ?? trigger;\n }\n\n private menuSectionsForAction(action: BarTitleMenuAction | BarTitleSplitAction): MenuSection[] {\n return barTitleChoiceSections(action.choices);\n }\n\n private toggleSectionMenu = (event: MouseEvent) => {\n this.sectionMenuInitialFocusVisible = event.detail === 0;\n this.openActionMenuId = '';\n this.sectionMenuOpen = !this.sectionMenuOpen;\n };\n\n private closeSectionMenu = () => {\n this.sectionMenuOpen = false;\n };\n\n private handleSectionSelect = (event: CustomEvent<MenuItemData>) => {\n const id = String(event.detail?.value ?? '');\n const selected = this.selectableSections.find(section => section.id === id);\n if (!selected || selected.isInactive) return;\n this.closeSectionMenu();\n this.dsSectionChange.emit(id);\n requestAnimationFrame(() => this.sectionTriggerEl?.focus());\n };\n\n private toggleActionMenu = (id: string, event: MouseEvent) => {\n this.actionMenuInitialFocusVisible = event.detail === 0;\n this.sectionMenuOpen = false;\n this.openActionMenuId = this.openActionMenuId === id ? '' : id;\n };\n\n private closeActionMenu = () => {\n this.openActionMenuId = '';\n };\n\n private handleActionSelect = (event: CustomEvent<MenuItemData>) => {\n const id = String(event.detail?.value ?? '');\n const action = findBarTitleAction(this.resolvedActionItems, id);\n if (!action || action.isInactive || ('isLoading' in action && action.isLoading)) return;\n const menuId = this.openActionMenuId;\n this.closeActionMenu();\n this.dsAction.emit(id);\n requestAnimationFrame(() => {\n if (menuId === '__overflow') {\n void this.actionTriggerEl?.setFocus?.();\n return;\n }\n const trigger = this.actionTriggerEls.get(menuId);\n if ('setFocus' in (trigger ?? {})) {\n const split = this.resolvedActionItems.some(\n item => !isBarTitleDivider(item) && item.id === menuId && item.type === 'split'\n );\n void trigger?.setFocus?.(split ? 'menu' : undefined);\n }\n });\n };\n\n private renderSectionSelector() {\n if (!this.hasSectionSelector) return null;\n return [\n <div class=\"bar-title__divider\" aria-hidden=\"true\" />,\n <div class=\"bar-title__section-selector\">\n <button\n ref={el => {\n this.sectionTriggerEl = el ?? null;\n }}\n id={this.sectionMenuTriggerId}\n class={{\n 'bar-title__section-trigger': true,\n 'bar-title__section-trigger--expanded': this.sectionMenuOpen,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n }}\n type=\"button\"\n aria-haspopup=\"menu\"\n aria-controls={this.sectionMenuId}\n aria-expanded={String(this.sectionMenuOpen)}\n aria-label={this.sectionTriggerAriaLabel}\n onClick={this.toggleSectionMenu}\n >\n <ds-text\n class=\"bar-title__section-label ds-interaction-fill__content\"\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis\n color=\"primary\"\n lineTruncation={1}\n >\n {this.selectedSectionLabel}\n </ds-text>\n <ds-icon\n class=\"bar-title__section-chevron ds-interaction-fill__content\"\n name=\"ChevronUpDown\"\n size=\"md\"\n color=\"inherit\"\n aria-hidden=\"true\"\n />\n </button>\n </div>,\n ];\n }\n\n private renderVisibleAction(action: BarTitleActionConfigItem) {\n if (isBarTitleDivider(action) || action.type === 'overflow') return null;\n const appearance = action.appearance ?? (action.type === 'split' ? 'filled' : 'unfilled');\n const variant: 'icon' | 'icon-label' | 'label' =\n action.type === 'icon' ? 'icon' : action.icon ? 'icon-label' : 'label';\n const menu = action.type === 'menu';\n const menuId = menu ? this.actionMenuDomId(action.id) : undefined;\n\n if (action.type === 'split') {\n const splitMenuId = this.actionMenuDomId(action.id);\n const splitProps = {\n key: action.id,\n ref: (el: FocusableButton | undefined) => {\n if (el) this.actionTriggerEls.set(action.id, el);\n else this.actionTriggerEls.delete(action.id);\n },\n class: 'bar-title__action bar-title__split-action',\n split: true,\n variant,\n label: action.label,\n icon: action.icon ?? '',\n menuAriaLabel: action.menuAriaLabel,\n controls: splitMenuId,\n expanded: this.openActionMenuId === action.id,\n size: 'md' as const,\n type: action.buttonType ?? 'button',\n isInactive: action.isInactive,\n isLoading: action.isLoading,\n onDsClick: () => this.dsAction.emit(action.id),\n onDsMenuClick: (event: CustomEvent<MouseEvent>) =>\n this.toggleActionMenu(action.id, event.detail),\n };\n return appearance === 'filled' ? (\n <ds-button-filled\n {...splitProps}\n intent={action.intent ?? 'brand'}\n contrast={action.contrast ?? 'bold'}\n />\n ) : (\n <ds-button-unfilled {...splitProps} />\n );\n }\n\n const buttonProps = {\n key: action.id,\n ref: (el: FocusableButton | undefined) => {\n if (el) this.actionTriggerEls.set(action.id, el);\n else this.actionTriggerEls.delete(action.id);\n },\n class: `bar-title__action bar-title__action--${action.type}${\n this.actionItems === undefined && action.id === this.primaryAction?.id\n ? ' bar-title__primary-action'\n : ''\n }`,\n variant,\n icon: action.icon ?? '',\n label: action.label,\n ariaLabel: action.ariaLabel ?? (action.type === 'icon' ? action.label : undefined),\n size: 'md' as const,\n type: action.buttonType ?? 'button',\n isInactive: action.isInactive,\n isLoading: action.isLoading,\n controls: menuId,\n expanded: menu ? this.openActionMenuId === action.id : undefined,\n hasMenu: menu,\n onDsClick: (event: CustomEvent<MouseEvent>) => {\n if (menu) this.toggleActionMenu(action.id, event.detail);\n else this.dsAction.emit(action.id);\n },\n };\n\n const button =\n appearance === 'filled' ? (\n <ds-button-filled\n {...buttonProps}\n intent={action.intent ?? 'brand'}\n contrast={action.contrast ?? 'bold'}\n />\n ) : (\n <ds-button-unfilled {...buttonProps} />\n );\n\n return action.type === 'icon' ? (\n <ds-tooltip key={action.id} label={action.label} side=\"bottom\" size=\"sm\">\n {button}\n </ds-tooltip>\n ) : (\n button\n );\n }\n\n private renderActions() {\n if (this.visibleActions.length === 0 && !this.showActionMenuTrigger) return null;\n\n return (\n <div class=\"bar-title__actions\">\n {this.visibleActions.map(action => this.renderVisibleAction(action))}\n {this.showActionMenuTrigger ? (\n <ds-tooltip label=\"Page options\" side=\"bottom\" size=\"sm\">\n <ds-button-unfilled\n ref={el => {\n this.actionTriggerEl = el ?? null;\n }}\n id={this.actionMenuTriggerId}\n class=\"bar-title__more-actions\"\n variant=\"icon\"\n icon=\"Ellipses\"\n aria-label={this.actionsAriaLabel}\n size=\"md\"\n activeFill={!this.compact}\n hasBorder={this.visibleActions.length > 0}\n haspopup=\"menu\"\n controls={this.actionMenuId}\n expanded={this.openActionMenuId === '__overflow'}\n onDsClick={(event: CustomEvent<MouseEvent>) =>\n this.toggleActionMenu('__overflow', event.detail)\n }\n />\n </ds-tooltip>\n ) : null}\n </div>\n );\n }\n\n private renderActionMenus() {\n return this.visibleActions.flatMap(action => {\n if (isBarTitleDivider(action) || (action.type !== 'menu' && action.type !== 'split')) {\n return [];\n }\n const menuLabel = action.menuAriaLabel ?? action.ariaLabel ?? action.label;\n return [\n <ds-menu\n key={action.id}\n id={this.actionMenuDomId(action.id)}\n class=\"bar-title__action-menu\"\n anchor={this.actionMenuAnchor(action.id)}\n align=\"end\"\n menuLabel={menuLabel}\n open={this.openActionMenuId === action.id}\n initialFocusVisible={this.actionMenuInitialFocusVisible}\n sections={this.menuSectionsForAction(action)}\n onDsClose={this.closeActionMenu}\n onDsSelect={this.handleActionSelect}\n />,\n ];\n });\n }\n\n private handleBreadcrumbSelect = (event: CustomEvent<BreadcrumbSelectDetail>) => {\n if (this.breadcrumbs.length === 0) {\n this.dsBack.emit(event.detail.originalEvent);\n return;\n }\n const forwarded = this.dsBreadcrumbSelect.emit(event.detail);\n if (forwarded.defaultPrevented) event.preventDefault();\n };\n\n private renderBreadcrumb() {\n const items = this.expandedBreadcrumbItems;\n if (items.length === 0) return null;\n return (\n <ds-breadcrumb\n class=\"bar-title__breadcrumb\"\n items={items}\n ariaLabel={this.breadcrumbAriaLabel}\n onDsSelect={this.handleBreadcrumbSelect}\n />\n );\n }\n\n private renderBack() {\n if (!this.showBack) return null;\n return (\n <ds-tooltip label={this.backTooltipLabel} side=\"bottom\" size=\"sm\">\n <ds-button-unfilled\n class=\"bar-title__back\"\n variant=\"icon\"\n icon=\"ChevronLeft\"\n aria-label={this.backAriaLabel}\n size=\"md\"\n activeFill={false}\n hasBorder={false}\n onDsClick={(event: CustomEvent<MouseEvent>) => this.dsBack.emit(event.detail)}\n />\n </ds-tooltip>\n );\n }\n\n render() {\n const compact = this.compact;\n return (\n <Host\n class={{\n 'bar-title-host--compact': compact,\n 'bar-title-host--expanded': this.effectiveVariant === 'expanded',\n 'bar-title-host--constrained': this.effectiveVariant === 'constrained',\n 'bar-title-host--has-description': !!this.description,\n 'bar-title-host--has-back': this.showBack,\n 'bar-title-host--has-breadcrumb': !compact && this.expandedBreadcrumbItems.length > 0,\n 'bar-title-host--shell-bar': this.placement === 'shell-bar',\n }}\n >\n <div\n class={{\n 'bar-title': true,\n 'bar-title--divider-hidden': !this.dividerVisible,\n 'ds-chrome-header': compact,\n }}\n >\n <div class=\"bar-title__inner\">\n {!compact ? this.renderBreadcrumb() : null}\n <div class=\"bar-title__row\">\n <div class=\"bar-title__leading\">\n <div class=\"bar-title__title-row\" data-shell-page-header-anchor>\n <div class=\"bar-title__identity\">\n {compact ? this.renderBack() : null}\n <ds-text\n class=\"bar-title__heading ds-control--md\"\n variant={compact ? 'text-title-small' : 'text-title-medium'}\n emphasis\n color=\"primary\"\n as=\"h1\"\n lineTruncation={1}\n >\n {this.heading}\n </ds-text>\n </div>\n {this.renderSectionSelector()}\n </div>\n {this.description && !compact ? (\n <ds-text\n class=\"bar-title__description\"\n variant=\"text-body-small\"\n color=\"secondary\"\n lineTruncation={2}\n wrap=\"wrap\"\n as=\"p\"\n >\n {this.description}\n </ds-text>\n ) : null}\n </div>\n {this.renderActions()}\n </div>\n </div>\n </div>\n\n {this.hasSectionSelector ? (\n <ds-menu\n id={this.sectionMenuId}\n class=\"bar-title__section-menu\"\n anchorId={this.sectionMenuTriggerId}\n menuLabel={this.sectionsAriaLabel}\n open={this.sectionMenuOpen}\n initialFocusVisible={this.sectionMenuInitialFocusVisible}\n sections={this.sectionMenuSections}\n onDsClose={this.closeSectionMenu}\n onDsSelect={this.handleSectionSelect}\n />\n ) : null}\n\n {this.renderActionMenus()}\n\n {this.showActionMenuTrigger ? (\n <ds-menu\n id={this.actionMenuId}\n class=\"bar-title__action-menu\"\n anchorId={this.actionMenuTriggerId}\n align=\"end\"\n menuLabel={this.actionsAriaLabel}\n open={this.openActionMenuId === '__overflow'}\n initialFocusVisible={this.actionMenuInitialFocusVisible}\n sections={this.actionMenuSections}\n onDsClose={this.closeActionMenu}\n onDsSelect={this.handleActionSelect}\n />\n ) : null}\n </Host>\n );\n }\n}\n",
565
+ "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport type { BreadcrumbItem, BreadcrumbSelectDetail } from '../Breadcrumb/breadcrumb-types';\nimport type { MenuItemData, MenuSection } from '../Menu/menu-types';\nimport {\n isBarTitleDivider,\n type BarTitleActionConfigItem,\n type BarTitleActionItem,\n type BarTitleMenuAction,\n type BarTitlePrimaryAction,\n type BarTitleSection,\n type BarTitleSectionItem,\n type BarTitlePlacement,\n type BarTitleSplitAction,\n type BarTitleVariant,\n} from './bar-title-types';\nimport {\n barTitleActionIdIssues,\n barTitleChoiceSections,\n findBarTitleAction,\n overflowBarTitleActionSections,\n resolveBarTitleActionItems,\n visibleBarTitleActions,\n} from './bar-title-actions';\n\nlet nextBarTitleId = 0;\n\ntype FocusableButton = HTMLElement & {\n setFocus?: (segment?: 'primary' | 'menu') => Promise<void>;\n};\n\n@Component({\n tag: 'ds-bar-title',\n styleUrl: 'BarTitle.css',\n scoped: true,\n})\nexport class BarTitle {\n @Element() el!: HTMLElement;\n\n /** The page's single visible h1. */\n @Prop() heading!: string;\n\n /** Optional supporting copy shown in the expanded presentation. */\n @Prop() description: string = '';\n\n /** Show the leading page-level Back action. */\n @Prop({ attribute: 'show-back' }) showBack: boolean = false;\n\n /** Accessible name for the leading Back action. */\n @Prop() backAriaLabel: string = 'Back';\n\n /** Visible parent-page label used by the expanded breadcrumb when breadcrumbs is empty. */\n @Prop() backLabel: string = 'Back';\n\n /** Optional expanded ancestor path. Compact variants continue to use the Back action. */\n @Prop() breadcrumbs: BreadcrumbItem[] = [];\n\n /** Accessible name for the expanded breadcrumb navigation landmark. */\n @Prop() breadcrumbAriaLabel: string = 'Breadcrumb';\n\n /** Optional page sections exposed through the active-section menu. */\n @Prop() sections: BarTitleSectionItem[] = [];\n\n /** Id of the active page section. */\n @Prop() value: string = '';\n\n /** Accessible name for the page-section menu. */\n @Prop() sectionsAriaLabel: string = 'Change page section';\n\n /** The one highest-emphasis page action. */\n @Prop() primaryAction: BarTitlePrimaryAction | null = null;\n\n /** Secondary page actions shown in the overflow menu. Dividers create groups. */\n @Prop() actions: BarTitleActionItem[] = [];\n\n /**\n * Ordered page-header actions. When supplied, this replaces the legacy\n * primaryAction/actions presentation while preserving the same dsAction event.\n */\n @Prop() actionItems?: BarTitleActionConfigItem[];\n\n /** Accessible name for the page-actions menu. */\n @Prop() actionsAriaLabel: string = 'More page actions';\n\n /** Explicit visual/capacity variant. ShellPage owns automatic selection. */\n @Prop() variant: BarTitleVariant = 'expanded';\n\n /** Page-header placement, or compact application-shell bar placement. */\n @Prop({ reflect: true }) placement: BarTitlePlacement = 'page';\n\n /** Draw the page-title divider beneath the header. */\n @Prop() showDivider: boolean = true;\n\n /** Override divider visibility for compact and constrained variants. */\n @Prop() showCompactDivider?: boolean;\n\n /** Emitted when the leading Back action is activated. */\n @Event() dsBack!: EventEmitter<MouseEvent>;\n\n /** Emitted when an authored expanded breadcrumb item is activated. */\n @Event({ cancelable: true }) dsBreadcrumbSelect!: EventEmitter<BreadcrumbSelectDetail>;\n\n /** Emitted with the newly selected page-section id. */\n @Event() dsSectionChange!: EventEmitter<string>;\n\n /** Emitted with the activated primary or overflow action id. */\n @Event() dsAction!: EventEmitter<string>;\n\n @State() private sectionMenuOpen = false;\n @State() private openActionMenuId = '';\n @State() private surfaceActionMenuId = '';\n @State() private sectionSurfaceOpen = false;\n @State() private sectionMenuInitialFocusVisible = false;\n @State() private actionMenuInitialFocusVisible = false;\n\n private readonly instanceId = nextBarTitleId++;\n private readonly sectionMenuTriggerId = `bar-title-section-trigger-${this.instanceId}`;\n private readonly sectionMenuId = `bar-title-section-menu-${this.instanceId}`;\n private readonly actionMenuTriggerId = `bar-title-action-trigger-${this.instanceId}`;\n private readonly actionMenuId = `bar-title-action-menu-${this.instanceId}`;\n private sectionTriggerEl: HTMLButtonElement | null = null;\n private actionTriggerEl: FocusableButton | null = null;\n private actionTriggerEls = new Map<string, FocusableButton>();\n\n componentWillLoad() {\n if (this.el.closest('ds-shell-page')) {\n this.el.setAttribute('data-shell-page-syncing', '');\n }\n this.reportActionIdIssues();\n }\n\n @Watch('sections')\n handleSectionsChange() {\n if (!this.hasSectionSelector) {\n this.closeSectionMenu();\n this.sectionSurfaceOpen = false;\n }\n }\n\n @Watch('actions')\n @Watch('primaryAction')\n @Watch('actionItems')\n handleActionsChange() {\n this.reportActionIdIssues();\n if (!this.availableActionMenuIds.has(this.openActionMenuId)) this.closeActionMenu();\n if (!this.availableActionMenuIds.has(this.surfaceActionMenuId)) this.surfaceActionMenuId = '';\n }\n\n private reportActionIdIssues() {\n for (const issue of barTitleActionIdIssues(this.resolvedActionItems)) {\n console.warn(`[ds-bar-title] ${issue}`);\n }\n }\n\n private get compact(): boolean {\n return this.effectiveVariant !== 'expanded';\n }\n\n private get effectiveVariant(): BarTitleVariant {\n return this.placement === 'shell-bar' ? 'compact' : this.variant;\n }\n\n private get dividerVisible(): boolean {\n return this.compact ? (this.showCompactDivider ?? this.showDivider) : this.showDivider;\n }\n\n private get expandedBreadcrumbItems(): BreadcrumbItem[] {\n if (this.breadcrumbs.length > 0) return this.breadcrumbs;\n if (!this.showBack) return [];\n return [\n {\n id: 'back',\n label: this.backLabel,\n ariaLabel: this.backAriaLabel,\n },\n ];\n }\n\n /** Tooltip copy is fixed and generic; backAriaLabel/actionsAriaLabel stay page-specific. */\n private get backTooltipLabel(): string {\n return 'Go back';\n }\n\n private get primaryCollapsed(): boolean {\n return (\n this.actionItems === undefined &&\n this.effectiveVariant === 'constrained' &&\n this.primaryAction !== null &&\n (this.primaryAction.collapse ?? 'auto') === 'auto'\n );\n }\n\n private get selectableSections(): BarTitleSection[] {\n return this.sections.filter((item): item is BarTitleSection => !isBarTitleDivider(item));\n }\n\n private get hasSectionSelector(): boolean {\n return this.selectableSections.length > 1;\n }\n\n private get effectiveValue(): string {\n return this.selectableSections.some(section => section.id === this.value)\n ? this.value\n : (this.selectableSections[0]?.id ?? '');\n }\n\n private get selectedSectionLabel(): string {\n return this.selectableSections.find(section => section.id === this.effectiveValue)?.label ?? '';\n }\n\n private get sectionTriggerAriaLabel(): string {\n return `${this.sectionsAriaLabel}. Current section: ${this.selectedSectionLabel}`;\n }\n\n private groupsFromSections(): MenuSection[] {\n const groups: MenuSection[] = [];\n let items: MenuItemData[] = [];\n const commit = () => {\n if (items.length > 0) groups.push({ items });\n items = [];\n };\n\n for (const item of this.sections) {\n if (isBarTitleDivider(item)) {\n commit();\n } else {\n items.push({\n label: item.label,\n value: item.id,\n isSelected: item.id === this.effectiveValue,\n isInactive: item.isInactive,\n });\n }\n }\n commit();\n return groups;\n }\n\n private get sectionMenuSections(): MenuSection[] {\n return this.groupsFromSections();\n }\n\n private get resolvedActionItems(): BarTitleActionConfigItem[] {\n return resolveBarTitleActionItems(this.actionItems, this.primaryAction, this.actions);\n }\n\n private get visibleActions(): BarTitleActionConfigItem[] {\n return visibleBarTitleActions(this.resolvedActionItems, this.effectiveVariant);\n }\n\n private get actionMenuSections(): MenuSection[] {\n return overflowBarTitleActionSections(this.resolvedActionItems, this.effectiveVariant);\n }\n\n private get showActionMenuTrigger(): boolean {\n return this.actionMenuSections.length > 0;\n }\n\n private get availableActionMenuIds(): Set<string> {\n const ids = new Set<string>();\n if (this.showActionMenuTrigger) ids.add('__overflow');\n for (const item of this.visibleActions) {\n if (!isBarTitleDivider(item) && (item.type === 'menu' || item.type === 'split')) {\n ids.add(item.id);\n }\n }\n return ids;\n }\n\n private actionMenuDomId(id: string): string {\n const index = this.resolvedActionItems.findIndex(\n item => !isBarTitleDivider(item) && item.id === id\n );\n return `bar-title-action-menu-${this.instanceId}-${index}`;\n }\n\n private actionMenuAnchor(id: string): HTMLElement | undefined {\n if (id === '__overflow') return this.actionTriggerEl ?? undefined;\n const trigger = this.actionTriggerEls.get(id);\n if (!trigger) return undefined;\n const splitMenu = trigger.querySelector<HTMLElement>('.ds-button-split__menu');\n return splitMenu ?? trigger;\n }\n\n private menuSectionsForAction(action: BarTitleMenuAction | BarTitleSplitAction): MenuSection[] {\n return barTitleChoiceSections(action.choices);\n }\n\n private toggleSectionMenu = (event: MouseEvent) => {\n this.sectionMenuInitialFocusVisible = event.detail === 0;\n this.openActionMenuId = '';\n this.sectionMenuOpen = !this.sectionMenuOpen;\n if (this.sectionMenuOpen) this.sectionSurfaceOpen = true;\n };\n\n private closeSectionMenu = () => {\n this.sectionMenuOpen = false;\n };\n\n private handleSectionSelect = (event: CustomEvent<MenuItemData>) => {\n const id = String(event.detail?.value ?? '');\n const selected = this.selectableSections.find(section => section.id === id);\n if (!selected || selected.isInactive) return;\n this.closeSectionMenu();\n this.dsSectionChange.emit(id);\n requestAnimationFrame(() => this.sectionTriggerEl?.focus());\n };\n\n private toggleActionMenu = (id: string, event: MouseEvent) => {\n this.actionMenuInitialFocusVisible = event.detail === 0;\n this.sectionMenuOpen = false;\n this.openActionMenuId = this.openActionMenuId === id ? '' : id;\n if (this.openActionMenuId) this.surfaceActionMenuId = this.openActionMenuId;\n };\n\n private finishActionMenuClose = (event: CustomEvent<void>) => {\n const id = this.surfaceActionMenuId;\n const domId = id === '__overflow' ? this.actionMenuId : this.actionMenuDomId(id);\n if (this.openActionMenuId !== id && (event.target as HTMLElement).id === domId) {\n this.surfaceActionMenuId = '';\n }\n };\n\n private closeActionMenu = () => {\n this.openActionMenuId = '';\n };\n\n private handleActionSelect = (event: CustomEvent<MenuItemData>) => {\n const id = String(event.detail?.value ?? '');\n const action = findBarTitleAction(this.resolvedActionItems, id);\n if (!action || action.isInactive || ('isLoading' in action && action.isLoading)) return;\n const menuId = this.openActionMenuId;\n this.closeActionMenu();\n this.dsAction.emit(id);\n requestAnimationFrame(() => {\n if (menuId === '__overflow') {\n void this.actionTriggerEl?.setFocus?.();\n return;\n }\n const trigger = this.actionTriggerEls.get(menuId);\n if ('setFocus' in (trigger ?? {})) {\n const split = this.resolvedActionItems.some(\n item => !isBarTitleDivider(item) && item.id === menuId && item.type === 'split'\n );\n void trigger?.setFocus?.(split ? 'menu' : undefined);\n }\n });\n };\n\n private renderSectionSelector() {\n if (!this.hasSectionSelector) return null;\n return [\n <div class=\"bar-title__divider\" aria-hidden=\"true\" />,\n <div class=\"bar-title__section-selector\">\n <button\n ref={el => {\n this.sectionTriggerEl = el ?? null;\n }}\n id={this.sectionMenuTriggerId}\n class={{\n 'bar-title__section-trigger': true,\n 'bar-title__section-trigger--expanded': this.sectionMenuOpen,\n 'ds-interaction-fill--surface-open': this.sectionSurfaceOpen,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n }}\n type=\"button\"\n aria-haspopup=\"menu\"\n aria-controls={this.sectionMenuId}\n aria-expanded={String(this.sectionMenuOpen)}\n aria-label={this.sectionTriggerAriaLabel}\n onClick={this.toggleSectionMenu}\n >\n <ds-text\n class=\"bar-title__section-label ds-interaction-fill__content\"\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis\n color=\"primary\"\n lineTruncation={1}\n >\n {this.selectedSectionLabel}\n </ds-text>\n <ds-icon\n class=\"bar-title__section-chevron ds-interaction-fill__content\"\n name=\"ChevronUpDown\"\n size=\"md\"\n color=\"inherit\"\n aria-hidden=\"true\"\n />\n </button>\n </div>,\n ];\n }\n\n private renderVisibleAction(action: BarTitleActionConfigItem) {\n if (isBarTitleDivider(action) || action.type === 'overflow') return null;\n const appearance = action.appearance ?? (action.type === 'split' ? 'filled' : 'unfilled');\n const variant: 'icon' | 'icon-label' | 'label' =\n action.type === 'icon' ? 'icon' : action.icon ? 'icon-label' : 'label';\n const menu = action.type === 'menu';\n const menuId = menu ? this.actionMenuDomId(action.id) : undefined;\n\n if (action.type === 'split') {\n const splitMenuId = this.actionMenuDomId(action.id);\n const splitProps = {\n key: action.id,\n ref: (el: FocusableButton | undefined) => {\n if (el) this.actionTriggerEls.set(action.id, el);\n else this.actionTriggerEls.delete(action.id);\n },\n class: 'bar-title__action bar-title__split-action',\n split: true,\n variant,\n label: action.label,\n icon: action.icon ?? '',\n menuAriaLabel: action.menuAriaLabel,\n controls: splitMenuId,\n expanded: this.openActionMenuId === action.id,\n surfaceOpen: this.surfaceActionMenuId === action.id,\n size: 'md' as const,\n type: action.buttonType ?? 'button',\n isInactive: action.isInactive,\n isLoading: action.isLoading,\n onDsClick: () => this.dsAction.emit(action.id),\n onDsMenuClick: (event: CustomEvent<MouseEvent>) =>\n this.toggleActionMenu(action.id, event.detail),\n };\n return appearance === 'filled' ? (\n <ds-button-filled\n {...splitProps}\n intent={action.intent ?? 'brand'}\n contrast={action.contrast ?? 'bold'}\n />\n ) : (\n <ds-button-unfilled {...splitProps} />\n );\n }\n\n const buttonProps = {\n key: action.id,\n ref: (el: FocusableButton | undefined) => {\n if (el) this.actionTriggerEls.set(action.id, el);\n else this.actionTriggerEls.delete(action.id);\n },\n class: `bar-title__action bar-title__action--${action.type}${\n this.actionItems === undefined && action.id === this.primaryAction?.id\n ? ' bar-title__primary-action'\n : ''\n }`,\n variant,\n icon: action.icon ?? '',\n label: action.label,\n ariaLabel: action.ariaLabel ?? (action.type === 'icon' ? action.label : undefined),\n size: 'md' as const,\n type: action.buttonType ?? 'button',\n isInactive: action.isInactive,\n isLoading: action.isLoading,\n controls: menuId,\n expanded: menu ? this.openActionMenuId === action.id : undefined,\n surfaceOpen: menu && this.surfaceActionMenuId === action.id,\n hasMenu: menu,\n onDsClick: (event: CustomEvent<MouseEvent>) => {\n if (menu) this.toggleActionMenu(action.id, event.detail);\n else this.dsAction.emit(action.id);\n },\n };\n\n const button =\n appearance === 'filled' ? (\n <ds-button-filled\n {...buttonProps}\n intent={action.intent ?? 'brand'}\n contrast={action.contrast ?? 'bold'}\n />\n ) : (\n <ds-button-unfilled {...buttonProps} />\n );\n\n return action.type === 'icon' ? (\n <ds-tooltip key={action.id} label={action.label} side=\"bottom\" size=\"sm\">\n {button}\n </ds-tooltip>\n ) : (\n button\n );\n }\n\n private renderActions() {\n if (this.visibleActions.length === 0 && !this.showActionMenuTrigger) return null;\n\n return (\n <div class=\"bar-title__actions\">\n {this.visibleActions.map(action => this.renderVisibleAction(action))}\n {this.showActionMenuTrigger ? (\n <ds-tooltip label=\"Page options\" side=\"bottom\" size=\"sm\">\n <ds-button-unfilled\n ref={el => {\n this.actionTriggerEl = el ?? null;\n }}\n id={this.actionMenuTriggerId}\n class=\"bar-title__more-actions\"\n variant=\"icon\"\n icon=\"Ellipses\"\n aria-label={this.actionsAriaLabel}\n size=\"md\"\n activeFill={!this.compact}\n hasBorder={this.visibleActions.length > 0}\n haspopup=\"menu\"\n controls={this.actionMenuId}\n expanded={this.openActionMenuId === '__overflow'}\n surfaceOpen={this.surfaceActionMenuId === '__overflow'}\n onDsClick={(event: CustomEvent<MouseEvent>) =>\n this.toggleActionMenu('__overflow', event.detail)\n }\n />\n </ds-tooltip>\n ) : null}\n </div>\n );\n }\n\n private renderActionMenus() {\n return this.visibleActions.flatMap(action => {\n if (isBarTitleDivider(action) || (action.type !== 'menu' && action.type !== 'split')) {\n return [];\n }\n const menuLabel = action.menuAriaLabel ?? action.ariaLabel ?? action.label;\n return [\n <ds-menu\n key={action.id}\n id={this.actionMenuDomId(action.id)}\n class=\"bar-title__action-menu\"\n anchor={this.actionMenuAnchor(action.id)}\n align=\"end\"\n menuLabel={menuLabel}\n open={this.openActionMenuId === action.id}\n initialFocusVisible={this.actionMenuInitialFocusVisible}\n sections={this.menuSectionsForAction(action)}\n onDsClose={this.closeActionMenu}\n onDsAfterClose={this.finishActionMenuClose}\n onDsSelect={this.handleActionSelect}\n />,\n ];\n });\n }\n\n private handleBreadcrumbSelect = (event: CustomEvent<BreadcrumbSelectDetail>) => {\n if (this.breadcrumbs.length === 0) {\n this.dsBack.emit(event.detail.originalEvent);\n return;\n }\n const forwarded = this.dsBreadcrumbSelect.emit(event.detail);\n if (forwarded.defaultPrevented) event.preventDefault();\n };\n\n private renderBreadcrumb() {\n const items = this.expandedBreadcrumbItems;\n if (items.length === 0) return null;\n return (\n <ds-breadcrumb\n class=\"bar-title__breadcrumb\"\n items={items}\n ariaLabel={this.breadcrumbAriaLabel}\n onDsSelect={this.handleBreadcrumbSelect}\n />\n );\n }\n\n private renderBack() {\n if (!this.showBack) return null;\n return (\n <ds-tooltip label={this.backTooltipLabel} side=\"bottom\" size=\"sm\">\n <ds-button-unfilled\n class=\"bar-title__back\"\n variant=\"icon\"\n icon=\"ChevronLeft\"\n aria-label={this.backAriaLabel}\n size=\"md\"\n activeFill={false}\n hasBorder={false}\n onDsClick={(event: CustomEvent<MouseEvent>) => this.dsBack.emit(event.detail)}\n />\n </ds-tooltip>\n );\n }\n\n render() {\n const compact = this.compact;\n return (\n <Host\n class={{\n 'bar-title-host--compact': compact,\n 'bar-title-host--expanded': this.effectiveVariant === 'expanded',\n 'bar-title-host--constrained': this.effectiveVariant === 'constrained',\n 'bar-title-host--has-description': !!this.description,\n 'bar-title-host--has-back': this.showBack,\n 'bar-title-host--has-breadcrumb': !compact && this.expandedBreadcrumbItems.length > 0,\n 'bar-title-host--shell-bar': this.placement === 'shell-bar',\n }}\n >\n <div\n class={{\n 'bar-title': true,\n 'bar-title--divider-hidden': !this.dividerVisible,\n 'ds-chrome-header': compact,\n }}\n >\n <div class=\"bar-title__inner\">\n {!compact ? this.renderBreadcrumb() : null}\n <div class=\"bar-title__row\">\n <div class=\"bar-title__leading\">\n <div class=\"bar-title__title-row\" data-shell-page-header-anchor>\n <div class=\"bar-title__identity\">\n {compact ? this.renderBack() : null}\n <ds-text\n class=\"bar-title__heading ds-control--md\"\n variant={compact ? 'text-title-small' : 'text-title-medium'}\n emphasis\n color=\"primary\"\n as=\"h1\"\n lineTruncation={1}\n >\n {this.heading}\n </ds-text>\n </div>\n {this.renderSectionSelector()}\n </div>\n {this.description && !compact ? (\n <ds-text\n class=\"bar-title__description\"\n variant=\"text-body-small\"\n color=\"secondary\"\n lineTruncation={2}\n wrap=\"wrap\"\n as=\"p\"\n >\n {this.description}\n </ds-text>\n ) : null}\n </div>\n {this.renderActions()}\n </div>\n </div>\n </div>\n\n {this.hasSectionSelector ? (\n <ds-menu\n id={this.sectionMenuId}\n class=\"bar-title__section-menu\"\n anchorId={this.sectionMenuTriggerId}\n menuLabel={this.sectionsAriaLabel}\n open={this.sectionMenuOpen}\n initialFocusVisible={this.sectionMenuInitialFocusVisible}\n sections={this.sectionMenuSections}\n onDsClose={this.closeSectionMenu}\n onDsAfterClose={() => {\n if (!this.sectionMenuOpen) this.sectionSurfaceOpen = false;\n }}\n onDsSelect={this.handleSectionSelect}\n />\n ) : null}\n\n {this.renderActionMenus()}\n\n {this.showActionMenuTrigger ? (\n <ds-menu\n id={this.actionMenuId}\n class=\"bar-title__action-menu\"\n anchorId={this.actionMenuTriggerId}\n align=\"end\"\n menuLabel={this.actionsAriaLabel}\n open={this.openActionMenuId === '__overflow'}\n initialFocusVisible={this.actionMenuInitialFocusVisible}\n sections={this.actionMenuSections}\n onDsClose={this.closeActionMenu}\n onDsAfterClose={this.finishActionMenuClose}\n onDsSelect={this.handleActionSelect}\n />\n ) : null}\n </Host>\n );\n }\n}\n",
561
566
  "type": "registry:ui"
562
567
  },
563
568
  {