@ds-mo/ui 12.5.0 → 12.6.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 (284) hide show
  1. package/dist/.build-stamp +1 -1
  2. package/dist/agent-patterns.json +21 -12
  3. package/dist/agent.json +1536 -1424
  4. package/dist/angular/ds-bar-workflow.d.ts +1 -1
  5. package/dist/angular/ds-bar-workflow.js +3 -3
  6. package/dist/angular/ds-mobile-bar-nav.d.ts +18 -0
  7. package/dist/angular/{ds-shell-mobile-bar.js → ds-mobile-bar-nav.js} +15 -15
  8. package/dist/angular/ds-mobile-header.d.ts +23 -0
  9. package/dist/angular/ds-mobile-header.js +45 -0
  10. package/dist/angular/ds-mobile-section-switcher.d.ts +18 -0
  11. package/dist/angular/{ds-shell-mobile-section-nav.js → ds-mobile-section-switcher.js} +15 -15
  12. package/dist/angular/ds-mobile-sheet-nav.d.ts +20 -0
  13. package/dist/angular/{ds-shell-mobile-nav.js → ds-mobile-sheet-nav.js} +13 -16
  14. package/dist/angular/ds-shell-app.d.ts +1 -1
  15. package/dist/angular/ds-shell-app.js +3 -3
  16. package/dist/angular/ds-shell-page.d.ts +1 -1
  17. package/dist/angular/ds-shell-page.js +3 -3
  18. package/dist/angular/index.d.ts +1 -1
  19. package/dist/angular/index.js +4 -3
  20. package/dist/angular/proxies.d.ts +4 -3
  21. package/dist/angular/proxies.js +4 -3
  22. package/dist/components/ds-agent-activity.js +1 -1
  23. package/dist/components/ds-agent-response.js +1 -1
  24. package/dist/components/ds-agent-tool-call.js +1 -1
  25. package/dist/components/ds-avatar.js +1 -1
  26. package/dist/components/ds-avatar.js.map +1 -1
  27. package/dist/components/ds-badge.js +1 -1
  28. package/dist/components/ds-bar-nav.js +1 -1
  29. package/dist/components/ds-bar-nav.js.map +1 -1
  30. package/dist/components/ds-bar-title.js +1 -1
  31. package/dist/components/ds-bar-title.js.map +1 -1
  32. package/dist/components/ds-bar-workflow.js +1 -1
  33. package/dist/components/ds-bar-workflow.js.map +1 -1
  34. package/dist/components/ds-breadcrumb.js +1 -1
  35. package/dist/components/ds-button-filled.js +1 -1
  36. package/dist/components/ds-button-unfilled.js +1 -1
  37. package/dist/components/ds-card-data-viz-bar.js +1 -1
  38. package/dist/components/ds-card-data-viz-donut.js +1 -1
  39. package/dist/components/ds-card-data-viz-line.js +1 -1
  40. package/dist/components/ds-card-setting.js +1 -1
  41. package/dist/components/ds-chart-bar.js +1 -1
  42. package/dist/components/ds-chart-donut.js +1 -1
  43. package/dist/components/ds-chart-legend.js +1 -1
  44. package/dist/components/ds-chart-legend.js.map +1 -1
  45. package/dist/components/ds-chart-line.js +1 -1
  46. package/dist/components/ds-checkbox.js +1 -1
  47. package/dist/components/ds-chip.js +1 -1
  48. package/dist/components/ds-chip.js.map +1 -1
  49. package/dist/components/ds-code-block.js +1 -1
  50. package/dist/components/ds-conversation-list-item.js +1 -1
  51. package/dist/components/ds-conversation-list-item.js.map +1 -1
  52. package/dist/components/ds-conversation-list-section.js +1 -1
  53. package/dist/components/ds-empty-state.js +1 -1
  54. package/dist/components/ds-field.js +1 -1
  55. package/dist/components/ds-input.js +1 -1
  56. package/dist/components/ds-input.js.map +1 -1
  57. package/dist/components/ds-loader.js +1 -1
  58. package/dist/components/ds-markdown.js +1 -1
  59. package/dist/components/ds-menu.js +1 -1
  60. package/dist/components/ds-message-composer.js +1 -1
  61. package/dist/components/ds-message-composer.js.map +1 -1
  62. package/dist/components/ds-message-scroller.js +1 -1
  63. package/dist/components/ds-message.js +1 -1
  64. package/dist/components/{ds-shell-mobile-nav.d.ts → ds-mobile-bar-nav.d.ts} +4 -4
  65. package/dist/components/ds-mobile-bar-nav.js +2 -0
  66. package/dist/components/ds-mobile-bar-nav.js.map +1 -0
  67. package/dist/components/ds-mobile-header.d.ts +11 -0
  68. package/dist/components/ds-mobile-header.js +2 -0
  69. package/dist/components/ds-mobile-header.js.map +1 -0
  70. package/dist/components/ds-mobile-section-switcher.d.ts +11 -0
  71. package/dist/components/ds-mobile-section-switcher.js +2 -0
  72. package/dist/components/ds-mobile-section-switcher.js.map +1 -0
  73. package/dist/components/{ds-shell-mobile-bar.d.ts → ds-mobile-sheet-nav.d.ts} +4 -4
  74. package/dist/components/ds-mobile-sheet-nav.js +2 -0
  75. package/dist/components/ds-mobile-sheet-nav.js.map +1 -0
  76. package/dist/components/ds-modal.js +1 -1
  77. package/dist/components/ds-modal.js.map +1 -1
  78. package/dist/components/ds-panel-nav.js +1 -1
  79. package/dist/components/ds-panel-nav.js.map +1 -1
  80. package/dist/components/ds-panel-sub-nav.js +1 -1
  81. package/dist/components/ds-panel-sub-nav.js.map +1 -1
  82. package/dist/components/ds-panel-tool-header.js +1 -1
  83. package/dist/components/ds-panel-tool-search.js +1 -1
  84. package/dist/components/ds-panel-tool-search.js.map +1 -1
  85. package/dist/components/ds-panel-tools.js +1 -1
  86. package/dist/components/ds-radio.js +1 -1
  87. package/dist/components/ds-radio.js.map +1 -1
  88. package/dist/components/ds-select-multi.js +1 -1
  89. package/dist/components/ds-select-multi.js.map +1 -1
  90. package/dist/components/ds-select.js +1 -1
  91. package/dist/components/ds-select.js.map +1 -1
  92. package/dist/components/ds-shell-app.js +1 -1
  93. package/dist/components/ds-shell-app.js.map +1 -1
  94. package/dist/components/ds-shell-gradient-picker.js +1 -1
  95. package/dist/components/ds-shell-gradient-picker.js.map +1 -1
  96. package/dist/components/ds-shell-gradient-swatch.js +1 -1
  97. package/dist/components/ds-shell-page.js +1 -1
  98. package/dist/components/ds-shell-page.js.map +1 -1
  99. package/dist/components/ds-shell-tools.js +1 -1
  100. package/dist/components/ds-shell-tools.js.map +1 -1
  101. package/dist/components/ds-skeleton.js +1 -1
  102. package/dist/components/ds-skeleton.js.map +1 -1
  103. package/dist/components/ds-swatch-picker.js +1 -1
  104. package/dist/components/ds-switch.js +1 -1
  105. package/dist/components/ds-tab-group.js +1 -1
  106. package/dist/components/ds-tag.js +1 -1
  107. package/dist/components/ds-tag.js.map +1 -1
  108. package/dist/components/ds-toast.js +1 -1
  109. package/dist/components/ds-toast.js.map +1 -1
  110. package/dist/components/ds-tooltip-data-viz.js +1 -1
  111. package/dist/components/ds-tooltip.js +1 -1
  112. package/dist/components/ds-typing-indicator.js +1 -1
  113. package/dist/components/p--bSkr_yn.js +2 -0
  114. package/dist/components/p--bSkr_yn.js.map +1 -0
  115. package/dist/components/p-46nk9-RO.js +2 -0
  116. package/dist/components/p-46nk9-RO.js.map +1 -0
  117. package/dist/components/{p-9dQQSgwT.js → p-BHYrsGBw.js} +2 -2
  118. package/dist/components/p-BY0PzkUp.js +2 -0
  119. package/dist/components/p-BgGP65Nn.js +2 -0
  120. package/dist/components/p-BgGP65Nn.js.map +1 -0
  121. package/dist/components/p-BkAMkjIR.js +2 -0
  122. package/dist/components/{p-Co8hD3P-.js.map → p-BkAMkjIR.js.map} +1 -1
  123. package/dist/components/p-BrusnEYP.js.map +1 -1
  124. package/dist/components/p-C2ym_Ich.js +2 -0
  125. package/dist/components/{p-Biz6xo-z.js → p-CFpWrYAH.js} +2 -2
  126. package/dist/components/p-CICGtu7H.js +2 -0
  127. package/dist/components/{p-CtD24EyI.js.map → p-CICGtu7H.js.map} +1 -1
  128. package/dist/components/p-CLzlL3G4.js +2 -0
  129. package/dist/components/p-CLzlL3G4.js.map +1 -0
  130. package/dist/components/p-CXcjwduV.js +2 -0
  131. package/dist/components/{p-BhxwpOH8.js.map → p-CXcjwduV.js.map} +1 -1
  132. package/dist/components/p-Ca-I2UnB.js +2 -0
  133. package/dist/components/p-Ca-I2UnB.js.map +1 -0
  134. package/dist/components/p-CaRyxTMz.js +2 -0
  135. package/dist/components/p-CaRyxTMz.js.map +1 -0
  136. package/dist/components/{p-g40DTeV8.js → p-CvKKmZJj.js} +2 -2
  137. package/dist/components/p-D4o2D1hk.js +2 -0
  138. package/dist/components/p-D4o2D1hk.js.map +1 -0
  139. package/dist/components/{p-DXnFhmZF.js → p-D9iN80yX.js} +2 -2
  140. package/dist/components/p-DD1Lgs7O.js +2 -0
  141. package/dist/components/p-DD1Lgs7O.js.map +1 -0
  142. package/dist/components/{p-DjPZxzIG.js → p-DQDpDYyF.js} +2 -2
  143. package/dist/components/p-DTea1Du7.js +2 -0
  144. package/dist/components/p-DTea1Du7.js.map +1 -0
  145. package/dist/components/p-DWdRSOBV.js +2 -0
  146. package/dist/components/{p-BPS9VExm.js.map → p-DWdRSOBV.js.map} +1 -1
  147. package/dist/components/p-DcFfilOX.js +2 -0
  148. package/dist/components/p-DcFfilOX.js.map +1 -0
  149. package/dist/components/p-Dg3xLna0.js +2 -0
  150. package/dist/components/{p-C_0Q8Cnp.js.map → p-Dg3xLna0.js.map} +1 -1
  151. package/dist/components/p-DvVW0hxm.js +2 -0
  152. package/dist/components/p-DvVW0hxm.js.map +1 -0
  153. package/dist/components/p-YtgcuJYx.js +2 -0
  154. package/dist/components/{p-PZ6R_nGI.js.map → p-YtgcuJYx.js.map} +1 -1
  155. package/dist/components/p-loq_--ce.js +2 -0
  156. package/dist/components/{p-NUNsefU1.js.map → p-loq_--ce.js.map} +1 -1
  157. package/dist/docs/components.json +3586 -3064
  158. package/dist/lib/shell/index.d.ts +1 -1
  159. package/dist/lib/shell/index.js +2 -1
  160. package/dist/lib/shell/index.js.map +2 -2
  161. package/dist/lib/shell/shell-responsive.d.ts +3 -3
  162. package/dist/lib/utils/index.d.ts +1 -0
  163. package/dist/lib/utils/index.js +8 -0
  164. package/dist/lib/utils/index.js.map +3 -3
  165. package/dist/lib/utils/required-validity.d.ts +14 -0
  166. package/dist/mcp-data/registry/agent-activity.json +2 -2
  167. package/dist/mcp-data/registry/badge.json +2 -2
  168. package/dist/mcp-data/registry/bar-nav.json +2 -2
  169. package/dist/mcp-data/registry/bar-title.json +2 -2
  170. package/dist/mcp-data/registry/bar-workflow.json +25 -4
  171. package/dist/mcp-data/registry/breadcrumb.json +2 -2
  172. package/dist/mcp-data/registry/button-unfilled.json +3 -3
  173. package/dist/mcp-data/registry/chart-legend.json +1 -1
  174. package/dist/mcp-data/registry/checkbox.json +1 -1
  175. package/dist/mcp-data/registry/conversation-list-item.json +2 -2
  176. package/dist/mcp-data/registry/input.json +1 -1
  177. package/dist/mcp-data/registry/loader.json +2 -2
  178. package/dist/mcp-data/registry/mobile-bar-nav.json +333 -0
  179. package/dist/mcp-data/registry/mobile-header.json +351 -0
  180. package/dist/mcp-data/registry/mobile-section-switcher.json +210 -0
  181. package/dist/mcp-data/registry/mobile-sheet-nav.json +317 -0
  182. package/dist/mcp-data/registry/panel-nav.json +2 -2
  183. package/dist/mcp-data/registry/panel-tools.json +2 -2
  184. package/dist/mcp-data/registry/radio.json +1 -1
  185. package/dist/mcp-data/registry/registry.json +1544 -1429
  186. package/dist/mcp-data/registry/select-multi.json +1 -1
  187. package/dist/mcp-data/registry/select.json +2 -2
  188. package/dist/mcp-data/registry/shell-app.json +17 -17
  189. package/dist/mcp-data/registry/shell-page.json +26 -7
  190. package/dist/mcp-data/registry/shell-tools.json +10 -7
  191. package/dist/mcp-data/registry/switch.json +1 -1
  192. package/dist/mcp-data/registry/toast.json +2 -2
  193. package/dist/react/components.d.ts +4 -3
  194. package/dist/react/components.js +4 -3
  195. package/dist/react/ds-mobile-bar-nav.d.ts +13 -0
  196. package/dist/react/ds-mobile-bar-nav.js +15 -0
  197. package/dist/react/ds-mobile-header.d.ts +13 -0
  198. package/dist/react/ds-mobile-header.js +15 -0
  199. package/dist/react/ds-mobile-section-switcher.d.ts +12 -0
  200. package/dist/react/ds-mobile-section-switcher.js +12 -0
  201. package/dist/react/ds-mobile-sheet-nav.d.ts +14 -0
  202. package/dist/react/ds-mobile-sheet-nav.js +16 -0
  203. package/dist/types/components/BarWorkflow/BarWorkflow.d.ts +6 -0
  204. package/dist/types/components/ButtonUnfilled/ButtonUnfilled.d.ts +1 -1
  205. package/dist/types/components/MobileBarNav/MobileBarNav.d.ts +26 -0
  206. package/dist/types/components/MobileBarNav/index.d.ts +4 -0
  207. package/dist/types/components/MobileBarNav/mobile-bar-nav-types.d.ts +5 -0
  208. package/dist/types/components/MobileHeader/MobileHeader.d.ts +39 -0
  209. package/dist/types/components/MobileHeader/index.d.ts +2 -0
  210. package/dist/types/components/MobileHeader/mobile-header-types.d.ts +5 -0
  211. package/dist/types/components/MobileSectionSwitcher/MobileSectionSwitcher.d.ts +35 -0
  212. package/dist/types/components/MobileSectionSwitcher/index.d.ts +3 -0
  213. package/dist/types/components/MobileSectionSwitcher/mobile-section-switcher-types.d.ts +4 -0
  214. package/dist/types/components/MobileSectionSwitcher/mobile-section-switcher-utils.d.ts +10 -0
  215. package/dist/types/components/{ShellMobileNav/ShellMobileNav.d.ts → MobileSheetNav/MobileSheetNav.d.ts} +5 -7
  216. package/dist/types/components/MobileSheetNav/index.d.ts +1 -0
  217. package/dist/types/components/ShellApp/ShellApp.d.ts +4 -4
  218. package/dist/types/components/ShellPage/ShellPage.d.ts +4 -0
  219. package/dist/types/components.d.ts +530 -381
  220. package/dist/types/shell/index.d.ts +1 -1
  221. package/dist/types/shell/shell-responsive.d.ts +3 -3
  222. package/dist/types/utils/index.d.ts +1 -0
  223. package/dist/types/utils/required-validity.d.ts +15 -0
  224. package/package.json +1 -1
  225. package/dist/angular/ds-shell-mobile-bar.d.ts +0 -18
  226. package/dist/angular/ds-shell-mobile-nav.d.ts +0 -23
  227. package/dist/angular/ds-shell-mobile-section-nav.d.ts +0 -18
  228. package/dist/components/ds-shell-mobile-bar.js +0 -2
  229. package/dist/components/ds-shell-mobile-bar.js.map +0 -1
  230. package/dist/components/ds-shell-mobile-nav.js +0 -2
  231. package/dist/components/ds-shell-mobile-nav.js.map +0 -1
  232. package/dist/components/ds-shell-mobile-section-nav.d.ts +0 -11
  233. package/dist/components/ds-shell-mobile-section-nav.js +0 -2
  234. package/dist/components/ds-shell-mobile-section-nav.js.map +0 -1
  235. package/dist/components/p-B6Vuayt6.js +0 -2
  236. package/dist/components/p-B6Vuayt6.js.map +0 -1
  237. package/dist/components/p-BPS9VExm.js +0 -2
  238. package/dist/components/p-BVV_rlIP.js +0 -2
  239. package/dist/components/p-BVV_rlIP.js.map +0 -1
  240. package/dist/components/p-BhxwpOH8.js +0 -2
  241. package/dist/components/p-BtEjlcB9.js +0 -2
  242. package/dist/components/p-BtEjlcB9.js.map +0 -1
  243. package/dist/components/p-CH6cNXXF.js +0 -2
  244. package/dist/components/p-CH6cNXXF.js.map +0 -1
  245. package/dist/components/p-C_0Q8Cnp.js +0 -2
  246. package/dist/components/p-Ce52yVXF.js +0 -2
  247. package/dist/components/p-Ce52yVXF.js.map +0 -1
  248. package/dist/components/p-CgHTNfC1.js +0 -2
  249. package/dist/components/p-CgHTNfC1.js.map +0 -1
  250. package/dist/components/p-Co8hD3P-.js +0 -2
  251. package/dist/components/p-CoYFJD-w.js +0 -2
  252. package/dist/components/p-CoYFJD-w.js.map +0 -1
  253. package/dist/components/p-Cpr3K41-.js +0 -2
  254. package/dist/components/p-Cpr3K41-.js.map +0 -1
  255. package/dist/components/p-CtD24EyI.js +0 -2
  256. package/dist/components/p-DYqReipj.js +0 -2
  257. package/dist/components/p-DYqReipj.js.map +0 -1
  258. package/dist/components/p-NUNsefU1.js +0 -2
  259. package/dist/components/p-PZ6R_nGI.js +0 -2
  260. package/dist/components/p-jCWtxthA.js +0 -2
  261. package/dist/components/p-yhj1lIPx.js +0 -2
  262. package/dist/mcp-data/registry/shell-mobile-bar.json +0 -314
  263. package/dist/mcp-data/registry/shell-mobile-nav.json +0 -348
  264. package/dist/mcp-data/registry/shell-mobile-section-nav.json +0 -267
  265. package/dist/react/ds-shell-mobile-bar.d.ts +0 -13
  266. package/dist/react/ds-shell-mobile-bar.js +0 -15
  267. package/dist/react/ds-shell-mobile-nav.d.ts +0 -15
  268. package/dist/react/ds-shell-mobile-nav.js +0 -17
  269. package/dist/react/ds-shell-mobile-section-nav.d.ts +0 -12
  270. package/dist/react/ds-shell-mobile-section-nav.js +0 -12
  271. package/dist/types/components/ShellMobileBar/ShellMobileBar.d.ts +0 -25
  272. package/dist/types/components/ShellMobileBar/index.d.ts +0 -4
  273. package/dist/types/components/ShellMobileBar/shell-mobile-bar-types.d.ts +0 -5
  274. package/dist/types/components/ShellMobileNav/index.d.ts +0 -2
  275. package/dist/types/components/ShellMobileNav/shell-mobile-nav-types.d.ts +0 -6
  276. package/dist/types/components/ShellMobileSectionNav/ShellMobileSectionNav.d.ts +0 -30
  277. package/dist/types/components/ShellMobileSectionNav/index.d.ts +0 -2
  278. /package/dist/components/{p-9dQQSgwT.js.map → p-BHYrsGBw.js.map} +0 -0
  279. /package/dist/components/{p-jCWtxthA.js.map → p-BY0PzkUp.js.map} +0 -0
  280. /package/dist/components/{p-yhj1lIPx.js.map → p-C2ym_Ich.js.map} +0 -0
  281. /package/dist/components/{p-Biz6xo-z.js.map → p-CFpWrYAH.js.map} +0 -0
  282. /package/dist/components/{p-g40DTeV8.js.map → p-CvKKmZJj.js.map} +0 -0
  283. /package/dist/components/{p-DXnFhmZF.js.map → p-D9iN80yX.js.map} +0 -0
  284. /package/dist/components/{p-DjPZxzIG.js.map → p-DQDpDYyF.js.map} +0 -0
@@ -48,7 +48,7 @@
48
48
  ],
49
49
  "accessibility": [
50
50
  "ShellPage exposes the routed page as the main landmark; render exactly one active ShellPage inside ShellApp.",
51
- "Provide exactly one BarTitle or BarWorkflow through the header slot so the page has one h1.",
51
+ "Provide one BarTitle or BarWorkflow through the desktop header slot and one MobileHeader through mobile-header when the page participates in the mobile shell.",
52
52
  "Keep loading, empty, validation, error, progress, success, and failure communication in the default product-content slot.",
53
53
  "Responsive variant changes preserve the same heading, controls, accessible names, and keyboard focus targets."
54
54
  ],
@@ -57,12 +57,13 @@
57
57
  "headerPresentation=auto selects the matching header variant from headerCapacity and scroll position.",
58
58
  "expanded, compact, and constrained are explicit overrides for exceptional layouts and deterministic examples.",
59
59
  "contentInset=default supplies standard page gutters; none permits maps, canvases, and other full-bleed content.",
60
- "The required header slot accepts one BarTitle or BarWorkflow. The default slot owns router output and all page product content."
60
+ "The header slot accepts one desktop BarTitle or BarWorkflow; mobile-header accepts the corresponding MobileHeader presentation. The default slot owns router output and all page product content."
61
61
  ],
62
62
  "responsiveBehavior": [
63
63
  "The owning application shell resolves viewport and persistent-chrome state into the explicit headerCapacity prop; ShellPage never guesses the cause of reduced width from its own rendered size.",
64
64
  "Roomy capacity expands at the top and compacts after scroll; compact capacity stays compact; constrained capacity uses the constrained action layout.",
65
65
  "A ShellPage-managed BarTitle remains concealed until headerCapacity is available and its rendered variant matches the resolved presentation, preventing a default-variant flash during route activation.",
66
+ "responsiveMode=mobile hides and inerts the desktop header, exposes mobile-header, and disables desktop scroll-compaction geometry.",
66
67
  "Update headerCapacity synchronously with shell layout state so opening tools or crossing application breakpoints cannot briefly render the wrong presentation.",
67
68
  "On roomy pages, the expanded header scrolls upward until its title and action row reaches the compact row position, then snaps variants without moving that shared visual anchor.",
68
69
  "Scroll compaction preserves the expanded page flow offset so content never jumps beneath the sticky header.",
@@ -71,15 +72,15 @@
71
72
  ],
72
73
  "frameworkCaveats": {
73
74
  "customElements": [
74
- "Import ShellPage with BarTitle or BarWorkflow, place one header in slot=header, and render route content as default children.",
75
+ "Import ShellPage with BarTitle or BarWorkflow in slot=header and MobileHeader in slot=mobile-header, then render route content as default children.",
75
76
  "Do not set BarTitle.variant while ShellPage owns headerPresentation."
76
77
  ],
77
78
  "react": [
78
- "Render DsBarTitle or DsBarWorkflow with slot=header inside DsShellPage and keep routed content as the remaining children.",
79
+ "Render the desktop header with slot=header and DsMobileHeader with slot=mobile-header inside DsShellPage.",
79
80
  "Use contentInset none only for deliberately full-bleed page products."
80
81
  ],
81
82
  "angular": [
82
- "Import the generated standalone DsShellPage adapter with DsBarTitle or DsBarWorkflow, preserve slot=header, and place router-outlet in the ShellPage default slot.",
83
+ "Import the generated standalone DsShellPage adapter with the desktop header and DsMobileHeader, preserve both header slots, and place router-outlet in the default slot.",
83
84
  "Do not add another overflow auto wrapper around the routed page."
84
85
  ]
85
86
  },
@@ -125,6 +126,15 @@
125
126
  "required": false,
126
127
  "mutable": false,
127
128
  "description": "Automatic capacity/scroll behavior, or an explicit header variant override."
129
+ },
130
+ "responsiveMode": {
131
+ "type": "ShellResponsiveMode",
132
+ "resolvedType": "\"desktop\" | \"mobile\" | \"tablet\"",
133
+ "attribute": "responsive-mode",
134
+ "default": "'desktop'",
135
+ "required": false,
136
+ "mutable": false,
137
+ "description": "Explicit shell breakpoint presentation. Mobile uses the dedicated mobile-header slot."
128
138
  }
129
139
  },
130
140
  "events": [],
@@ -157,6 +167,15 @@
157
167
  "required": false,
158
168
  "mutable": false,
159
169
  "description": "Automatic capacity/scroll behavior, or an explicit header variant override."
170
+ },
171
+ "responsiveMode": {
172
+ "type": "ShellResponsiveMode",
173
+ "resolvedType": "\"desktop\" | \"mobile\" | \"tablet\"",
174
+ "attribute": "responsive-mode",
175
+ "default": "'desktop'",
176
+ "required": false,
177
+ "mutable": false,
178
+ "description": "Explicit shell breakpoint presentation. Mobile uses the dedicated mobile-header slot."
160
179
  }
161
180
  },
162
181
  "events": [],
@@ -199,12 +218,12 @@
199
218
  "files": [
200
219
  {
201
220
  "path": "src/wc/components/ShellPage/ShellPage.css",
202
- "content": ":host {\n display: block;\n position: relative;\n min-width: 0;\n min-height: 100%;\n box-sizing: border-box;\n background: var(--color-background-primary);\n}\n\n.shell-page__scroll-sentinel {\n position: absolute;\n inset-block-start: var(--ds-shell-page-header-travel, 0);\n inset-inline-start: 0;\n width: var(--dimension-stroke-width-012);\n height: var(--dimension-stroke-width-012);\n pointer-events: none;\n}\n\n.shell-page__sticky-header {\n position: sticky;\n inset-block-start: var(--ds-shell-page-sticky-offset, 0);\n z-index: var(--dimension-z-index-raised);\n min-width: 0;\n background: var(--color-background-primary);\n overflow: visible;\n overflow-anchor: none;\n}\n\n.shell-page__sticky-header ::slotted(ds-bar-title),\n.shell-page__sticky-header ::slotted(ds-bar-workflow) {\n display: block;\n min-width: 0;\n}\n\n.shell-page__flow-spacer {\n height: var(--ds-shell-page-flow-spacer, 0);\n background: var(--color-background-primary);\n}\n\n.shell-page__content {\n min-width: 0;\n box-sizing: border-box;\n background: var(--color-background-primary);\n}\n\n:host(.shell-page-host--inset-default) .shell-page__content {\n padding: var(--dimension-space-400) var(--dimension-space-400) var(--dimension-space-800);\n}\n\n:host(.shell-page-host--inset-none) .shell-page__content {\n padding: 0;\n}\n",
221
+ "content": ":host {\n display: block;\n position: relative;\n min-width: 0;\n min-height: 100%;\n box-sizing: border-box;\n background: var(--color-background-primary);\n}\n\n.shell-page__scroll-sentinel {\n position: absolute;\n inset-block-start: var(--ds-shell-page-header-travel, 0);\n inset-inline-start: 0;\n width: var(--dimension-stroke-width-012);\n height: var(--dimension-stroke-width-012);\n pointer-events: none;\n}\n\n.shell-page__sticky-header {\n position: sticky;\n inset-block-start: var(--ds-shell-page-sticky-offset, 0);\n z-index: var(--dimension-z-index-raised);\n min-width: 0;\n background: var(--color-background-primary);\n overflow: visible;\n overflow-anchor: none;\n}\n\n.shell-page__desktop-header ::slotted(ds-bar-title),\n.shell-page__desktop-header ::slotted(ds-bar-workflow),\n.shell-page__mobile-header ::slotted(ds-mobile-header) {\n display: block;\n min-width: 0;\n}\n\n.shell-page__desktop-header[hidden],\n.shell-page__mobile-header[hidden] {\n display: none;\n}\n\n:host(.shell-page-host--mobile) .shell-page__scroll-sentinel,\n:host(.shell-page-host--mobile) .shell-page__flow-spacer {\n display: none;\n}\n\n.shell-page__flow-spacer {\n height: var(--ds-shell-page-flow-spacer, 0);\n background: var(--color-background-primary);\n}\n\n.shell-page__content {\n min-width: 0;\n box-sizing: border-box;\n background: var(--color-background-primary);\n}\n\n:host(.shell-page-host--inset-default) .shell-page__content {\n padding: var(--dimension-space-400) var(--dimension-space-400) var(--dimension-space-800);\n}\n\n:host(.shell-page-host--inset-none) .shell-page__content {\n padding: 0;\n}\n",
203
222
  "type": "registry:ui"
204
223
  },
205
224
  {
206
225
  "path": "src/wc/components/ShellPage/ShellPage.tsx",
207
- "content": "import { Component, Element, h, Host, Prop, State, Watch } from '@stencil/core';\nimport { resolveCssLengthPx } from '../../utils/resolve-css-length-px';\nimport type { BarTitleVariant } from '../BarTitle/bar-title-types';\nimport { resolveShellPageHeaderVariant } from './shell-page-responsive';\nimport type {\n ShellPageCapacity,\n ShellPageContentInset,\n ShellPageHeaderPresentation,\n} from './shell-page-types';\n\ntype BarTitleElement = HTMLElement & { variant: BarTitleVariant };\n\n@Component({\n tag: 'ds-shell-page',\n styleUrl: 'ShellPage.css',\n scoped: true,\n})\nexport class ShellPage {\n private static readonly MAX_HEADER_REVEAL_FRAMES = 12;\n private static readonly MAX_HEADER_GEOMETRY_FRAMES = 12;\n\n @Element() el!: HTMLElement;\n\n /** Automatic capacity/scroll behavior, or an explicit header variant override. */\n @Prop() headerPresentation: ShellPageHeaderPresentation = 'auto';\n\n /** Available page-header capacity supplied by the owning application shell. */\n @Prop() headerCapacity?: ShellPageCapacity;\n\n /** Standard page gutters, or no inset for full-bleed page content. */\n @Prop() contentInset: ShellPageContentInset = 'default';\n\n @State() private pageTopVisible = true;\n\n private sentinelEl: HTMLElement | null = null;\n private spacerEl: HTMLElement | null = null;\n private stickyHeaderEl: HTMLElement | null = null;\n private headerEl: BarTitleElement | null = null;\n private expandedHeaderHeight = 0;\n private headerTravel = 0;\n private expandedGeometryFrozen = false;\n private headerResizeObserver: ResizeObserver | null = null;\n private headerMutationObserver: MutationObserver | null = null;\n private headerRevealFrame: number | null = null;\n private headerGeometryFrame: number | null = null;\n private scrollRoot: HTMLElement | Window | null = null;\n\n @Watch('headerCapacity')\n @Watch('headerPresentation')\n handleHeaderContractChange() {\n this.syncHeaderVariant(true);\n }\n\n componentDidLoad() {\n this.observeHeader(this.findHeader());\n this.connectScrollRoot();\n }\n\n componentDidRender() {\n this.syncHeaderVariant();\n }\n\n disconnectedCallback() {\n this.headerResizeObserver?.disconnect();\n this.headerResizeObserver = null;\n this.headerMutationObserver?.disconnect();\n this.headerMutationObserver = null;\n this.cancelHeaderReveal();\n this.cancelHeaderGeometrySync();\n this.scrollRoot?.removeEventListener('scroll', this.handleScroll);\n this.scrollRoot = null;\n }\n\n private get effectiveVariant(): BarTitleVariant {\n return resolveShellPageHeaderVariant(\n this.headerPresentation,\n this.headerCapacity ?? 'roomy',\n this.pageTopVisible\n );\n }\n\n private get headerContractResolved(): boolean {\n return this.headerPresentation !== 'auto' || this.headerCapacity !== undefined;\n }\n\n private get isScrollCompacted(): boolean {\n return (\n this.headerPresentation === 'auto' && this.headerCapacity === 'roomy' && !this.pageTopVisible\n );\n }\n\n private composedParent(element: Element): Element | null {\n return (element as HTMLElement).assignedSlot?.parentElement ?? element.parentElement;\n }\n\n private findScrollRoot(): HTMLElement | Window {\n let ancestor = this.composedParent(this.el);\n while (ancestor && ancestor !== document.documentElement) {\n const overflow = getComputedStyle(ancestor).overflowY;\n if (overflow === 'auto' || overflow === 'scroll' || overflow === 'overlay') {\n return ancestor as HTMLElement;\n }\n ancestor = this.composedParent(ancestor);\n }\n return window;\n }\n\n private connectScrollRoot() {\n const next = this.findScrollRoot();\n if (next === this.scrollRoot) return;\n this.scrollRoot?.removeEventListener('scroll', this.handleScroll);\n this.scrollRoot = next;\n this.scrollRoot.addEventListener('scroll', this.handleScroll, { passive: true });\n this.handleScroll();\n }\n\n private handleScroll = () => {\n if (!this.sentinelEl || !this.scrollRoot) return;\n const rootTop =\n this.scrollRoot === window ? 0 : (this.scrollRoot as HTMLElement).getBoundingClientRect().top;\n const nextPageTopVisible = this.sentinelEl.getBoundingClientRect().top >= rootTop;\n if (\n !nextPageTopVisible &&\n this.pageTopVisible &&\n this.headerEl?.classList.contains(this.variantClass('expanded'))\n ) {\n this.captureExpandedHeaderGeometry(this.headerEl);\n this.expandedGeometryFrozen = true;\n }\n if (\n nextPageTopVisible &&\n this.expandedGeometryFrozen &&\n this.el.getBoundingClientRect().top >= rootTop\n ) {\n this.expandedGeometryFrozen = false;\n if (this.headerEl?.classList.contains(this.variantClass('expanded'))) {\n this.captureExpandedHeaderGeometry(this.headerEl);\n }\n }\n this.pageTopVisible = nextPageTopVisible;\n };\n\n private findHeader(): BarTitleElement | null {\n return this.el.querySelector<BarTitleElement>('ds-bar-title');\n }\n\n private get compactHeaderHeight(): number {\n return resolveCssLengthPx('--dimension-size-600', 0);\n }\n\n private setHeaderTravel(distance: number) {\n const next = Math.max(0, distance);\n if (Math.abs(next - this.headerTravel) < 0.5) return;\n this.headerTravel = next;\n this.el.style.setProperty('--ds-shell-page-header-travel', `${next}px`);\n this.handleScroll();\n }\n\n private measureHeaderTravel(header: BarTitleElement) {\n const anchor = header.querySelector<HTMLElement>('[data-shell-page-header-anchor]');\n const compactHeight = this.compactHeaderHeight;\n if (!anchor || compactHeight <= 0) return;\n\n const headerRect = header.getBoundingClientRect();\n const anchorRect = anchor.getBoundingClientRect();\n if (headerRect.height <= 0 || anchorRect.height <= 0) return;\n\n const expandedAnchorTop = anchorRect.top - headerRect.top;\n const compactAnchorTop = Math.max(0, (compactHeight - anchorRect.height) / 2);\n this.setHeaderTravel(expandedAnchorTop - compactAnchorTop);\n }\n\n private captureExpandedHeaderGeometry(header: BarTitleElement) {\n this.expandedHeaderHeight = header.getBoundingClientRect().height;\n this.measureHeaderTravel(header);\n }\n\n private setSpacerHeight(height: number) {\n if (!this.spacerEl) return;\n this.spacerEl.style.setProperty('--ds-shell-page-flow-spacer', `${Math.max(0, height)}px`);\n }\n\n private variantClass(variant: BarTitleVariant): string {\n return `bar-title-host--${variant}`;\n }\n\n private setStickyHeaderOffset(renderedVariant: BarTitleVariant) {\n this.stickyHeaderEl?.style.setProperty(\n '--ds-shell-page-sticky-offset',\n renderedVariant === 'expanded' &&\n this.headerPresentation === 'auto' &&\n this.headerCapacity === 'roomy'\n ? `${-this.headerTravel}px`\n : '0px'\n );\n }\n\n private syncRenderedHeaderGeometry(header: BarTitleElement) {\n const renderedVariant = this.effectiveVariant;\n if (!header.classList.contains(this.variantClass(renderedVariant))) return;\n\n // Update both flow-preservation pieces before reading layout. Otherwise a\n // reverse compact → expanded render can briefly contain the tall header and\n // the compact spacer together, giving browser scroll anchoring a transient\n // height change to compensate.\n this.setSpacerHeight(\n this.isScrollCompacted ? this.expandedHeaderHeight - this.compactHeaderHeight : 0\n );\n this.setStickyHeaderOffset(renderedVariant);\n\n if (renderedVariant === 'expanded' && !this.expandedGeometryFrozen) {\n this.captureExpandedHeaderGeometry(header);\n this.setStickyHeaderOffset(renderedVariant);\n }\n }\n\n private cancelHeaderGeometrySync() {\n if (this.headerGeometryFrame !== null) cancelAnimationFrame(this.headerGeometryFrame);\n this.headerGeometryFrame = null;\n }\n\n private scheduleHeaderGeometrySync(header: BarTitleElement) {\n this.cancelHeaderGeometrySync();\n let framesRemaining = ShellPage.MAX_HEADER_GEOMETRY_FRAMES;\n let framesElapsed = 0;\n const sync = () => {\n if (this.headerEl !== header) return;\n framesRemaining -= 1;\n framesElapsed += 1;\n // In WebKit a child's componentDidLoad can precede its assignment into\n // the parent's rendered slot. Revalidate after ShellApp has had a frame\n // to expose the actual content scroller.\n this.connectScrollRoot();\n this.syncRenderedHeaderGeometry(header);\n const geometryReady =\n this.effectiveVariant !== 'expanded' ||\n (this.expandedHeaderHeight > 0 && this.headerTravel > 0);\n if ((geometryReady && framesElapsed >= 2) || framesRemaining <= 0) {\n this.headerGeometryFrame = null;\n return;\n }\n this.headerGeometryFrame = requestAnimationFrame(sync);\n };\n this.headerGeometryFrame = requestAnimationFrame(sync);\n }\n\n private cancelHeaderReveal() {\n if (this.headerRevealFrame !== null) cancelAnimationFrame(this.headerRevealFrame);\n this.headerRevealFrame = null;\n this.headerEl?.removeAttribute('data-shell-page-syncing');\n }\n\n private revealHeaderWhenSynced(header: BarTitleElement, variant: BarTitleVariant) {\n // Stencil normally re-renders within a frame or two of a prop change. Stop\n // polling at the cap, but fail closed: a stale variant must never paint.\n let framesRemaining = ShellPage.MAX_HEADER_REVEAL_FRAMES;\n const reveal = () => {\n if (this.headerEl !== header) {\n header.removeAttribute('data-shell-page-syncing');\n return;\n }\n framesRemaining -= 1;\n if (header.classList.contains(this.variantClass(variant))) {\n header.removeAttribute('data-shell-page-syncing');\n this.headerRevealFrame = null;\n return;\n }\n if (framesRemaining <= 0) {\n this.headerRevealFrame = null;\n return;\n }\n this.headerRevealFrame = requestAnimationFrame(reveal);\n };\n this.headerRevealFrame = requestAnimationFrame(reveal);\n }\n\n private syncHeaderVariant(concealUntilSynced = false) {\n const header = this.headerEl;\n if (!header) return;\n\n if (!this.headerContractResolved) {\n this.cancelHeaderReveal();\n header.setAttribute('data-shell-page-syncing', '');\n return;\n }\n\n const next = this.effectiveVariant;\n const renderedVariantIsStale = !header.classList.contains(this.variantClass(next));\n if (!renderedVariantIsStale) this.syncRenderedHeaderGeometry(header);\n if (concealUntilSynced) {\n this.cancelHeaderReveal();\n header.setAttribute('data-shell-page-syncing', '');\n }\n if (header.variant !== next) header.variant = next;\n if (concealUntilSynced) {\n this.revealHeaderWhenSynced(header, next);\n }\n }\n\n private observeHeader(header: BarTitleElement | null) {\n this.cancelHeaderReveal();\n this.cancelHeaderGeometrySync();\n this.headerResizeObserver?.disconnect();\n this.headerResizeObserver = null;\n this.headerMutationObserver?.disconnect();\n this.headerMutationObserver = null;\n this.headerEl = header;\n this.expandedHeaderHeight = 0;\n this.expandedGeometryFrozen = false;\n this.setHeaderTravel(0);\n this.setSpacerHeight(0);\n this.stickyHeaderEl?.style.setProperty('--ds-shell-page-sticky-offset', '0px');\n\n if (!header) return;\n if (header.classList.contains(this.variantClass('expanded'))) {\n this.captureExpandedHeaderGeometry(header);\n }\n if (typeof ResizeObserver !== 'undefined') {\n this.headerResizeObserver = new ResizeObserver(() => {\n this.syncRenderedHeaderGeometry(header);\n });\n this.headerResizeObserver.observe(header);\n }\n if (typeof MutationObserver !== 'undefined') {\n this.headerMutationObserver = new MutationObserver(() => {\n this.syncRenderedHeaderGeometry(header);\n });\n this.headerMutationObserver.observe(header, {\n attributes: true,\n attributeFilter: ['class'],\n });\n }\n this.syncHeaderVariant(true);\n this.scheduleHeaderGeometrySync(header);\n }\n\n private handleHeaderSlotChange = (event: Event) => {\n const slot = event.target as HTMLSlotElement;\n const header = slot\n .assignedElements()\n .find(element => element.tagName.toLowerCase() === 'ds-bar-title');\n this.observeHeader((header as BarTitleElement | undefined) ?? null);\n };\n\n render() {\n return (\n <Host\n role=\"main\"\n class={{\n 'shell-page-host--inset-default': this.contentInset === 'default',\n 'shell-page-host--inset-none': this.contentInset === 'none',\n [`shell-page-host--header-${this.effectiveVariant}`]: true,\n }}\n >\n <div\n ref={el => {\n this.sentinelEl = el ?? null;\n }}\n class=\"shell-page__scroll-sentinel\"\n aria-hidden=\"true\"\n />\n <div\n ref={el => {\n this.stickyHeaderEl = el ?? null;\n }}\n class=\"shell-page__sticky-header\"\n >\n <slot name=\"header\" onSlotchange={this.handleHeaderSlotChange} />\n </div>\n <div\n ref={el => {\n this.spacerEl = el ?? null;\n }}\n class=\"shell-page__flow-spacer\"\n aria-hidden=\"true\"\n />\n <div class=\"shell-page__content\">\n <slot />\n </div>\n </Host>\n );\n }\n}\n",
226
+ "content": "import { Component, Element, h, Host, Prop, State, Watch } from '@stencil/core';\nimport type { ShellResponsiveMode } from '../../shell/shell-responsive';\nimport { resolveCssLengthPx } from '../../utils/resolve-css-length-px';\nimport type { BarTitleVariant } from '../BarTitle/bar-title-types';\nimport { resolveShellPageHeaderVariant } from './shell-page-responsive';\nimport type {\n ShellPageCapacity,\n ShellPageContentInset,\n ShellPageHeaderPresentation,\n} from './shell-page-types';\n\ntype BarTitleElement = HTMLElement & { variant: BarTitleVariant };\n\n@Component({\n tag: 'ds-shell-page',\n styleUrl: 'ShellPage.css',\n scoped: true,\n})\nexport class ShellPage {\n private static readonly MAX_HEADER_REVEAL_FRAMES = 12;\n private static readonly MAX_HEADER_GEOMETRY_FRAMES = 12;\n\n @Element() el!: HTMLElement;\n\n /** Automatic capacity/scroll behavior, or an explicit header variant override. */\n @Prop() headerPresentation: ShellPageHeaderPresentation = 'auto';\n\n /** Available page-header capacity supplied by the owning application shell. */\n @Prop() headerCapacity?: ShellPageCapacity;\n\n /** Standard page gutters, or no inset for full-bleed page content. */\n @Prop() contentInset: ShellPageContentInset = 'default';\n\n /** Explicit shell breakpoint presentation. Mobile uses the dedicated mobile-header slot. */\n @Prop({ attribute: 'responsive-mode', reflect: true })\n responsiveMode: ShellResponsiveMode = 'desktop';\n\n @State() private pageTopVisible = true;\n\n private sentinelEl: HTMLElement | null = null;\n private spacerEl: HTMLElement | null = null;\n private stickyHeaderEl: HTMLElement | null = null;\n private headerEl: BarTitleElement | null = null;\n private expandedHeaderHeight = 0;\n private headerTravel = 0;\n private expandedGeometryFrozen = false;\n private headerResizeObserver: ResizeObserver | null = null;\n private headerMutationObserver: MutationObserver | null = null;\n private headerRevealFrame: number | null = null;\n private headerGeometryFrame: number | null = null;\n private scrollRoot: HTMLElement | Window | null = null;\n\n @Watch('headerCapacity')\n @Watch('headerPresentation')\n handleHeaderContractChange() {\n this.syncHeaderVariant(true);\n }\n\n @Watch('responsiveMode')\n handleResponsiveModeChange() {\n this.observeHeader(this.responsiveMode === 'mobile' ? null : this.findHeader());\n }\n\n componentDidLoad() {\n this.observeHeader(this.responsiveMode === 'mobile' ? null : this.findHeader());\n this.connectScrollRoot();\n }\n\n componentDidRender() {\n this.syncHeaderVariant();\n }\n\n disconnectedCallback() {\n this.headerResizeObserver?.disconnect();\n this.headerResizeObserver = null;\n this.headerMutationObserver?.disconnect();\n this.headerMutationObserver = null;\n this.cancelHeaderReveal();\n this.cancelHeaderGeometrySync();\n this.scrollRoot?.removeEventListener('scroll', this.handleScroll);\n this.scrollRoot = null;\n }\n\n private get effectiveVariant(): BarTitleVariant {\n return resolveShellPageHeaderVariant(\n this.headerPresentation,\n this.headerCapacity ?? 'roomy',\n this.pageTopVisible\n );\n }\n\n private get headerContractResolved(): boolean {\n return this.headerPresentation !== 'auto' || this.headerCapacity !== undefined;\n }\n\n private get isScrollCompacted(): boolean {\n return (\n this.headerPresentation === 'auto' && this.headerCapacity === 'roomy' && !this.pageTopVisible\n );\n }\n\n private composedParent(element: Element): Element | null {\n return (element as HTMLElement).assignedSlot?.parentElement ?? element.parentElement;\n }\n\n private findScrollRoot(): HTMLElement | Window {\n let ancestor = this.composedParent(this.el);\n while (ancestor && ancestor !== document.documentElement) {\n const overflow = getComputedStyle(ancestor).overflowY;\n if (overflow === 'auto' || overflow === 'scroll' || overflow === 'overlay') {\n return ancestor as HTMLElement;\n }\n ancestor = this.composedParent(ancestor);\n }\n return window;\n }\n\n private connectScrollRoot() {\n const next = this.findScrollRoot();\n if (next === this.scrollRoot) return;\n this.scrollRoot?.removeEventListener('scroll', this.handleScroll);\n this.scrollRoot = next;\n this.scrollRoot.addEventListener('scroll', this.handleScroll, { passive: true });\n this.handleScroll();\n }\n\n private handleScroll = () => {\n if (!this.sentinelEl || !this.scrollRoot) return;\n const rootTop =\n this.scrollRoot === window ? 0 : (this.scrollRoot as HTMLElement).getBoundingClientRect().top;\n const nextPageTopVisible = this.sentinelEl.getBoundingClientRect().top >= rootTop;\n if (\n !nextPageTopVisible &&\n this.pageTopVisible &&\n this.headerEl?.classList.contains(this.variantClass('expanded'))\n ) {\n this.captureExpandedHeaderGeometry(this.headerEl);\n this.expandedGeometryFrozen = true;\n }\n if (\n nextPageTopVisible &&\n this.expandedGeometryFrozen &&\n this.el.getBoundingClientRect().top >= rootTop\n ) {\n this.expandedGeometryFrozen = false;\n if (this.headerEl?.classList.contains(this.variantClass('expanded'))) {\n this.captureExpandedHeaderGeometry(this.headerEl);\n }\n }\n this.pageTopVisible = nextPageTopVisible;\n };\n\n private findHeader(): BarTitleElement | null {\n return this.el.querySelector<BarTitleElement>('ds-bar-title');\n }\n\n private get compactHeaderHeight(): number {\n return resolveCssLengthPx('--dimension-size-600', 0);\n }\n\n private setHeaderTravel(distance: number) {\n const next = Math.max(0, distance);\n if (Math.abs(next - this.headerTravel) < 0.5) return;\n this.headerTravel = next;\n this.el.style.setProperty('--ds-shell-page-header-travel', `${next}px`);\n this.handleScroll();\n }\n\n private measureHeaderTravel(header: BarTitleElement) {\n const anchor = header.querySelector<HTMLElement>('[data-shell-page-header-anchor]');\n const compactHeight = this.compactHeaderHeight;\n if (!anchor || compactHeight <= 0) return;\n\n const headerRect = header.getBoundingClientRect();\n const anchorRect = anchor.getBoundingClientRect();\n if (headerRect.height <= 0 || anchorRect.height <= 0) return;\n\n const expandedAnchorTop = anchorRect.top - headerRect.top;\n const compactAnchorTop = Math.max(0, (compactHeight - anchorRect.height) / 2);\n this.setHeaderTravel(expandedAnchorTop - compactAnchorTop);\n }\n\n private captureExpandedHeaderGeometry(header: BarTitleElement) {\n this.expandedHeaderHeight = header.getBoundingClientRect().height;\n this.measureHeaderTravel(header);\n }\n\n private setSpacerHeight(height: number) {\n if (!this.spacerEl) return;\n this.spacerEl.style.setProperty('--ds-shell-page-flow-spacer', `${Math.max(0, height)}px`);\n }\n\n private variantClass(variant: BarTitleVariant): string {\n return `bar-title-host--${variant}`;\n }\n\n private setStickyHeaderOffset(renderedVariant: BarTitleVariant) {\n this.stickyHeaderEl?.style.setProperty(\n '--ds-shell-page-sticky-offset',\n renderedVariant === 'expanded' &&\n this.headerPresentation === 'auto' &&\n this.headerCapacity === 'roomy'\n ? `${-this.headerTravel}px`\n : '0px'\n );\n }\n\n private syncRenderedHeaderGeometry(header: BarTitleElement) {\n const renderedVariant = this.effectiveVariant;\n if (!header.classList.contains(this.variantClass(renderedVariant))) return;\n\n // Update both flow-preservation pieces before reading layout. Otherwise a\n // reverse compact → expanded render can briefly contain the tall header and\n // the compact spacer together, giving browser scroll anchoring a transient\n // height change to compensate.\n this.setSpacerHeight(\n this.isScrollCompacted ? this.expandedHeaderHeight - this.compactHeaderHeight : 0\n );\n this.setStickyHeaderOffset(renderedVariant);\n\n if (renderedVariant === 'expanded' && !this.expandedGeometryFrozen) {\n this.captureExpandedHeaderGeometry(header);\n this.setStickyHeaderOffset(renderedVariant);\n }\n }\n\n private cancelHeaderGeometrySync() {\n if (this.headerGeometryFrame !== null) cancelAnimationFrame(this.headerGeometryFrame);\n this.headerGeometryFrame = null;\n }\n\n private scheduleHeaderGeometrySync(header: BarTitleElement) {\n this.cancelHeaderGeometrySync();\n let framesRemaining = ShellPage.MAX_HEADER_GEOMETRY_FRAMES;\n let framesElapsed = 0;\n const sync = () => {\n if (this.headerEl !== header) return;\n framesRemaining -= 1;\n framesElapsed += 1;\n // In WebKit a child's componentDidLoad can precede its assignment into\n // the parent's rendered slot. Revalidate after ShellApp has had a frame\n // to expose the actual content scroller.\n this.connectScrollRoot();\n this.syncRenderedHeaderGeometry(header);\n const geometryReady =\n this.effectiveVariant !== 'expanded' ||\n (this.expandedHeaderHeight > 0 && this.headerTravel > 0);\n if ((geometryReady && framesElapsed >= 2) || framesRemaining <= 0) {\n this.headerGeometryFrame = null;\n return;\n }\n this.headerGeometryFrame = requestAnimationFrame(sync);\n };\n this.headerGeometryFrame = requestAnimationFrame(sync);\n }\n\n private cancelHeaderReveal() {\n if (this.headerRevealFrame !== null) cancelAnimationFrame(this.headerRevealFrame);\n this.headerRevealFrame = null;\n this.headerEl?.removeAttribute('data-shell-page-syncing');\n }\n\n private revealHeaderWhenSynced(header: BarTitleElement, variant: BarTitleVariant) {\n // Stencil normally re-renders within a frame or two of a prop change. Stop\n // polling at the cap, but fail closed: a stale variant must never paint.\n let framesRemaining = ShellPage.MAX_HEADER_REVEAL_FRAMES;\n const reveal = () => {\n if (this.headerEl !== header) {\n header.removeAttribute('data-shell-page-syncing');\n return;\n }\n framesRemaining -= 1;\n if (header.classList.contains(this.variantClass(variant))) {\n header.removeAttribute('data-shell-page-syncing');\n this.headerRevealFrame = null;\n return;\n }\n if (framesRemaining <= 0) {\n this.headerRevealFrame = null;\n return;\n }\n this.headerRevealFrame = requestAnimationFrame(reveal);\n };\n this.headerRevealFrame = requestAnimationFrame(reveal);\n }\n\n private syncHeaderVariant(concealUntilSynced = false) {\n const header = this.headerEl;\n if (!header) return;\n\n if (!this.headerContractResolved) {\n this.cancelHeaderReveal();\n header.setAttribute('data-shell-page-syncing', '');\n return;\n }\n\n const next = this.effectiveVariant;\n const renderedVariantIsStale = !header.classList.contains(this.variantClass(next));\n if (!renderedVariantIsStale) this.syncRenderedHeaderGeometry(header);\n if (concealUntilSynced) {\n this.cancelHeaderReveal();\n header.setAttribute('data-shell-page-syncing', '');\n }\n if (header.variant !== next) header.variant = next;\n if (concealUntilSynced) {\n this.revealHeaderWhenSynced(header, next);\n }\n }\n\n private observeHeader(header: BarTitleElement | null) {\n this.cancelHeaderReveal();\n this.cancelHeaderGeometrySync();\n this.headerResizeObserver?.disconnect();\n this.headerResizeObserver = null;\n this.headerMutationObserver?.disconnect();\n this.headerMutationObserver = null;\n this.headerEl = header;\n this.expandedHeaderHeight = 0;\n this.expandedGeometryFrozen = false;\n this.setHeaderTravel(0);\n this.setSpacerHeight(0);\n this.stickyHeaderEl?.style.setProperty('--ds-shell-page-sticky-offset', '0px');\n\n if (!header) return;\n if (header.classList.contains(this.variantClass('expanded'))) {\n this.captureExpandedHeaderGeometry(header);\n }\n if (typeof ResizeObserver !== 'undefined') {\n this.headerResizeObserver = new ResizeObserver(() => {\n this.syncRenderedHeaderGeometry(header);\n });\n this.headerResizeObserver.observe(header);\n }\n if (typeof MutationObserver !== 'undefined') {\n this.headerMutationObserver = new MutationObserver(() => {\n this.syncRenderedHeaderGeometry(header);\n });\n this.headerMutationObserver.observe(header, {\n attributes: true,\n attributeFilter: ['class'],\n });\n }\n this.syncHeaderVariant(true);\n this.scheduleHeaderGeometrySync(header);\n }\n\n private handleHeaderSlotChange = (event: Event) => {\n if (this.responsiveMode === 'mobile') return;\n const slot = event.target as HTMLSlotElement;\n const header = slot\n .assignedElements()\n .find(element => element.tagName.toLowerCase() === 'ds-bar-title');\n this.observeHeader((header as BarTitleElement | undefined) ?? null);\n };\n\n render() {\n const mobile = this.responsiveMode === 'mobile';\n return (\n <Host\n role=\"main\"\n class={{\n 'shell-page-host--inset-default': this.contentInset === 'default',\n 'shell-page-host--inset-none': this.contentInset === 'none',\n 'shell-page-host--mobile': mobile,\n [`shell-page-host--header-${this.effectiveVariant}`]: true,\n }}\n >\n <div\n ref={el => {\n this.sentinelEl = el ?? null;\n }}\n class=\"shell-page__scroll-sentinel\"\n aria-hidden=\"true\"\n />\n <div\n ref={el => {\n this.stickyHeaderEl = el ?? null;\n }}\n class=\"shell-page__sticky-header\"\n >\n <div\n class=\"shell-page__desktop-header\"\n hidden={mobile}\n aria-hidden={mobile ? 'true' : undefined}\n inert={mobile ? true : undefined}\n >\n <slot name=\"header\" onSlotchange={this.handleHeaderSlotChange} />\n </div>\n <div\n class=\"shell-page__mobile-header\"\n hidden={!mobile}\n aria-hidden={!mobile ? 'true' : undefined}\n inert={!mobile ? true : undefined}\n >\n <slot name=\"mobile-header\" />\n </div>\n </div>\n <div\n ref={el => {\n this.spacerEl = el ?? null;\n }}\n class=\"shell-page__flow-spacer\"\n aria-hidden=\"true\"\n />\n <div class=\"shell-page__content\">\n <slot />\n </div>\n </Host>\n );\n }\n}\n",
208
227
  "type": "registry:ui"
209
228
  },
210
229
  {
@@ -31,7 +31,9 @@
31
31
  "commonlyComposedWith": [
32
32
  "component:ds-shell-app",
33
33
  "component:ds-panel-tool-header",
34
- "component:ds-shell-mobile-bar"
34
+ "component:ds-mobile-header",
35
+ "component:ds-mobile-section-switcher",
36
+ "component:ds-mobile-bar-nav"
35
37
  ],
36
38
  "patterns": [
37
39
  "pattern:application-shell"
@@ -39,7 +41,7 @@
39
41
  "accessibility": [
40
42
  "Only the active mobile tool view participates in focus or the accessibility tree.",
41
43
  "Header Back and application-owned header actions preserve the PanelTools event contract.",
42
- "Inbox uses TabGroup semantics for Messages, Stacks, and Activity."
44
+ "Inbox uses the MobileSectionSwitcher popup trigger for Messages, Stacks, and Activity and never relies on a gesture-only interaction."
43
45
  ],
44
46
  "states": [
45
47
  "The application owns open and activeTool and replaces headers when tool depth changes.",
@@ -48,7 +50,7 @@
48
50
  ],
49
51
  "responsiveBehavior": [
50
52
  "Desktop and tablet compose PanelTools-compatible rail and drawer chrome.",
51
- "Mobile renders the active tool across the shell stage and groups available inbox tools in a segmented control."
53
+ "Mobile renders one centered MobileHeader for every tool and presents Messages, Stacks, and Activity directly as peer section titles without an additional Inbox heading."
52
54
  ],
53
55
  "frameworkCaveats": {
54
56
  "customElements": [
@@ -431,19 +433,20 @@
431
433
  "@ds-mo/tokens"
432
434
  ],
433
435
  "registryDependencies": [
434
- "panel-tool-header",
436
+ "button-unfilled",
437
+ "mobile-header",
435
438
  "panel-tools",
436
- "tab-group"
439
+ "tooltip"
437
440
  ],
438
441
  "files": [
439
442
  {
440
443
  "path": "src/wc/components/ShellTools/ShellTools.css",
441
- "content": ":host {\n display: block;\n min-width: 0;\n min-height: 0;\n height: 100%;\n background: transparent;\n}\n\n:host([responsive-mode='mobile']) {\n width: 100%;\n background: var(--color-background-primary);\n}\n\n.shell-tools__desktop,\n.shell-tools__desktop ds-panel-tools,\n.shell-tools__mobile {\n display: block;\n min-width: 0;\n min-height: 0;\n height: 100%;\n}\n\n.shell-tools__mobile {\n display: grid;\n grid-template-rows: auto auto minmax(0, 1fr);\n background: var(--color-background-primary);\n}\n\n.shell-tools__inbox-nav {\n padding: var(--dimension-space-100);\n border-bottom: var(--dimension-stroke-width-012) solid var(--color-border-tertiary);\n background: var(--color-background-primary);\n}\n\n.shell-tools__mobile-body,\n.shell-tools__view {\n min-width: 0;\n min-height: 0;\n height: 100%;\n}\n\n.shell-tools__mobile-body {\n overflow: hidden;\n}\n\n.shell-tools__view {\n overflow: auto;\n overscroll-behavior: contain;\n}\n\n.shell-tools__view[hidden] {\n display: none;\n}\n\n.shell-tools__view ::slotted(*) {\n min-height: 0;\n}\n",
444
+ "content": ":host {\n display: block;\n min-width: 0;\n min-height: 0;\n height: 100%;\n background: transparent;\n}\n\n:host([responsive-mode='mobile']) {\n width: 100%;\n background: var(--color-background-primary);\n}\n\n.shell-tools__desktop,\n.shell-tools__desktop ds-panel-tools,\n.shell-tools__mobile {\n display: block;\n min-width: 0;\n min-height: 0;\n height: 100%;\n}\n\n.shell-tools__mobile {\n display: grid;\n grid-template-rows: auto minmax(0, 1fr);\n background: var(--color-background-primary);\n}\n\n.shell-tools__mobile-header {\n grid-row: 1;\n}\n\n.shell-tools__mobile-body,\n.shell-tools__view {\n min-width: 0;\n min-height: 0;\n height: 100%;\n}\n\n.shell-tools__mobile-body {\n grid-row: 2;\n overflow: hidden;\n}\n\n.shell-tools__view {\n overflow: auto;\n overscroll-behavior: contain;\n}\n\n.shell-tools__view[hidden] {\n display: none;\n}\n\n.shell-tools__view ::slotted(*) {\n min-height: 0;\n}\n",
442
445
  "type": "registry:ui"
443
446
  },
444
447
  {
445
448
  "path": "src/wc/components/ShellTools/ShellTools.tsx",
446
- "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n Watch,\n} from '@stencil/core';\nimport {\n PANEL_TOOLS_LABELS,\n PANEL_TOOLS_TOOL_IDS,\n type PanelToolsHeaderAction,\n type PanelToolsHeaderConfig,\n type PanelToolsHeaders,\n type PanelToolsItem,\n type PanelToolsToolId,\n} from '../PanelTools/panel-tools-types';\nimport {\n parsePanelToolsItems,\n reconcilePanelToolsAvailability,\n resolvePanelToolActivation,\n} from '../PanelTools/panel-tools-utils';\nimport {\n isShellInboxTool,\n resolveAvailableInboxTool,\n type ShellInboxToolId,\n type ShellResponsiveMode,\n} from '../../shell/shell-responsive';\n\nconst MOBILE_VIEW_ORDER: PanelToolsToolId[] = [\n 'search',\n 'agents',\n 'messages',\n 'stacks',\n 'activity',\n 'help',\n];\n\n@Component({\n tag: 'ds-shell-tools',\n styleUrl: 'ShellTools.css',\n scoped: true,\n})\nexport class ShellTools {\n @Element() el!: HTMLElement;\n\n /** Resolved by ShellApp; mobile uses a full-stage tool presentation. */\n @Prop({ attribute: 'responsive-mode', reflect: true })\n responsiveMode: ShellResponsiveMode = 'desktop';\n @Prop({ mutable: true, reflect: true }) open: boolean = false;\n @Prop({ mutable: true, attribute: 'active-tool', reflect: true })\n activeTool: PanelToolsToolId | '' = '';\n @Prop({ mutable: true, reflect: true }) presentation: 'drawer' | 'fullscreen' = 'drawer';\n @Prop({ attribute: 'fullscreen-header-mode', reflect: true })\n fullscreenHeaderMode: 'shared' | 'split' = 'shared';\n @Prop() items: PanelToolsItem[] = [];\n @Prop({ attribute: 'items-json' }) itemsJson: string = '';\n @Prop() headers: PanelToolsHeaders = {};\n @Prop({ attribute: 'headers-json' }) headersJson: string = '';\n @Prop({ attribute: 'storage-key' }) storageKey: string = '';\n @Prop() toolsLabel: string = 'Tools';\n @Prop() toolShortcutsLabel: string = 'Tool shortcuts';\n @Prop() inboxLabel: string = 'Inbox';\n @Prop() inboxNavigationLabel: string = 'Inbox sections';\n\n @Event({ bubbles: true, composed: true }) dsToolChange!: EventEmitter<{\n id: PanelToolsToolId;\n selected: boolean;\n }>;\n @Event({ bubbles: true, composed: true }) dsPresentationChange!: EventEmitter<{\n presentation: 'drawer' | 'fullscreen';\n }>;\n @Event({ bubbles: true, composed: true }) dsHeaderBack!: EventEmitter<{\n tool: PanelToolsToolId;\n }>;\n @Event({ bubbles: true, composed: true }) dsHeaderAction!: EventEmitter<{\n tool: PanelToolsToolId;\n id: string;\n }>;\n\n private panelToolsEl: HTMLDsPanelToolsElement | null = null;\n\n private get resolvedItems(): PanelToolsItem[] {\n return parsePanelToolsItems(this.items, this.itemsJson);\n }\n\n private get availableInboxTools(): ShellInboxToolId[] {\n return this.resolvedItems\n .filter(item => !item.isInactive && isShellInboxTool(item.id))\n .map(item => item.id as ShellInboxToolId);\n }\n\n private get mobileActiveTool(): PanelToolsToolId | '' {\n if (!isShellInboxTool(this.activeTool)) return this.activeTool;\n return resolveAvailableInboxTool(this.activeTool, this.availableInboxTools);\n }\n\n componentWillLoad() {\n this.reconcileAvailability();\n }\n\n @Watch('items')\n @Watch('itemsJson')\n handleItemsChange() {\n this.reconcileAvailability();\n }\n\n private reconcileAvailability() {\n const next = reconcilePanelToolsAvailability(this.resolvedItems, this.open, this.activeTool);\n this.open = next.open;\n this.activeTool = next.activeTool;\n }\n\n private resolvedHeaders(): PanelToolsHeaders {\n if (Object.keys(this.headers).length) return this.headers;\n if (!this.headersJson.trim()) return {};\n try {\n const parsed = JSON.parse(this.headersJson);\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as PanelToolsHeaders)\n : {};\n } catch {\n return {};\n }\n }\n\n private activeHeader(tool: PanelToolsToolId): PanelToolsHeaderConfig {\n return this.resolvedHeaders()[tool] ?? {};\n }\n\n private activateMobileTool(id: PanelToolsToolId) {\n const item = this.resolvedItems.find(entry => entry.id === id);\n if (!item || item.isInactive) return;\n const next = resolvePanelToolActivation(this.open, this.activeTool, id);\n this.open = next.open;\n this.activeTool = next.activeTool;\n this.dsToolChange.emit({ id, selected: next.selected });\n }\n\n private selectInboxTool = (event: CustomEvent<string>) => {\n const id = event.detail;\n if (!isShellInboxTool(id) || id === this.mobileActiveTool) return;\n this.open = true;\n this.activeTool = id;\n this.dsToolChange.emit({ id, selected: true });\n };\n\n private handlePanelToolChange = (\n event: CustomEvent<{ id: PanelToolsToolId; selected: boolean }>\n ) => {\n this.activeTool = event.detail.id;\n this.open = event.detail.selected;\n };\n\n private handlePanelPresentationChange = (\n event: CustomEvent<{ presentation: 'drawer' | 'fullscreen' }>\n ) => {\n this.presentation = event.detail.presentation;\n };\n\n private handleMobileHeaderAction = (\n tool: PanelToolsToolId,\n event: CustomEvent<{ id: string }>\n ) => {\n event.stopPropagation();\n this.dsHeaderAction.emit({ tool, id: event.detail.id });\n };\n\n /** Match PanelTools' imperative activation contract in every responsive mode. */\n @Method()\n async activateTool(id: PanelToolsToolId) {\n if (this.responsiveMode !== 'mobile') {\n await this.panelToolsEl?.activateTool(id);\n return;\n }\n this.activateMobileTool(id);\n }\n\n /** Close the active global tool surface. */\n @Method()\n async closeDrawer() {\n if (this.responsiveMode !== 'mobile') {\n await this.panelToolsEl?.closeDrawer();\n return;\n }\n if (!this.open) return;\n const id = this.activeTool;\n this.open = false;\n if (id) this.dsToolChange.emit({ id, selected: false });\n }\n\n /** Focus an application-owned action in the visible shared tool header. */\n @Method()\n async focusHeaderAction(id: string) {\n if (this.responsiveMode !== 'mobile') {\n await this.panelToolsEl?.focusHeaderAction(id);\n return;\n }\n const action = this.el.querySelector(`[data-header-action-id=\"${CSS.escape(id)}\"]`) as\n | (HTMLElement & { setFocus?: () => Promise<void> })\n | null;\n await action?.setFocus?.();\n }\n\n private renderForwardedSlots() {\n return PANEL_TOOLS_TOOL_IDS.flatMap(id => [\n <slot name={id} slot={id} />,\n <slot name={`${id}-view`} slot={`${id}-view`} />,\n ]);\n }\n\n private renderDesktop() {\n return (\n <div class=\"shell-tools__desktop\">\n <ds-panel-tools\n open={this.open}\n activeTool={this.activeTool}\n presentation={this.presentation}\n fullscreenHeaderMode={this.fullscreenHeaderMode}\n items={this.resolvedItems}\n headers={this.resolvedHeaders()}\n storageKey={this.storageKey}\n toolsLabel={this.toolsLabel}\n toolShortcutsLabel={this.toolShortcutsLabel}\n ref={element => {\n this.panelToolsEl = element as HTMLDsPanelToolsElement | null;\n }}\n onDsToolChange={this.handlePanelToolChange}\n onDsPresentationChange={this.handlePanelPresentationChange}\n >\n {this.renderForwardedSlots()}\n </ds-panel-tools>\n </div>\n );\n }\n\n private renderMobileHeader(tool: PanelToolsToolId) {\n const header = this.activeHeader(tool);\n const configuredTitle = header.title?.trim();\n const title = isShellInboxTool(tool)\n ? header.showBack && configuredTitle\n ? configuredTitle\n : this.inboxLabel\n : configuredTitle || PANEL_TOOLS_LABELS[tool];\n const actions = (header.actions ?? []).filter(action => action.id !== 'fullscreen');\n\n return (\n <ds-panel-tool-header\n class=\"shell-tools__mobile-header\"\n heading={title}\n showBack={header.showBack ?? false}\n backIcon={header.backIcon || 'ChevronLeft'}\n backAriaLabel={header.backAriaLabel || 'Back'}\n showMenu={false}\n ref={element => {\n const toolHeader = element as\n | (HTMLDsPanelToolHeaderElement & { actions: PanelToolsHeaderAction[] })\n | undefined;\n if (toolHeader) toolHeader.actions = actions;\n }}\n onDsBack={() => this.dsHeaderBack.emit({ tool })}\n onDsAction={(event: CustomEvent<{ id: string }>) =>\n this.handleMobileHeaderAction(tool, event)}\n />\n );\n }\n\n private renderMobile() {\n const tool = this.mobileActiveTool;\n const inboxTabs = this.availableInboxTools.map(id => ({\n id,\n label: PANEL_TOOLS_LABELS[id],\n }));\n\n return (\n <div\n class=\"shell-tools__mobile\"\n aria-hidden={this.open && tool ? undefined : 'true'}\n inert={this.open && tool ? undefined : true}\n >\n {tool ? this.renderMobileHeader(tool) : null}\n {tool && isShellInboxTool(tool) && inboxTabs.length ? (\n <div class=\"shell-tools__inbox-nav\">\n <ds-tab-group\n tabs={inboxTabs}\n value={tool}\n aria-label={this.inboxNavigationLabel}\n onDsChange={this.selectInboxTool}\n />\n </div>\n ) : null}\n <div class=\"shell-tools__mobile-body\">\n {MOBILE_VIEW_ORDER.map(id => {\n const active = this.open && id === tool;\n return (\n <div\n class={{\n 'shell-tools__view': true,\n 'shell-tools__view--active': active,\n }}\n hidden={!active}\n aria-hidden={active ? undefined : 'true'}\n inert={active ? undefined : true}\n >\n <slot name={`${id}-view`}>\n <slot name={id} />\n </slot>\n </div>\n );\n })}\n </div>\n </div>\n );\n }\n\n render() {\n const mobile = this.responsiveMode === 'mobile';\n return (\n <Host\n role={\n mobile\n ? this.presentation === 'fullscreen'\n ? 'dialog'\n : 'complementary'\n : undefined\n }\n aria-modal={mobile && this.presentation === 'fullscreen' ? 'true' : undefined}\n aria-label={mobile ? this.toolsLabel : undefined}\n >\n {mobile ? this.renderMobile() : this.renderDesktop()}\n </Host>\n );\n }\n}\n",
449
+ "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n Watch,\n} from '@stencil/core';\nimport {\n PANEL_TOOLS_LABELS,\n PANEL_TOOLS_TOOL_IDS,\n type PanelToolsHeaderConfig,\n type PanelToolsHeaders,\n type PanelToolsItem,\n type PanelToolsToolId,\n} from '../PanelTools/panel-tools-types';\nimport {\n parsePanelToolsItems,\n reconcilePanelToolsAvailability,\n resolvePanelToolActivation,\n} from '../PanelTools/panel-tools-utils';\nimport {\n isShellInboxTool,\n resolveAvailableInboxTool,\n type ShellInboxToolId,\n type ShellResponsiveMode,\n} from '../../shell/shell-responsive';\n\nconst MOBILE_VIEW_ORDER: PanelToolsToolId[] = [\n 'search',\n 'agents',\n 'messages',\n 'stacks',\n 'activity',\n 'help',\n];\n\n@Component({\n tag: 'ds-shell-tools',\n styleUrl: 'ShellTools.css',\n scoped: true,\n})\nexport class ShellTools {\n @Element() el!: HTMLElement;\n\n /** Resolved by ShellApp; mobile uses a full-stage tool presentation. */\n @Prop({ attribute: 'responsive-mode', reflect: true })\n responsiveMode: ShellResponsiveMode = 'desktop';\n @Prop({ mutable: true, reflect: true }) open: boolean = false;\n @Prop({ mutable: true, attribute: 'active-tool', reflect: true })\n activeTool: PanelToolsToolId | '' = '';\n @Prop({ mutable: true, reflect: true }) presentation: 'drawer' | 'fullscreen' = 'drawer';\n @Prop({ attribute: 'fullscreen-header-mode', reflect: true })\n fullscreenHeaderMode: 'shared' | 'split' = 'shared';\n @Prop() items: PanelToolsItem[] = [];\n @Prop({ attribute: 'items-json' }) itemsJson: string = '';\n @Prop() headers: PanelToolsHeaders = {};\n @Prop({ attribute: 'headers-json' }) headersJson: string = '';\n @Prop({ attribute: 'storage-key' }) storageKey: string = '';\n @Prop() toolsLabel: string = 'Tools';\n @Prop() toolShortcutsLabel: string = 'Tool shortcuts';\n @Prop() inboxLabel: string = 'Inbox';\n @Prop() inboxNavigationLabel: string = 'Inbox sections';\n\n @Event({ bubbles: true, composed: true }) dsToolChange!: EventEmitter<{\n id: PanelToolsToolId;\n selected: boolean;\n }>;\n @Event({ bubbles: true, composed: true }) dsPresentationChange!: EventEmitter<{\n presentation: 'drawer' | 'fullscreen';\n }>;\n @Event({ bubbles: true, composed: true }) dsHeaderBack!: EventEmitter<{\n tool: PanelToolsToolId;\n }>;\n @Event({ bubbles: true, composed: true }) dsHeaderAction!: EventEmitter<{\n tool: PanelToolsToolId;\n id: string;\n }>;\n\n private panelToolsEl: HTMLDsPanelToolsElement | null = null;\n\n private get resolvedItems(): PanelToolsItem[] {\n return parsePanelToolsItems(this.items, this.itemsJson);\n }\n\n private get availableInboxTools(): ShellInboxToolId[] {\n return this.resolvedItems\n .filter(item => !item.isInactive && isShellInboxTool(item.id))\n .map(item => item.id as ShellInboxToolId);\n }\n\n private get mobileActiveTool(): PanelToolsToolId | '' {\n if (!isShellInboxTool(this.activeTool)) return this.activeTool;\n return resolveAvailableInboxTool(this.activeTool, this.availableInboxTools);\n }\n\n componentWillLoad() {\n this.reconcileAvailability();\n }\n\n @Watch('items')\n @Watch('itemsJson')\n handleItemsChange() {\n this.reconcileAvailability();\n }\n\n private reconcileAvailability() {\n const next = reconcilePanelToolsAvailability(this.resolvedItems, this.open, this.activeTool);\n this.open = next.open;\n this.activeTool = next.activeTool;\n }\n\n private resolvedHeaders(): PanelToolsHeaders {\n if (Object.keys(this.headers).length) return this.headers;\n if (!this.headersJson.trim()) return {};\n try {\n const parsed = JSON.parse(this.headersJson);\n return parsed && typeof parsed === 'object' && !Array.isArray(parsed)\n ? (parsed as PanelToolsHeaders)\n : {};\n } catch {\n return {};\n }\n }\n\n private activeHeader(tool: PanelToolsToolId): PanelToolsHeaderConfig {\n return this.resolvedHeaders()[tool] ?? {};\n }\n\n private activateMobileTool(id: PanelToolsToolId) {\n const item = this.resolvedItems.find(entry => entry.id === id);\n if (!item || item.isInactive) return;\n const next = resolvePanelToolActivation(this.open, this.activeTool, id);\n this.open = next.open;\n this.activeTool = next.activeTool;\n this.dsToolChange.emit({ id, selected: next.selected });\n }\n\n private selectInboxTool = (event: CustomEvent<string>) => {\n const id = event.detail;\n if (!isShellInboxTool(id) || id === this.mobileActiveTool) return;\n this.open = true;\n this.activeTool = id;\n this.dsToolChange.emit({ id, selected: true });\n };\n\n private handlePanelToolChange = (\n event: CustomEvent<{ id: PanelToolsToolId; selected: boolean }>\n ) => {\n this.activeTool = event.detail.id;\n this.open = event.detail.selected;\n };\n\n private handlePanelPresentationChange = (\n event: CustomEvent<{ presentation: 'drawer' | 'fullscreen' }>\n ) => {\n this.presentation = event.detail.presentation;\n };\n\n private handleMobileHeaderAction = (\n tool: PanelToolsToolId,\n id: string,\n event: CustomEvent<MouseEvent>\n ) => {\n event.stopPropagation();\n this.dsHeaderAction.emit({ tool, id });\n };\n\n /** Match PanelTools' imperative activation contract in every responsive mode. */\n @Method()\n async activateTool(id: PanelToolsToolId) {\n if (this.responsiveMode !== 'mobile') {\n await this.panelToolsEl?.activateTool(id);\n return;\n }\n this.activateMobileTool(id);\n }\n\n /** Close the active global tool surface. */\n @Method()\n async closeDrawer() {\n if (this.responsiveMode !== 'mobile') {\n await this.panelToolsEl?.closeDrawer();\n return;\n }\n if (!this.open) return;\n const id = this.activeTool;\n this.open = false;\n if (id) this.dsToolChange.emit({ id, selected: false });\n }\n\n /** Focus an application-owned action in the visible shared tool header. */\n @Method()\n async focusHeaderAction(id: string) {\n if (this.responsiveMode !== 'mobile') {\n await this.panelToolsEl?.focusHeaderAction(id);\n return;\n }\n const action = this.el.querySelector(`[data-header-action-id=\"${CSS.escape(id)}\"]`) as\n | (HTMLElement & { setFocus?: () => Promise<void> })\n | null;\n await action?.setFocus?.();\n }\n\n private renderForwardedSlots() {\n return PANEL_TOOLS_TOOL_IDS.flatMap(id => [\n <slot name={id} slot={id} />,\n <slot name={`${id}-view`} slot={`${id}-view`} />,\n ]);\n }\n\n private renderDesktop() {\n return (\n <div class=\"shell-tools__desktop\">\n <ds-panel-tools\n open={this.open}\n activeTool={this.activeTool}\n presentation={this.presentation}\n fullscreenHeaderMode={this.fullscreenHeaderMode}\n items={this.resolvedItems}\n headers={this.resolvedHeaders()}\n storageKey={this.storageKey}\n toolsLabel={this.toolsLabel}\n toolShortcutsLabel={this.toolShortcutsLabel}\n ref={element => {\n this.panelToolsEl = element as HTMLDsPanelToolsElement | null;\n }}\n onDsToolChange={this.handlePanelToolChange}\n onDsPresentationChange={this.handlePanelPresentationChange}\n >\n {this.renderForwardedSlots()}\n </ds-panel-tools>\n </div>\n );\n }\n\n private renderMobileHeader(tool: PanelToolsToolId) {\n const header = this.activeHeader(tool);\n const configuredTitle = header.title?.trim();\n const title = configuredTitle || PANEL_TOOLS_LABELS[tool];\n const actions = (header.actions ?? []).filter(action => action.id !== 'fullscreen');\n const sections =\n isShellInboxTool(tool) && !header.showBack\n ? this.availableInboxTools.map(id => ({\n id,\n label: PANEL_TOOLS_LABELS[id],\n }))\n : [];\n\n return (\n <ds-mobile-header\n class=\"shell-tools__mobile-header\"\n heading={title}\n headingLevel=\"h2\"\n sections={sections}\n value={tool}\n sectionsAriaLabel={this.inboxNavigationLabel}\n onDsSectionChange={this.selectInboxTool}\n >\n {header.showBack ? (\n <ds-tooltip\n slot=\"leading\"\n label={header.backAriaLabel || 'Back'}\n side=\"bottom\"\n size=\"sm\"\n >\n <ds-button-unfilled\n variant=\"icon\"\n icon={header.backIcon || 'ChevronLeft'}\n size=\"md\"\n aria-label={header.backAriaLabel || 'Back'}\n activeFill={false}\n hasBorder={false}\n onDsClick={() => this.dsHeaderBack.emit({ tool })}\n />\n </ds-tooltip>\n ) : null}\n {actions.map(action => (\n <ds-tooltip\n slot=\"trailing\"\n key={action.id}\n label={action.ariaLabel}\n side=\"bottom\"\n size=\"sm\"\n >\n <ds-button-unfilled\n id={action.triggerId || undefined}\n data-header-action-id={action.id}\n variant=\"icon\"\n icon={action.icon}\n size=\"md\"\n aria-label={action.ariaLabel}\n haspopup={action.haspopup}\n controls={action.controls}\n expanded={action.expanded}\n pressed={action.pressed}\n isActive={!!action.expanded}\n isInactive={action.isInactive}\n activeFill={false}\n hasBorder={false}\n onDsClick={(event: CustomEvent<MouseEvent>) =>\n this.handleMobileHeaderAction(tool, action.id, event)}\n />\n </ds-tooltip>\n ))}\n </ds-mobile-header>\n );\n }\n\n private renderMobile() {\n const tool = this.mobileActiveTool;\n return (\n <div\n class=\"shell-tools__mobile\"\n aria-hidden={this.open && tool ? undefined : 'true'}\n inert={this.open && tool ? undefined : true}\n >\n {tool ? this.renderMobileHeader(tool) : null}\n <div class=\"shell-tools__mobile-body\">\n {MOBILE_VIEW_ORDER.map(id => {\n const active = this.open && id === tool;\n return (\n <div\n class={{\n 'shell-tools__view': true,\n 'shell-tools__view--active': active,\n }}\n hidden={!active}\n aria-hidden={active ? undefined : 'true'}\n inert={active ? undefined : true}\n >\n <slot name={`${id}-view`}>\n <slot name={id} />\n </slot>\n </div>\n );\n })}\n </div>\n </div>\n );\n }\n\n render() {\n const mobile = this.responsiveMode === 'mobile';\n return (\n <Host\n role={\n mobile\n ? this.presentation === 'fullscreen'\n ? 'dialog'\n : 'complementary'\n : undefined\n }\n aria-modal={mobile && this.presentation === 'fullscreen' ? 'true' : undefined}\n aria-label={mobile ? this.toolsLabel : undefined}\n >\n {mobile ? this.renderMobile() : this.renderDesktop()}\n </Host>\n );\n }\n}\n",
447
450
  "type": "registry:ui"
448
451
  }
449
452
  ]
@@ -354,7 +354,7 @@
354
354
  },
355
355
  {
356
356
  "path": "src/wc/components/Switch/Switch.tsx",
357
- "content": "import { AttachInternals, Component, Element, Prop, State, Event, EventEmitter, Watch, h, Host } from '@stencil/core';\n\nexport type SwitchSize = 'md' | 'sm' | 'xs';\n\nlet generatedLabelId = 0;\n\n@Component({\n tag: 'ds-switch',\n styleUrl: 'Switch.css',\n shadow: true,\n formAssociated: true,\n})\nexport class Switch {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n /** Current on/off state. The initial value is restored when the owning form resets. */\n @Prop({ mutable: true }) checked: boolean = false;\n\n @Prop({ reflect: true }) name: string | undefined;\n @Prop() value: string = 'on';\n\n /** Optional value submitted when unchecked; omitted by default like a native checkbox. */\n @Prop() uncheckedValue: string | undefined;\n\n /** Associates the switch with a form by id when it is rendered outside that form. */\n @Prop({ reflect: true }) form: string | undefined;\n\n @Prop({ reflect: true }) disabled: boolean = false;\n @Prop({ reflect: true }) readOnly: boolean = false;\n @Prop({ reflect: true }) required: boolean = false;\n @Prop() requiredMessage: string = 'This field is required.';\n @Prop() isInactive: boolean = false;\n\n /** Compact track size for placement inside controls, menus, and form rows. */\n @Prop() size: SwitchSize = 'md';\n\n /**\n * Removes standalone control semantics so a composite owner such as a\n * `menuitemcheckbox` can use the switch as an aria-hidden visual indicator.\n */\n @Prop() presentation: boolean = false;\n\n @Event() dsChange!: EventEmitter<boolean>;\n\n private initialChecked = false;\n @State() private formDisabled = false;\n @State() private focused = false;\n @State() private touched = false;\n\n componentWillLoad() {\n this.initialChecked = this.checked;\n this.syncFormValue();\n }\n\n componentDidLoad() {\n this.syncNativeLabels();\n }\n\n @Watch('checked')\n @Watch('value')\n @Watch('uncheckedValue')\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('presentation')\n @Watch('required')\n syncFormValue() {\n const inactive = this.presentation || this.isInactive || this.disabled || this.formDisabled;\n const submissionValue = this.checked ? this.value : this.uncheckedValue ?? null;\n const state = this.checked ? 'checked' : 'unchecked';\n this.internals.setFormValue(inactive ? null : submissionValue, state);\n\n const missing = this.required && !inactive && !this.checked;\n this.internals.setValidity(missing ? { valueMissing: true } : {}, missing ? this.requiredMessage : '');\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.checked = this.initialChecked;\n this.focused = false;\n this.touched = false;\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n this.checked = state === 'checked';\n }\n\n private syncNativeLabels() {\n if (this.presentation) return;\n if (this.el.hasAttribute('aria-label') || this.el.hasAttribute('aria-labelledby')) return;\n\n const labelIds = (Array.from(this.internals.labels) as HTMLLabelElement[]).map(label => {\n if (!label.id) label.id = `ds-switch-label-${++generatedLabelId}`;\n return label.id;\n });\n if (labelIds.length) this.el.setAttribute('aria-labelledby', labelIds.join(' '));\n }\n\n private handleClick = () => {\n if (this.presentation || this.readOnly || this.isInactive || this.disabled || this.formDisabled) return;\n this.checked = !this.checked;\n this.dsChange.emit(this.checked);\n };\n\n private handleKeyDown = (event: KeyboardEvent) => {\n if (this.presentation || this.readOnly || this.isInactive || this.disabled || this.formDisabled) return;\n if (event.key === ' ' || event.key === 'Enter') {\n event.preventDefault();\n this.handleClick();\n }\n };\n\n private handleFocus = () => {\n if (!this.presentation) this.focused = true;\n };\n\n private handleBlur = () => {\n if (this.presentation) return;\n this.focused = false;\n this.touched = true;\n };\n\n render() {\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n const invalid = this.required && !inactive && !this.checked;\n const dirty = this.checked !== this.initialChecked;\n // Switch owns its structural track and thumb strokes so nested interaction layers cannot cover them.\n return (\n <Host\n role={this.presentation ? undefined : 'switch'}\n aria-checked={this.presentation ? undefined : String(this.checked)}\n aria-disabled={!this.presentation && inactive ? 'true' : undefined}\n aria-readonly={!this.presentation && this.readOnly ? 'true' : undefined}\n aria-required={!this.presentation && this.required ? 'true' : undefined}\n aria-invalid={!this.presentation && invalid ? 'true' : undefined}\n aria-hidden={this.presentation ? 'true' : undefined}\n data-checked={this.checked ? '' : undefined}\n data-unchecked={!this.checked ? '' : undefined}\n data-disabled={inactive ? '' : undefined}\n data-readonly={this.readOnly ? '' : undefined}\n data-required={this.required ? '' : undefined}\n data-focused={!this.presentation && this.focused ? '' : undefined}\n data-filled={!this.presentation && this.checked ? '' : undefined}\n data-dirty={!this.presentation && dirty ? '' : undefined}\n data-touched={!this.presentation && this.touched ? '' : undefined}\n data-valid={!this.presentation && !inactive && !invalid ? '' : undefined}\n data-invalid={!this.presentation && invalid ? '' : undefined}\n tabIndex={this.presentation || inactive ? -1 : 0}\n class={{\n switch: true,\n checked: this.checked,\n 'switch--md': this.size === 'md',\n 'switch--sm': this.size === 'sm',\n 'switch--xs': this.size === 'xs',\n 'switch--readonly': this.readOnly,\n 'switch--presentation': this.presentation,\n 'ds-focus-ring': true,\n 'ds-control-inactive': inactive,\n }}\n onClick={this.handleClick}\n onKeyDown={this.handleKeyDown}\n onFocus={this.handleFocus}\n onBlur={this.handleBlur}\n >\n <span class=\"thumb\" />\n </Host>\n );\n }\n}\n",
357
+ "content": "import { AttachInternals, Component, Element, Prop, State, Event, EventEmitter, Watch, h, Host } from '@stencil/core';\nimport { DEFAULT_REQUIRED_MESSAGE, setRequiredValidity } from '../../utils';\n\nexport type SwitchSize = 'md' | 'sm' | 'xs';\n\nlet generatedLabelId = 0;\n\n@Component({\n tag: 'ds-switch',\n styleUrl: 'Switch.css',\n shadow: true,\n formAssociated: true,\n})\nexport class Switch {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n /** Current on/off state. The initial value is restored when the owning form resets. */\n @Prop({ mutable: true }) checked: boolean = false;\n\n @Prop({ reflect: true }) name: string | undefined;\n @Prop() value: string = 'on';\n\n /** Optional value submitted when unchecked; omitted by default like a native checkbox. */\n @Prop() uncheckedValue: string | undefined;\n\n /** Associates the switch with a form by id when it is rendered outside that form. */\n @Prop({ reflect: true }) form: string | undefined;\n\n @Prop({ reflect: true }) disabled: boolean = false;\n @Prop({ reflect: true }) readOnly: boolean = false;\n @Prop({ reflect: true }) required: boolean = false;\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n @Prop() isInactive: boolean = false;\n\n /** Compact track size for placement inside controls, menus, and form rows. */\n @Prop() size: SwitchSize = 'md';\n\n /**\n * Removes standalone control semantics so a composite owner such as a\n * `menuitemcheckbox` can use the switch as an aria-hidden visual indicator.\n */\n @Prop() presentation: boolean = false;\n\n @Event() dsChange!: EventEmitter<boolean>;\n\n private initialChecked = false;\n @State() private formDisabled = false;\n @State() private focused = false;\n @State() private touched = false;\n\n componentWillLoad() {\n this.initialChecked = this.checked;\n this.syncFormValue();\n }\n\n componentDidLoad() {\n this.syncNativeLabels();\n }\n\n @Watch('checked')\n @Watch('value')\n @Watch('uncheckedValue')\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('presentation')\n @Watch('required')\n syncFormValue() {\n const inactive = this.presentation || this.isInactive || this.disabled || this.formDisabled;\n const submissionValue = this.checked ? this.value : this.uncheckedValue ?? null;\n const state = this.checked ? 'checked' : 'unchecked';\n this.internals.setFormValue(inactive ? null : submissionValue, state);\n\n const missing = this.required && !inactive && !this.checked;\n setRequiredValidity(this.internals, missing, this.requiredMessage);\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.checked = this.initialChecked;\n this.focused = false;\n this.touched = false;\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n this.checked = state === 'checked';\n }\n\n private syncNativeLabels() {\n if (this.presentation) return;\n if (this.el.hasAttribute('aria-label') || this.el.hasAttribute('aria-labelledby')) return;\n\n const labelIds = (Array.from(this.internals.labels) as HTMLLabelElement[]).map(label => {\n if (!label.id) label.id = `ds-switch-label-${++generatedLabelId}`;\n return label.id;\n });\n if (labelIds.length) this.el.setAttribute('aria-labelledby', labelIds.join(' '));\n }\n\n private handleClick = () => {\n if (this.presentation || this.readOnly || this.isInactive || this.disabled || this.formDisabled) return;\n this.checked = !this.checked;\n this.dsChange.emit(this.checked);\n };\n\n private handleKeyDown = (event: KeyboardEvent) => {\n if (this.presentation || this.readOnly || this.isInactive || this.disabled || this.formDisabled) return;\n if (event.key === ' ' || event.key === 'Enter') {\n event.preventDefault();\n this.handleClick();\n }\n };\n\n private handleFocus = () => {\n if (!this.presentation) this.focused = true;\n };\n\n private handleBlur = () => {\n if (this.presentation) return;\n this.focused = false;\n this.touched = true;\n };\n\n render() {\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n const invalid = this.required && !inactive && !this.checked;\n const dirty = this.checked !== this.initialChecked;\n // Switch owns its structural track and thumb strokes so nested interaction layers cannot cover them.\n return (\n <Host\n role={this.presentation ? undefined : 'switch'}\n aria-checked={this.presentation ? undefined : String(this.checked)}\n aria-disabled={!this.presentation && inactive ? 'true' : undefined}\n aria-readonly={!this.presentation && this.readOnly ? 'true' : undefined}\n aria-required={!this.presentation && this.required ? 'true' : undefined}\n aria-invalid={!this.presentation && invalid ? 'true' : undefined}\n aria-hidden={this.presentation ? 'true' : undefined}\n data-checked={this.checked ? '' : undefined}\n data-unchecked={!this.checked ? '' : undefined}\n data-disabled={inactive ? '' : undefined}\n data-readonly={this.readOnly ? '' : undefined}\n data-required={this.required ? '' : undefined}\n data-focused={!this.presentation && this.focused ? '' : undefined}\n data-filled={!this.presentation && this.checked ? '' : undefined}\n data-dirty={!this.presentation && dirty ? '' : undefined}\n data-touched={!this.presentation && this.touched ? '' : undefined}\n data-valid={!this.presentation && !inactive && !invalid ? '' : undefined}\n data-invalid={!this.presentation && invalid ? '' : undefined}\n tabIndex={this.presentation || inactive ? -1 : 0}\n class={{\n switch: true,\n checked: this.checked,\n 'switch--md': this.size === 'md',\n 'switch--sm': this.size === 'sm',\n 'switch--xs': this.size === 'xs',\n 'switch--readonly': this.readOnly,\n 'switch--presentation': this.presentation,\n 'ds-focus-ring': true,\n 'ds-control-inactive': inactive,\n }}\n onClick={this.handleClick}\n onKeyDown={this.handleKeyDown}\n onFocus={this.handleFocus}\n onBlur={this.handleBlur}\n >\n <span class=\"thumb\" />\n </Host>\n );\n }\n}\n",
358
358
  "type": "registry:ui"
359
359
  }
360
360
  ]
@@ -325,12 +325,12 @@
325
325
  "files": [
326
326
  {
327
327
  "path": "src/wc/components/Toast/Toast.css",
328
- "content": ":host {\n display: contents;\n}\n\n.toast-region {\n position: fixed;\n inset: 0;\n z-index: var(--dimension-z-index-floating);\n pointer-events: none;\n outline: none;\n}\n\n.toast-viewport {\n position: absolute;\n right: var(--dimension-space-200);\n bottom: var(--dimension-space-200);\n display: grid;\n width: min(\n var(--dimension-card-width-sm),\n calc(100vw - 2 * var(--dimension-space-200))\n );\n}\n\n@media (max-width: 767px) {\n :host([avoid-mobile-shell-bar]) .toast-viewport {\n bottom: calc(\n var(--dimension-space-200) +\n var(--dimension-stroke-width-012) +\n var(--dimension-space-050) +\n var(--dimension-size-600) +\n max(var(--dimension-space-050), env(safe-area-inset-bottom))\n );\n }\n}\n\n.toast-positioner {\n grid-area: 1 / 1;\n align-self: end;\n justify-self: center;\n width: 100%;\n pointer-events: auto;\n transform: translateY(\n calc(-1 * var(--toast-index) * var(--dimension-space-100))\n );\n transition:\n transform var(--effect-motion-short-3),\n width var(--effect-motion-short-3);\n}\n\n.toast-viewport:not([data-expanded]) .toast-positioner {\n width: calc(\n 100% - 2 * var(--toast-index) * var(--dimension-space-100)\n );\n}\n\n.toast-viewport[data-expanded] .toast-positioner {\n transform: translateY(calc(-1 * var(--toast-offset-y)));\n}\n\n.toast-positioner[data-limited] {\n visibility: hidden;\n pointer-events: none;\n}\n\n.toast-positioner--anchored {\n position: fixed;\n width: min(\n var(--dimension-card-width-sm),\n calc(100vw - 2 * var(--dimension-space-200))\n );\n transform: none;\n}\n\n.toast-positioner--anchored[data-positioned='false'] {\n visibility: hidden;\n}\n\n.toast-surface {\n position: relative;\n display: block;\n box-sizing: border-box;\n width: 100%;\n height: var(--toast-height, auto);\n padding: var(--dimension-space-200);\n overflow: hidden;\n color: var(--color-foreground-primary);\n background-color: var(--color-background-primary);\n border-radius: var(--dimension-radius-050);\n box-shadow: var(--effect-elevation-elevated-floating);\n transform: translate(\n var(--toast-swipe-movement-x),\n var(--toast-swipe-movement-y)\n );\n animation: toast-enter var(--effect-motion-short-2) both;\n}\n\n.toast-positioner--ending .toast-surface {\n animation: toast-exit var(--effect-motion-short-3) both;\n}\n\n.toast-surface--swiping {\n transition: none;\n user-select: none;\n}\n\n.toast-content {\n display: flex;\n flex-direction: column;\n min-width: 0;\n padding-inline-end: var(--dimension-size-500);\n transition: opacity var(--effect-motion-short-3);\n}\n\n.toast-copy {\n display: flex;\n flex-direction: column;\n min-width: 0;\n min-height: var(--typography-lineheight-md);\n margin-block: calc(-1 * var(--dimension-space-025));\n gap: var(--dimension-space-050);\n}\n\n.toast-title,\n.toast-description {\n min-width: 0;\n}\n\n.toast-action {\n align-self: start;\n margin-top: calc(\n var(--dimension-space-200) + var(--dimension-space-025)\n );\n}\n\n.toast-close {\n position: absolute;\n top: var(--dimension-space-100);\n right: var(--dimension-space-100);\n transition: opacity var(--effect-motion-short-3);\n}\n\n.toast-viewport:not([data-expanded]) .toast-positioner[data-behind] .toast-content,\n.toast-viewport:not([data-expanded]) .toast-positioner[data-behind] .toast-close {\n opacity: 0;\n}\n\n.toast-assertive-announcer {\n position: absolute;\n width: var(--dimension-size-100);\n height: var(--dimension-size-100);\n padding: 0;\n overflow: hidden;\n white-space: nowrap;\n clip-path: inset(50%);\n}\n\n@keyframes toast-enter {\n from {\n opacity: 0;\n transform: translateY(var(--dimension-space-100));\n }\n\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes toast-exit {\n from {\n opacity: 1;\n }\n\n to {\n opacity: 0;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .toast-positioner,\n .toast-surface,\n .toast-content,\n .toast-close,\n .toast-positioner--ending .toast-surface {\n transition: none;\n animation: none;\n }\n\n .toast-positioner--ending .toast-surface {\n opacity: 0;\n }\n}\n",
328
+ "content": "@import '../../utils/visually-hidden.css';\n\n:host {\n display: contents;\n}\n\n.toast-region {\n position: fixed;\n inset: 0;\n z-index: var(--dimension-z-index-floating);\n pointer-events: none;\n outline: none;\n}\n\n.toast-viewport {\n position: absolute;\n right: var(--dimension-space-200);\n bottom: var(--dimension-space-200);\n display: grid;\n width: min(\n var(--dimension-card-width-sm),\n calc(100vw - 2 * var(--dimension-space-200))\n );\n}\n\n@media (max-width: 767px) {\n :host([avoid-mobile-shell-bar]) .toast-viewport {\n bottom: calc(\n var(--dimension-space-200) +\n var(--dimension-stroke-width-012) +\n var(--dimension-space-200) +\n var(--dimension-size-500) +\n max(var(--dimension-space-200), env(safe-area-inset-bottom))\n );\n }\n}\n\n.toast-positioner {\n grid-area: 1 / 1;\n align-self: end;\n justify-self: center;\n width: 100%;\n pointer-events: auto;\n transform: translateY(\n calc(-1 * var(--toast-index) * var(--dimension-space-100))\n );\n transition:\n transform var(--effect-motion-short-3),\n width var(--effect-motion-short-3);\n}\n\n.toast-viewport:not([data-expanded]) .toast-positioner {\n width: calc(\n 100% - 2 * var(--toast-index) * var(--dimension-space-100)\n );\n}\n\n.toast-viewport[data-expanded] .toast-positioner {\n transform: translateY(calc(-1 * var(--toast-offset-y)));\n}\n\n.toast-positioner[data-limited] {\n visibility: hidden;\n pointer-events: none;\n}\n\n.toast-positioner--anchored {\n position: fixed;\n width: min(\n var(--dimension-card-width-sm),\n calc(100vw - 2 * var(--dimension-space-200))\n );\n transform: none;\n}\n\n.toast-positioner--anchored[data-positioned='false'] {\n visibility: hidden;\n}\n\n.toast-surface {\n position: relative;\n display: block;\n box-sizing: border-box;\n width: 100%;\n height: var(--toast-height, auto);\n padding: var(--dimension-space-200);\n overflow: hidden;\n color: var(--color-foreground-primary);\n background-color: var(--color-background-primary);\n border-radius: var(--dimension-radius-050);\n box-shadow: var(--effect-elevation-elevated-floating);\n transform: translate(\n var(--toast-swipe-movement-x),\n var(--toast-swipe-movement-y)\n );\n animation: toast-enter var(--effect-motion-short-2) both;\n}\n\n.toast-positioner--ending .toast-surface {\n animation: toast-exit var(--effect-motion-short-3) both;\n}\n\n.toast-surface--swiping {\n transition: none;\n user-select: none;\n}\n\n.toast-content {\n display: flex;\n flex-direction: column;\n min-width: 0;\n padding-inline-end: var(--dimension-size-500);\n transition: opacity var(--effect-motion-short-3);\n}\n\n.toast-copy {\n display: flex;\n flex-direction: column;\n min-width: 0;\n min-height: var(--typography-lineheight-md);\n margin-block: calc(-1 * var(--dimension-space-025));\n gap: var(--dimension-space-050);\n}\n\n.toast-title,\n.toast-description {\n min-width: 0;\n}\n\n.toast-action {\n align-self: start;\n margin-top: calc(\n var(--dimension-space-200) + var(--dimension-space-025)\n );\n}\n\n.toast-close {\n position: absolute;\n top: var(--dimension-space-100);\n right: var(--dimension-space-100);\n transition: opacity var(--effect-motion-short-3);\n}\n\n.toast-viewport:not([data-expanded]) .toast-positioner[data-behind] .toast-content,\n.toast-viewport:not([data-expanded]) .toast-positioner[data-behind] .toast-close {\n opacity: 0;\n}\n\n\n@keyframes toast-enter {\n from {\n opacity: 0;\n transform: translateY(var(--dimension-space-100));\n }\n\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes toast-exit {\n from {\n opacity: 1;\n }\n\n to {\n opacity: 0;\n }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .toast-positioner,\n .toast-surface,\n .toast-content,\n .toast-close,\n .toast-positioner--ending .toast-surface {\n transition: none;\n animation: none;\n }\n\n .toast-positioner--ending .toast-surface {\n opacity: 0;\n }\n}\n",
329
329
  "type": "registry:ui"
330
330
  },
331
331
  {
332
332
  "path": "src/wc/components/Toast/Toast.tsx",
333
- "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 {\n resolveCssLengthPx,\n resolveCssTimeMs,\n resolveMotionTimeMs,\n TOKEN_DEFAULTS,\n} from '../../utils';\nimport {\n toastManager as defaultToastManager,\n type ToastActionEventDetail,\n type ToastCloseContext,\n type ToastCloseEventDetail,\n type ToastEventDetail,\n type ToastManager,\n type ToastRecord,\n type ToastSwipeDirection,\n} from '../../toast';\nimport { computeTooltipPosition } from '../Tooltip/tooltip-position';\n\ninterface ToastTimer {\n timerKey: number;\n remaining: number;\n startedAt: number;\n handle: ReturnType<typeof setTimeout> | null;\n}\n\ninterface SwipeState {\n id: string;\n x: number;\n y: number;\n direction?: ToastSwipeDirection;\n}\n\ninterface AnchoredPosition {\n x: number;\n y: number;\n side: 'top' | 'right' | 'bottom' | 'left';\n}\n\ninterface ActiveSwipe {\n id: string;\n pointerId: number;\n pointerType: string;\n startX: number;\n startY: number;\n maxDistance: number;\n cancelled: boolean;\n axis: 'x' | 'y' | null;\n}\n\nconst SWIPE_THRESHOLD = 40;\nconst SWIPE_REVERSE_CANCEL_THRESHOLD = 10;\nconst GLOBAL_STACK_HOVER = '__global-stack__';\nconst INTERACTIVE_SWIPE_SELECTOR =\n 'button,a,input,textarea,select,[role=\"button\"],[data-ds-toast-swipe-ignore]';\n\nconst attachedManagers = new WeakMap<ToastManager, Toast>();\nconst connectedToastRegions = new Set<Toast>();\nlet activeToastRegion: Toast | null = null;\nlet toastInstanceCounter = 0;\n\n@Component({\n tag: 'ds-toast',\n styleUrl: 'Toast.css',\n scoped: true,\n})\nexport class Toast {\n @Element() el!: HTMLElement;\n\n /** Manager that supplies toast records. Assign as a JavaScript property. */\n @Prop() manager: ToastManager = defaultToastManager;\n /** Maximum visible global-stack records. Limited records remain mounted and inert. */\n @Prop() limit: number = 3;\n /** Default auto-dismiss delay. Use 0 for persistent records. */\n @Prop() timeout: number | string = TOKEN_DEFAULTS.animationDelayLong2;\n /** Accessible label for the notification region. */\n @Prop() label: string = 'Notifications';\n /** Localized accessible label for every toast close action. */\n @Prop() closeLabel: string = 'Close notification';\n /** Keep the global stack 16px above the persistent mobile shell bar below 768px. */\n @Prop({ attribute: 'avoid-mobile-shell-bar', reflect: true })\n avoidMobileShellBar: boolean = false;\n /** Allowed swipe-to-dismiss directions. Assign as a JavaScript property. */\n @Prop() swipeDirections: ToastSwipeDirection[] = ['down', 'right'];\n\n @State() private records: readonly ToastRecord[] = [];\n @State() private expanded = false;\n @State() private keyboardActive = false;\n @State() private swipe: SwipeState | null = null;\n @State() private layoutVersion = 0;\n\n @Event() dsToastClose!: EventEmitter<ToastCloseEventDetail>;\n @Event() dsToastRemove!: EventEmitter<ToastEventDetail>;\n @Event() dsToastAction!: EventEmitter<ToastActionEventDetail>;\n\n private readonly instanceId = ++toastInstanceCounter;\n private readonly domIds = new Map<string, number>();\n private readonly timers = new Map<string, ToastTimer>();\n private readonly closeWatchdogs = new Map<string, ReturnType<typeof setTimeout>>();\n private readonly closeFrames = new Map<string, number>();\n private readonly closeEvents = new Set<string>();\n private readonly activationFrames = new Map<string, number>();\n private readonly pauseReasons = new Set<string>();\n private readonly hoveredIds = new Set<string>();\n private readonly heights = new Map<string, number>();\n private readonly measuredUpdateKeys = new Map<string, number>();\n private readonly anchoredPositions = new Map<string, AnchoredPosition>();\n private readonly dismissedSwipeDirections = new Map<string, ToastSwipeDirection>();\n private readonly observedElements = new Set<Element>();\n private readonly pendingRemovals: Array<{\n manager: ToastManager;\n context: ToastCloseContext;\n }> = [];\n\n private unsubscribe: (() => void) | null = null;\n private attachedManager: ToastManager | null = null;\n private previousFocus: HTMLElement | null = null;\n private resizeObserver: ResizeObserver | null = null;\n private mutationObserver: MutationObserver | null = null;\n private layoutFrame: number | null = null;\n private activeSwipe: ActiveSwipe | null = null;\n private nextDomId = 0;\n private connected = false;\n private pendingHoverLeave = false;\n\n connectedCallback() {\n this.connect();\n }\n\n componentDidLoad() {\n this.connect();\n }\n\n private connect() {\n if (this.connected) return;\n this.connected = true;\n this.resizeObserver ??= new ResizeObserver(() => this.scheduleLayout());\n this.mutationObserver ??= new MutationObserver(() => this.scheduleLayout());\n this.mutationObserver.observe(document.body, { childList: true, subtree: true });\n if (!this.unsubscribe) this.attachManager(this.manager);\n window.addEventListener('blur', this.handleWindowBlur);\n window.addEventListener('focus', this.handleWindowFocus);\n window.addEventListener('resize', this.handleWindowResize);\n window.addEventListener('scroll', this.scheduleLayout, true);\n document.addEventListener('keydown', this.handleDocumentKeyDown);\n document.addEventListener('visibilitychange', this.handleVisibilityChange);\n connectedToastRegions.add(this);\n // eslint-disable-next-line @typescript-eslint/no-this-alias -- module coordinator tracks the active connected viewport\n if (!activeToastRegion) activeToastRegion = this;\n }\n\n componentDidRender() {\n this.syncObservedElements();\n this.flushPendingRemovals();\n this.scheduleLayout();\n }\n\n disconnectedCallback() {\n this.connected = false;\n this.flushPendingRemovals();\n this.detachManager();\n this.resetManagerState();\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n this.mutationObserver?.disconnect();\n this.mutationObserver = null;\n this.observedElements.clear();\n if (this.layoutFrame !== null) cancelAnimationFrame(this.layoutFrame);\n this.layoutFrame = null;\n window.removeEventListener('blur', this.handleWindowBlur);\n window.removeEventListener('focus', this.handleWindowFocus);\n window.removeEventListener('resize', this.handleWindowResize);\n window.removeEventListener('scroll', this.scheduleLayout, true);\n document.removeEventListener('keydown', this.handleDocumentKeyDown);\n document.removeEventListener('visibilitychange', this.handleVisibilityChange);\n connectedToastRegions.delete(this);\n if (activeToastRegion === this) {\n const remaining = Array.from(connectedToastRegions);\n activeToastRegion = remaining[remaining.length - 1] ?? null;\n }\n }\n\n @Watch('manager')\n onManagerChange(next: ToastManager) {\n this.detachManager();\n this.resetManagerState();\n if (this.connected) this.attachManager(next);\n }\n\n @Watch('timeout')\n onTimeoutChange() {\n for (const timer of this.timers.values()) {\n if (timer.handle) clearTimeout(timer.handle);\n }\n this.timers.clear();\n this.syncTimers();\n }\n\n private attachManager(manager: ToastManager) {\n const attached = attachedManagers.get(manager);\n if (attached && attached !== this) {\n console.warn('[ds-toast] A manager can control only one connected ds-toast instance.');\n return;\n }\n attachedManagers.set(manager, this);\n this.attachedManager = manager;\n this.unsubscribe = manager.subscribe(records => {\n this.records = records;\n this.syncLifecycle(records);\n this.scheduleLayout();\n });\n }\n\n private detachManager() {\n this.unsubscribe?.();\n this.unsubscribe = null;\n if (this.attachedManager && attachedManagers.get(this.attachedManager) === this) {\n attachedManagers.delete(this.attachedManager);\n }\n this.attachedManager = null;\n }\n\n private resetManagerState() {\n this.flushPendingRemovals();\n if (this.el.contains(document.activeElement)) this.restorePreviousFocus();\n this.clearAllTimers();\n for (const frame of this.activationFrames.values()) cancelAnimationFrame(frame);\n for (const frame of this.closeFrames.values()) cancelAnimationFrame(frame);\n this.activationFrames.clear();\n this.closeFrames.clear();\n this.closeEvents.clear();\n this.pauseReasons.clear();\n this.hoveredIds.clear();\n this.pendingHoverLeave = false;\n this.heights.clear();\n this.measuredUpdateKeys.clear();\n this.anchoredPositions.clear();\n this.dismissedSwipeDirections.clear();\n this.domIds.clear();\n this.activeSwipe = null;\n this.swipe = null;\n this.expanded = false;\n this.keyboardActive = false;\n this.records = [];\n this.resizeObserver?.disconnect();\n this.observedElements.clear();\n }\n\n private syncObservedElements() {\n const next = new Set<Element>(\n this.el.querySelectorAll<HTMLElement>('.toast-positioner'),\n );\n for (const record of this.records) {\n if (!record.positioner) continue;\n const anchor = this.resolveAnchor(record.positioner.anchor);\n if (anchor) next.add(anchor);\n }\n\n for (const element of this.observedElements) {\n if (!next.has(element)) {\n this.resizeObserver?.unobserve(element);\n this.observedElements.delete(element);\n }\n }\n for (const element of next) {\n if (this.observedElements.has(element)) continue;\n this.resizeObserver?.observe(element);\n this.observedElements.add(element);\n }\n }\n\n private flushPendingRemovals() {\n for (const pending of this.pendingRemovals.splice(0)) {\n pending.manager.notifyRemove(pending.context);\n this.dsToastRemove.emit({\n id: pending.context.id,\n toast: pending.context.toast,\n });\n }\n }\n\n private syncLifecycle(records: readonly ToastRecord[]) {\n const recordIds = new Set(records.map(record => record.id));\n for (const id of this.hoveredIds) {\n if (id !== GLOBAL_STACK_HOVER && !recordIds.has(id)) this.hoveredIds.delete(id);\n }\n const hasEndingRecords = records.some(\n record => record.transitionStatus === 'ending',\n );\n if (this.pendingHoverLeave && !hasEndingRecords && !this.hoveredIds.size) {\n this.pendingHoverLeave = false;\n this.expanded = false;\n this.resumeTimers('hover');\n } else if (!this.hoveredIds.size && !this.pendingHoverLeave) {\n this.resumeTimers('hover');\n }\n\n for (const [id, timer] of this.timers) {\n if (recordIds.has(id)) continue;\n if (timer.handle) clearTimeout(timer.handle);\n this.timers.delete(id);\n }\n\n for (const [id, watchdog] of this.closeWatchdogs) {\n if (recordIds.has(id)) continue;\n clearTimeout(watchdog);\n this.closeWatchdogs.delete(id);\n }\n\n for (const [id, frame] of this.activationFrames) {\n if (recordIds.has(id)) continue;\n cancelAnimationFrame(frame);\n this.activationFrames.delete(id);\n }\n\n for (const [id, frame] of this.closeFrames) {\n if (recordIds.has(id)) continue;\n cancelAnimationFrame(frame);\n this.closeFrames.delete(id);\n }\n\n for (const id of this.domIds.keys()) {\n if (recordIds.has(id)) continue;\n this.closeEvents.delete(id);\n this.domIds.delete(id);\n this.heights.delete(id);\n this.measuredUpdateKeys.delete(id);\n this.anchoredPositions.delete(id);\n this.dismissedSwipeDirections.delete(id);\n }\n\n for (const record of records) {\n this.ensureDomId(record.id);\n if (\n record.transitionStatus === 'starting' &&\n !this.activationFrames.has(record.id)\n ) {\n const manager = this.attachedManager;\n const frame = requestAnimationFrame(() => {\n this.activationFrames.delete(record.id);\n manager?.activate(record.id);\n });\n this.activationFrames.set(record.id, frame);\n }\n\n if (record.transitionStatus === 'ending') {\n this.clearToastTimer(record.id);\n this.beginRemoval(record);\n } else {\n const watchdog = this.closeWatchdogs.get(record.id);\n if (watchdog) clearTimeout(watchdog);\n this.closeWatchdogs.delete(record.id);\n const closeFrame = this.closeFrames.get(record.id);\n if (closeFrame) cancelAnimationFrame(closeFrame);\n this.closeFrames.delete(record.id);\n this.closeEvents.delete(record.id);\n }\n }\n\n this.syncTimers();\n }\n\n private syncTimers() {\n const activeIds = new Set<string>();\n for (const record of this.records) {\n if (record.transitionStatus !== 'active') continue;\n activeIds.add(record.id);\n const duration = this.resolveToastTimeout(record);\n if (duration <= 0) {\n this.clearToastTimer(record.id);\n continue;\n }\n\n const existing = this.timers.get(record.id);\n if (!existing || existing.timerKey !== record.timerKey) {\n this.clearToastTimer(record.id);\n this.timers.set(record.id, {\n timerKey: record.timerKey,\n remaining: duration,\n startedAt: 0,\n handle: null,\n });\n }\n if (!this.isPaused) this.startToastTimer(record.id);\n }\n\n for (const id of this.timers.keys()) {\n if (!activeIds.has(id)) this.clearToastTimer(id);\n }\n }\n\n private resolveToastTimeout(record: ToastRecord): number {\n const value = record.timeout ?? this.timeout;\n if (typeof value === 'number') return Math.max(0, value);\n return resolveCssTimeMs(value, TOKEN_DEFAULTS.animationDelayLong2);\n }\n\n private startToastTimer(id: string) {\n const timer = this.timers.get(id);\n if (!timer || timer.handle || timer.remaining <= 0) return;\n timer.startedAt = Date.now();\n timer.handle = setTimeout(() => {\n timer.handle = null;\n timer.remaining = 0;\n this.manager.close(id, 'timeout');\n }, timer.remaining);\n }\n\n private pauseTimers(reason: string) {\n const wasPaused = this.isPaused;\n this.pauseReasons.add(reason);\n if (wasPaused) return;\n const now = Date.now();\n for (const timer of this.timers.values()) {\n if (!timer.handle) continue;\n clearTimeout(timer.handle);\n timer.handle = null;\n timer.remaining = Math.max(0, timer.remaining - (now - timer.startedAt));\n }\n }\n\n private resumeTimers(reason: string) {\n this.pauseReasons.delete(reason);\n if (this.isPaused) return;\n for (const id of this.timers.keys()) this.startToastTimer(id);\n }\n\n private get isPaused(): boolean {\n return this.pauseReasons.size > 0;\n }\n\n private clearToastTimer(id: string) {\n const timer = this.timers.get(id);\n if (timer?.handle) clearTimeout(timer.handle);\n this.timers.delete(id);\n }\n\n private clearAllTimers() {\n for (const timer of this.timers.values()) {\n if (timer.handle) clearTimeout(timer.handle);\n }\n for (const watchdog of this.closeWatchdogs.values()) clearTimeout(watchdog);\n this.timers.clear();\n this.closeWatchdogs.clear();\n }\n\n private beginRemoval(record: ToastRecord) {\n if (!this.closeEvents.has(record.id)) {\n this.closeEvents.add(record.id);\n this.dsToastClose.emit({\n id: record.id,\n reason: record.closeReason ?? 'programmatic',\n toast: record,\n });\n this.moveFocusAfterClose(record.id);\n }\n if (\n this.closeWatchdogs.has(record.id) ||\n this.closeFrames.has(record.id)\n ) return;\n\n const duration = resolveMotionTimeMs(\n TOKEN_DEFAULTS.motionShort3,\n TOKEN_DEFAULTS.animationDurationShort3,\n );\n if (duration <= 0) {\n queueMicrotask(() => this.completeRemoval(record));\n return;\n }\n const frame = requestAnimationFrame(() => {\n this.closeFrames.delete(record.id);\n const current = this.records.find(candidate => candidate.id === record.id);\n if (!current || current.transitionStatus !== 'ending') return;\n this.closeWatchdogs.set(\n record.id,\n setTimeout(() => this.completeRemoval(current), duration),\n );\n });\n this.closeFrames.set(record.id, frame);\n }\n\n private completeRemoval(record: ToastRecord) {\n const current = this.records.find(candidate => candidate.id === record.id);\n if (!current || current.transitionStatus !== 'ending') return;\n const watchdog = this.closeWatchdogs.get(record.id);\n if (watchdog) clearTimeout(watchdog);\n this.closeWatchdogs.delete(record.id);\n const closeFrame = this.closeFrames.get(record.id);\n if (closeFrame) cancelAnimationFrame(closeFrame);\n this.closeFrames.delete(record.id);\n const manager = this.attachedManager;\n const context = manager?.remove(record.id);\n if (manager && context) this.pendingRemovals.push({ manager, context });\n }\n\n private handleAnimationComplete(record: ToastRecord) {\n if (record.transitionStatus === 'ending') this.completeRemoval(record);\n }\n\n private bindAnimationCancel(element: HTMLDivElement | undefined, id: string) {\n if (!element || element.dataset['cancelBound'] === 'true') return;\n element.dataset['cancelBound'] = 'true';\n element.addEventListener('animationcancel', () => {\n const record = this.records.find(candidate => candidate.id === id);\n if (record) this.handleAnimationComplete(record);\n });\n }\n\n private handleWindowBlur = () => this.pauseTimers('window');\n private handleWindowFocus = () => this.resumeTimers('window');\n private handleVisibilityChange = () => {\n if (document.hidden) this.pauseTimers('document');\n else this.resumeTimers('document');\n };\n\n private handlePointerEnter(id: string) {\n // eslint-disable-next-line @typescript-eslint/no-this-alias -- pointer interaction selects the viewport that receives F6\n activeToastRegion = this;\n this.pendingHoverLeave = false;\n this.hoveredIds.add(id);\n this.expanded = true;\n this.pauseTimers('hover');\n }\n\n private handlePointerLeave(id: string) {\n this.hoveredIds.delete(id);\n if (this.hoveredIds.size) return;\n if (this.records.some(record => record.transitionStatus === 'ending')) {\n this.pendingHoverLeave = true;\n return;\n }\n this.expanded = false;\n this.resumeTimers('hover');\n }\n\n private handleFocusIn = () => {\n const active = document.activeElement;\n if (!(active instanceof HTMLElement) || !active.matches(':focus-visible')) return;\n // eslint-disable-next-line @typescript-eslint/no-this-alias -- module coordinator routes F6 to the last focused viewport\n activeToastRegion = this;\n this.keyboardActive = true;\n this.expanded = true;\n this.pauseTimers('focus');\n };\n\n private handleFocusOut = (event: FocusEvent) => {\n const next = event.relatedTarget;\n if (next instanceof Node && this.el.contains(next)) return;\n if (!this.keyboardActive) return;\n this.keyboardActive = false;\n this.expanded = this.hoveredIds.size > 0;\n this.resumeTimers('focus');\n };\n\n private handleDocumentKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'F6') return;\n const eligible = Array.from(connectedToastRegions).filter(\n region => region.activeRecords.length > 0,\n );\n const target =\n activeToastRegion && activeToastRegion.activeRecords.length\n ? activeToastRegion\n : eligible[eligible.length - 1];\n if (target !== this) return;\n event.preventDefault();\n if (!this.el.contains(document.activeElement)) {\n this.previousFocus = document.activeElement as HTMLElement | null;\n }\n this.keyboardActive = true;\n this.regionElement?.focus();\n };\n\n private handleRegionKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n const target = event.target as HTMLElement;\n const positioner = target.closest<HTMLElement>('.toast-positioner');\n const id = positioner?.dataset['toastId'] ?? this.activeRecords[0]?.id;\n if (id) {\n event.preventDefault();\n this.manager.close(id, 'escape');\n }\n return;\n }\n\n if (event.key === 'Tab' && event.shiftKey && event.target === this.regionElement) {\n event.preventDefault();\n this.restorePreviousFocus();\n }\n };\n\n private get regionElement(): HTMLElement | null {\n return this.el.querySelector<HTMLElement>('.toast-region');\n }\n\n private restorePreviousFocus() {\n this.previousFocus?.focus?.();\n this.previousFocus = null;\n this.keyboardActive = false;\n }\n\n private moveFocusAfterClose(id: string) {\n const active = document.activeElement;\n const closing = this.positionerElement(id);\n if (!active || !closing?.contains(active)) return;\n requestAnimationFrame(() => {\n const next = this.activeRecords.find(record => record.id !== id);\n const control = next\n ? this.positionerElement(next.id)?.querySelector<\n HTMLElement & { setFocus?: () => Promise<void> }\n >('ds-button-unfilled')\n : null;\n if (control?.setFocus) void control.setFocus();\n else if (next) this.regionElement?.focus();\n else this.restorePreviousFocus();\n });\n }\n\n private handleAction(record: ToastRecord, event: CustomEvent<MouseEvent>) {\n const detail: ToastActionEventDetail = {\n id: record.id,\n toast: record,\n originalEvent: event.detail,\n };\n this.dsToastAction.emit(detail);\n record.action?.onAction?.({\n id: record.id,\n data: record.data,\n manager: this.manager,\n originalEvent: event.detail,\n });\n }\n\n private handlePointerDown(record: ToastRecord, event: PointerEvent) {\n if (event.button !== 0) return;\n const target = event.target as Element;\n if (target.closest(INTERACTIVE_SWIPE_SELECTOR)) return;\n\n const surface = event.currentTarget as HTMLElement;\n surface.setPointerCapture(event.pointerId);\n this.activeSwipe = {\n id: record.id,\n pointerId: event.pointerId,\n pointerType: event.pointerType,\n startX: event.clientX,\n startY: event.clientY,\n maxDistance: 0,\n cancelled: false,\n axis: null,\n };\n this.swipe = { id: record.id, x: 0, y: 0 };\n if (event.pointerType === 'touch') this.pauseTimers('swipe');\n }\n\n private handlePointerMove(event: PointerEvent) {\n const active = this.activeSwipe;\n if (!active || active.pointerId !== event.pointerId || active.cancelled) return;\n const dx = event.clientX - active.startX;\n const dy = event.clientY - active.startY;\n if (!active.axis && Math.max(Math.abs(dx), Math.abs(dy)) > 1) {\n active.axis = Math.abs(dx) >= Math.abs(dy) ? 'x' : 'y';\n }\n if (!active.axis) return;\n const horizontal = active.axis === 'x';\n const direction: ToastSwipeDirection = horizontal\n ? dx >= 0 ? 'right' : 'left'\n : dy >= 0 ? 'down' : 'up';\n const allowed = this.swipeDirections.includes(direction);\n const rawDistance = horizontal ? Math.abs(dx) : Math.abs(dy);\n const distance = allowed ? rawDistance : Math.sqrt(rawDistance);\n const signed = (horizontal ? dx : dy) < 0 ? -distance : distance;\n const x = horizontal ? signed : 0;\n const y = horizontal ? 0 : signed;\n\n if (active.maxDistance - distance >= SWIPE_REVERSE_CANCEL_THRESHOLD) {\n active.cancelled = true;\n this.cancelSwipe();\n return;\n }\n active.maxDistance = Math.max(active.maxDistance, distance);\n if (rawDistance > 1) event.preventDefault();\n this.swipe = { id: active.id, x, y, direction: allowed ? direction : undefined };\n }\n\n private handlePointerUp(event: PointerEvent) {\n const active = this.activeSwipe;\n if (!active || active.pointerId !== event.pointerId) return;\n const swipe = this.swipe;\n const distance = swipe ? Math.max(Math.abs(swipe.x), Math.abs(swipe.y)) : 0;\n const shouldDismiss =\n !active.cancelled &&\n !!swipe?.direction &&\n distance >= SWIPE_THRESHOLD;\n const id = active.id;\n const wasTouch = active.pointerType === 'touch';\n this.activeSwipe = null;\n this.swipe = null;\n if (wasTouch) this.resumeTimers('swipe');\n if (shouldDismiss && swipe.direction) {\n this.dismissedSwipeDirections.set(id, swipe.direction);\n this.manager.close(id, 'swipe');\n }\n }\n\n private handlePointerCancel(event: PointerEvent) {\n if (this.activeSwipe?.pointerId !== event.pointerId) return;\n this.cancelSwipe();\n }\n\n private cancelSwipe() {\n const wasTouch = this.activeSwipe?.pointerType === 'touch';\n this.activeSwipe = null;\n this.swipe = null;\n if (wasTouch) this.resumeTimers('swipe');\n }\n\n private get touchAction(): string {\n const horizontal =\n this.swipeDirections.includes('left') || this.swipeDirections.includes('right');\n const vertical =\n this.swipeDirections.includes('up') || this.swipeDirections.includes('down');\n if (horizontal && vertical) return 'none';\n if (horizontal) return 'pan-y';\n if (vertical) return 'pan-x';\n return 'auto';\n }\n\n private handleWindowResize = () => {\n this.heights.clear();\n this.measuredUpdateKeys.clear();\n this.layoutVersion += 1;\n this.scheduleLayout();\n };\n\n private scheduleLayout = () => {\n if (this.layoutFrame !== null) return;\n this.layoutFrame = requestAnimationFrame(() => {\n this.layoutFrame = null;\n this.measureLayout();\n });\n };\n\n private measureNaturalHeight(element: HTMLElement, expandedWidth: boolean): number {\n const surface = element.querySelector<HTMLElement>('.toast-surface');\n if (!surface) return 0;\n const previousWidth = element.style.width;\n const previousTransition = element.style.transition;\n const previousHeight = surface.style.height;\n\n if (expandedWidth) {\n element.style.transition = 'none';\n element.style.width = '100%';\n }\n surface.style.height = 'auto';\n const height = surface.getBoundingClientRect().height;\n\n surface.style.height = previousHeight;\n if (expandedWidth) {\n element.style.width = previousWidth;\n element.getBoundingClientRect();\n element.style.transition = previousTransition;\n }\n return height;\n }\n\n private measureLayout() {\n let changed = false;\n for (const record of this.records) {\n const element = this.positionerElement(record.id);\n if (!element) continue;\n let height = this.heights.get(record.id) ?? 0;\n const needsHeightMeasurement =\n height <= 0 || this.measuredUpdateKeys.get(record.id) !== record.updateKey;\n if (needsHeightMeasurement) {\n height = this.measureNaturalHeight(element, !record.positioner);\n if (height > 0) {\n if (this.heights.get(record.id) !== height) changed = true;\n this.heights.set(record.id, height);\n this.measuredUpdateKeys.set(record.id, record.updateKey);\n }\n }\n\n if (!record.positioner) continue;\n const anchor = this.resolveAnchor(record.positioner.anchor);\n if (!anchor || height <= 0) {\n if (this.anchoredPositions.delete(record.id)) changed = true;\n continue;\n }\n const rect = element.getBoundingClientRect();\n const sideOffset = resolveCssLengthPx(\n record.positioner.sideOffset,\n TOKEN_DEFAULTS.space050,\n );\n const alignOffset = resolveCssLengthPx(record.positioner.alignOffset, 0);\n const position = computeTooltipPosition({\n anchorRect: anchor.getBoundingClientRect(),\n popupWidth: rect.width,\n popupHeight: height,\n side: record.positioner.side ?? 'top',\n align: record.positioner.align ?? 'center',\n sideOffsetPx: sideOffset,\n alignOffsetPx: alignOffset,\n viewportPadPx: resolveCssLengthPx(undefined, TOKEN_DEFAULTS.space200),\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n });\n const current = this.anchoredPositions.get(record.id);\n if (\n !current ||\n current.x !== position.x ||\n current.y !== position.y ||\n current.side !== position.resolvedSide\n ) {\n this.anchoredPositions.set(record.id, {\n x: position.x,\n y: position.y,\n side: position.resolvedSide,\n });\n changed = true;\n }\n }\n if (changed) this.layoutVersion += 1;\n }\n\n private resolveAnchor(anchor: HTMLElement | string): HTMLElement | null {\n return typeof anchor === 'string' ? document.getElementById(anchor) : anchor;\n }\n\n private positionerElement(id: string): HTMLElement | null {\n return (\n Array.from(this.el.querySelectorAll<HTMLElement>('.toast-positioner'))\n .find(element => element.dataset['toastId'] === id) ?? null\n );\n }\n\n private ensureDomId(id: string): number {\n const current = this.domIds.get(id);\n if (current) return current;\n const next = ++this.nextDomId;\n this.domIds.set(id, next);\n return next;\n }\n\n private get activeRecords(): ToastRecord[] {\n return this.records.filter(record => record.transitionStatus !== 'ending');\n }\n\n private globalLayout() {\n const globalRecords = this.records.filter(record => !record.positioner);\n let offset = 0;\n let visibleIndex = 0;\n const gap = resolveCssLengthPx(undefined, TOKEN_DEFAULTS.space200);\n const layouts = new Map<string, { index: number; offset: number; limited: boolean }>();\n\n globalRecords.forEach((record, domIndex) => {\n if (record.transitionStatus === 'ending') {\n layouts.set(record.id, { index: domIndex, offset, limited: false });\n return;\n }\n\n const limited = visibleIndex >= Math.max(1, this.limit);\n layouts.set(record.id, { index: visibleIndex, offset, limited });\n offset += (this.heights.get(record.id) ?? 0) + gap;\n visibleIndex += 1;\n });\n return layouts;\n }\n\n private renderToast(\n record: ToastRecord,\n layout: { index: number; offset: number; limited: boolean },\n ) {\n const domId = this.ensureDomId(record.id);\n const titleId = `ds-toast-${this.instanceId}-${domId}-title`;\n const descriptionId = `ds-toast-${this.instanceId}-${domId}-description`;\n const swipe = this.swipe?.id === record.id ? this.swipe : null;\n const swipeDirection = swipe?.direction ?? this.dismissedSwipeDirections.get(record.id);\n const anchored = !!record.positioner;\n const anchoredPosition = this.anchoredPositions.get(record.id);\n const hiddenHighPriority = record.priority === 'high' && !this.keyboardActive;\n const focusTabIndex = this.keyboardActive && !layout.limited ? 0 : -1;\n const toastHeight = this.heights.get(record.id);\n\n return (\n <div\n ref={(element?: HTMLDivElement) => this.bindAnimationCancel(element, record.id)}\n class={{\n 'toast-positioner': true,\n 'toast-positioner--anchored': anchored,\n 'toast-positioner--ending': record.transitionStatus === 'ending',\n }}\n data-toast-id={record.id}\n data-behind={!anchored && layout.index > 0 ? '' : undefined}\n data-limited={layout.limited ? '' : undefined}\n data-positioned={anchored ? String(!!anchoredPosition) : undefined}\n data-side={anchoredPosition?.side}\n inert={layout.limited ? true : undefined}\n aria-hidden={layout.limited ? 'true' : undefined}\n style={{\n '--toast-index': String(layout.index),\n '--toast-offset-y': `${layout.offset}px`,\n '--toast-height': toastHeight ? `${toastHeight}px` : undefined,\n zIndex: String(this.records.length - layout.index),\n left: anchoredPosition ? `${anchoredPosition.x}px` : undefined,\n top: anchoredPosition ? `${anchoredPosition.y}px` : undefined,\n }}\n onAnimationEnd={() => this.handleAnimationComplete(record)}\n >\n <div\n class={{\n 'toast-surface': true,\n 'toast-surface--swiping': !!swipe,\n }}\n role={record.priority === 'high' ? 'alertdialog' : 'dialog'}\n aria-modal=\"false\"\n aria-labelledby={\n record.title ? titleId : record.description ? descriptionId : undefined\n }\n aria-describedby={record.title && record.description ? descriptionId : undefined}\n aria-hidden={hiddenHighPriority ? 'true' : undefined}\n data-type={record.type}\n data-priority={record.priority}\n data-transition-status={record.transitionStatus}\n data-swiping={swipe ? '' : undefined}\n data-swipe-direction={swipeDirection}\n style={{\n '--toast-swipe-movement-x': `${swipe?.x ?? 0}px`,\n '--toast-swipe-movement-y': `${swipe?.y ?? 0}px`,\n touchAction: this.touchAction,\n }}\n onPointerDown={(event: PointerEvent) => this.handlePointerDown(record, event)}\n onPointerMove={(event: PointerEvent) => this.handlePointerMove(event)}\n onPointerUp={(event: PointerEvent) => this.handlePointerUp(event)}\n onPointerCancel={(event: PointerEvent) => this.handlePointerCancel(event)}\n onPointerEnter={\n anchored ? () => this.handlePointerEnter(record.id) : undefined\n }\n onPointerLeave={\n anchored ? () => this.handlePointerLeave(record.id) : undefined\n }\n >\n <div class=\"toast-content\">\n <div class=\"toast-copy\">\n {record.title && (\n <ds-text\n class=\"toast-title\"\n as=\"div\"\n variant=\"text-title-small\"\n color=\"primary\"\n textId={titleId}\n >\n {record.title}\n </ds-text>\n )}\n {record.description && (\n <ds-text\n class=\"toast-description\"\n as=\"div\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n textId={descriptionId}\n >\n {record.description}\n </ds-text>\n )}\n </div>\n {record.action && (\n <ds-button-unfilled\n class=\"toast-action\"\n variant=\"label\"\n size=\"md\"\n type=\"button\"\n label={record.action.label}\n aria-label={record.action.ariaLabel}\n hasBorder\n focusTabIndex={focusTabIndex}\n aria-hidden={!this.keyboardActive ? 'true' : undefined}\n onDsClick={(event: CustomEvent<MouseEvent>) =>\n this.handleAction(record, event)\n }\n />\n )}\n </div>\n <ds-button-unfilled\n class=\"toast-close\"\n variant=\"icon\"\n size=\"md\"\n type=\"button\"\n icon=\"Cross\"\n aria-label={this.closeLabel}\n hasBorder={false}\n activeFill={false}\n focusTabIndex={focusTabIndex}\n aria-hidden={!this.keyboardActive ? 'true' : undefined}\n onDsClick={() => this.manager.close(record.id, 'close-button')}\n />\n </div>\n </div>\n );\n }\n\n render() {\n void this.layoutVersion;\n const globalLayouts = this.globalLayout();\n const globalRecords = this.records.filter(record => !record.positioner);\n const anchoredRecords = this.records.filter(record => !!record.positioner);\n const highPriority = this.records.filter(\n record =>\n record.priority === 'high' &&\n record.transitionStatus !== 'ending' &&\n !globalLayouts.get(record.id)?.limited,\n );\n\n return (\n <Host>\n <section\n class=\"toast-region\"\n role=\"region\"\n aria-label={this.label}\n aria-live=\"polite\"\n aria-atomic=\"false\"\n aria-relevant=\"additions text\"\n tabIndex={-1}\n onFocusin={this.handleFocusIn}\n onFocusout={(event: FocusEvent) => this.handleFocusOut(event)}\n onKeyDown={(event: KeyboardEvent) => this.handleRegionKeyDown(event)}\n >\n <div\n class=\"toast-viewport\"\n data-expanded={this.expanded ? '' : undefined}\n onPointerEnter={() => this.handlePointerEnter(GLOBAL_STACK_HOVER)}\n onPointerLeave={() => this.handlePointerLeave(GLOBAL_STACK_HOVER)}\n >\n {globalRecords.map(record =>\n this.renderToast(\n record,\n globalLayouts.get(record.id) ?? { index: 0, offset: 0, limited: false },\n )\n )}\n </div>\n <div class=\"toast-anchored-layer\">\n {anchoredRecords.map(record =>\n this.renderToast(record, { index: 0, offset: 0, limited: false })\n )}\n </div>\n <div class=\"toast-assertive-announcer\">\n {highPriority.map(record => (\n <div role=\"alert\" aria-atomic=\"true\">\n {[record.title, record.description].filter(Boolean).join('. ')}\n </div>\n ))}\n </div>\n </section>\n </Host>\n );\n }\n}\n",
333
+ "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 {\n resolveCssLengthPx,\n resolveCssTimeMs,\n resolveMotionTimeMs,\n TOKEN_DEFAULTS,\n} from '../../utils';\nimport {\n toastManager as defaultToastManager,\n type ToastActionEventDetail,\n type ToastCloseContext,\n type ToastCloseEventDetail,\n type ToastEventDetail,\n type ToastManager,\n type ToastRecord,\n type ToastSwipeDirection,\n} from '../../toast';\nimport { computeTooltipPosition } from '../Tooltip/tooltip-position';\n\ninterface ToastTimer {\n timerKey: number;\n remaining: number;\n startedAt: number;\n handle: ReturnType<typeof setTimeout> | null;\n}\n\ninterface SwipeState {\n id: string;\n x: number;\n y: number;\n direction?: ToastSwipeDirection;\n}\n\ninterface AnchoredPosition {\n x: number;\n y: number;\n side: 'top' | 'right' | 'bottom' | 'left';\n}\n\ninterface ActiveSwipe {\n id: string;\n pointerId: number;\n pointerType: string;\n startX: number;\n startY: number;\n maxDistance: number;\n cancelled: boolean;\n axis: 'x' | 'y' | null;\n}\n\nconst SWIPE_THRESHOLD = 40;\nconst SWIPE_REVERSE_CANCEL_THRESHOLD = 10;\nconst GLOBAL_STACK_HOVER = '__global-stack__';\nconst INTERACTIVE_SWIPE_SELECTOR =\n 'button,a,input,textarea,select,[role=\"button\"],[data-ds-toast-swipe-ignore]';\n\nconst attachedManagers = new WeakMap<ToastManager, Toast>();\nconst connectedToastRegions = new Set<Toast>();\nlet activeToastRegion: Toast | null = null;\nlet toastInstanceCounter = 0;\n\n@Component({\n tag: 'ds-toast',\n styleUrl: 'Toast.css',\n scoped: true,\n})\nexport class Toast {\n @Element() el!: HTMLElement;\n\n /** Manager that supplies toast records. Assign as a JavaScript property. */\n @Prop() manager: ToastManager = defaultToastManager;\n /** Maximum visible global-stack records. Limited records remain mounted and inert. */\n @Prop() limit: number = 3;\n /** Default auto-dismiss delay. Use 0 for persistent records. */\n @Prop() timeout: number | string = TOKEN_DEFAULTS.animationDelayLong2;\n /** Accessible label for the notification region. */\n @Prop() label: string = 'Notifications';\n /** Localized accessible label for every toast close action. */\n @Prop() closeLabel: string = 'Close notification';\n /** Keep the global stack 16px above the persistent mobile shell bar below 768px. */\n @Prop({ attribute: 'avoid-mobile-shell-bar', reflect: true })\n avoidMobileShellBar: boolean = false;\n /** Allowed swipe-to-dismiss directions. Assign as a JavaScript property. */\n @Prop() swipeDirections: ToastSwipeDirection[] = ['down', 'right'];\n\n @State() private records: readonly ToastRecord[] = [];\n @State() private expanded = false;\n @State() private keyboardActive = false;\n @State() private swipe: SwipeState | null = null;\n @State() private layoutVersion = 0;\n\n @Event() dsToastClose!: EventEmitter<ToastCloseEventDetail>;\n @Event() dsToastRemove!: EventEmitter<ToastEventDetail>;\n @Event() dsToastAction!: EventEmitter<ToastActionEventDetail>;\n\n private readonly instanceId = ++toastInstanceCounter;\n private readonly domIds = new Map<string, number>();\n private readonly timers = new Map<string, ToastTimer>();\n private readonly closeWatchdogs = new Map<string, ReturnType<typeof setTimeout>>();\n private readonly closeFrames = new Map<string, number>();\n private readonly closeEvents = new Set<string>();\n private readonly activationFrames = new Map<string, number>();\n private readonly pauseReasons = new Set<string>();\n private readonly hoveredIds = new Set<string>();\n private readonly heights = new Map<string, number>();\n private readonly measuredUpdateKeys = new Map<string, number>();\n private readonly anchoredPositions = new Map<string, AnchoredPosition>();\n private readonly dismissedSwipeDirections = new Map<string, ToastSwipeDirection>();\n private readonly observedElements = new Set<Element>();\n private readonly pendingRemovals: Array<{\n manager: ToastManager;\n context: ToastCloseContext;\n }> = [];\n\n private unsubscribe: (() => void) | null = null;\n private attachedManager: ToastManager | null = null;\n private previousFocus: HTMLElement | null = null;\n private resizeObserver: ResizeObserver | null = null;\n private mutationObserver: MutationObserver | null = null;\n private layoutFrame: number | null = null;\n private activeSwipe: ActiveSwipe | null = null;\n private nextDomId = 0;\n private connected = false;\n private pendingHoverLeave = false;\n\n connectedCallback() {\n this.connect();\n }\n\n componentDidLoad() {\n this.connect();\n }\n\n private connect() {\n if (this.connected) return;\n this.connected = true;\n this.resizeObserver ??= new ResizeObserver(() => this.scheduleLayout());\n this.mutationObserver ??= new MutationObserver(() => this.scheduleLayout());\n this.mutationObserver.observe(document.body, { childList: true, subtree: true });\n if (!this.unsubscribe) this.attachManager(this.manager);\n window.addEventListener('blur', this.handleWindowBlur);\n window.addEventListener('focus', this.handleWindowFocus);\n window.addEventListener('resize', this.handleWindowResize);\n window.addEventListener('scroll', this.scheduleLayout, true);\n document.addEventListener('keydown', this.handleDocumentKeyDown);\n document.addEventListener('visibilitychange', this.handleVisibilityChange);\n connectedToastRegions.add(this);\n // eslint-disable-next-line @typescript-eslint/no-this-alias -- module coordinator tracks the active connected viewport\n if (!activeToastRegion) activeToastRegion = this;\n }\n\n componentDidRender() {\n this.syncObservedElements();\n this.flushPendingRemovals();\n this.scheduleLayout();\n }\n\n disconnectedCallback() {\n this.connected = false;\n this.flushPendingRemovals();\n this.detachManager();\n this.resetManagerState();\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n this.mutationObserver?.disconnect();\n this.mutationObserver = null;\n this.observedElements.clear();\n if (this.layoutFrame !== null) cancelAnimationFrame(this.layoutFrame);\n this.layoutFrame = null;\n window.removeEventListener('blur', this.handleWindowBlur);\n window.removeEventListener('focus', this.handleWindowFocus);\n window.removeEventListener('resize', this.handleWindowResize);\n window.removeEventListener('scroll', this.scheduleLayout, true);\n document.removeEventListener('keydown', this.handleDocumentKeyDown);\n document.removeEventListener('visibilitychange', this.handleVisibilityChange);\n connectedToastRegions.delete(this);\n if (activeToastRegion === this) {\n const remaining = Array.from(connectedToastRegions);\n activeToastRegion = remaining[remaining.length - 1] ?? null;\n }\n }\n\n @Watch('manager')\n onManagerChange(next: ToastManager) {\n this.detachManager();\n this.resetManagerState();\n if (this.connected) this.attachManager(next);\n }\n\n @Watch('timeout')\n onTimeoutChange() {\n for (const timer of this.timers.values()) {\n if (timer.handle) clearTimeout(timer.handle);\n }\n this.timers.clear();\n this.syncTimers();\n }\n\n private attachManager(manager: ToastManager) {\n const attached = attachedManagers.get(manager);\n if (attached && attached !== this) {\n console.warn('[ds-toast] A manager can control only one connected ds-toast instance.');\n return;\n }\n attachedManagers.set(manager, this);\n this.attachedManager = manager;\n this.unsubscribe = manager.subscribe(records => {\n this.records = records;\n this.syncLifecycle(records);\n this.scheduleLayout();\n });\n }\n\n private detachManager() {\n this.unsubscribe?.();\n this.unsubscribe = null;\n if (this.attachedManager && attachedManagers.get(this.attachedManager) === this) {\n attachedManagers.delete(this.attachedManager);\n }\n this.attachedManager = null;\n }\n\n private resetManagerState() {\n this.flushPendingRemovals();\n if (this.el.contains(document.activeElement)) this.restorePreviousFocus();\n this.clearAllTimers();\n for (const frame of this.activationFrames.values()) cancelAnimationFrame(frame);\n for (const frame of this.closeFrames.values()) cancelAnimationFrame(frame);\n this.activationFrames.clear();\n this.closeFrames.clear();\n this.closeEvents.clear();\n this.pauseReasons.clear();\n this.hoveredIds.clear();\n this.pendingHoverLeave = false;\n this.heights.clear();\n this.measuredUpdateKeys.clear();\n this.anchoredPositions.clear();\n this.dismissedSwipeDirections.clear();\n this.domIds.clear();\n this.activeSwipe = null;\n this.swipe = null;\n this.expanded = false;\n this.keyboardActive = false;\n this.records = [];\n this.resizeObserver?.disconnect();\n this.observedElements.clear();\n }\n\n private syncObservedElements() {\n const next = new Set<Element>(\n this.el.querySelectorAll<HTMLElement>('.toast-positioner'),\n );\n for (const record of this.records) {\n if (!record.positioner) continue;\n const anchor = this.resolveAnchor(record.positioner.anchor);\n if (anchor) next.add(anchor);\n }\n\n for (const element of this.observedElements) {\n if (!next.has(element)) {\n this.resizeObserver?.unobserve(element);\n this.observedElements.delete(element);\n }\n }\n for (const element of next) {\n if (this.observedElements.has(element)) continue;\n this.resizeObserver?.observe(element);\n this.observedElements.add(element);\n }\n }\n\n private flushPendingRemovals() {\n for (const pending of this.pendingRemovals.splice(0)) {\n pending.manager.notifyRemove(pending.context);\n this.dsToastRemove.emit({\n id: pending.context.id,\n toast: pending.context.toast,\n });\n }\n }\n\n private syncLifecycle(records: readonly ToastRecord[]) {\n const recordIds = new Set(records.map(record => record.id));\n for (const id of this.hoveredIds) {\n if (id !== GLOBAL_STACK_HOVER && !recordIds.has(id)) this.hoveredIds.delete(id);\n }\n const hasEndingRecords = records.some(\n record => record.transitionStatus === 'ending',\n );\n if (this.pendingHoverLeave && !hasEndingRecords && !this.hoveredIds.size) {\n this.pendingHoverLeave = false;\n this.expanded = false;\n this.resumeTimers('hover');\n } else if (!this.hoveredIds.size && !this.pendingHoverLeave) {\n this.resumeTimers('hover');\n }\n\n for (const [id, timer] of this.timers) {\n if (recordIds.has(id)) continue;\n if (timer.handle) clearTimeout(timer.handle);\n this.timers.delete(id);\n }\n\n for (const [id, watchdog] of this.closeWatchdogs) {\n if (recordIds.has(id)) continue;\n clearTimeout(watchdog);\n this.closeWatchdogs.delete(id);\n }\n\n for (const [id, frame] of this.activationFrames) {\n if (recordIds.has(id)) continue;\n cancelAnimationFrame(frame);\n this.activationFrames.delete(id);\n }\n\n for (const [id, frame] of this.closeFrames) {\n if (recordIds.has(id)) continue;\n cancelAnimationFrame(frame);\n this.closeFrames.delete(id);\n }\n\n for (const id of this.domIds.keys()) {\n if (recordIds.has(id)) continue;\n this.closeEvents.delete(id);\n this.domIds.delete(id);\n this.heights.delete(id);\n this.measuredUpdateKeys.delete(id);\n this.anchoredPositions.delete(id);\n this.dismissedSwipeDirections.delete(id);\n }\n\n for (const record of records) {\n this.ensureDomId(record.id);\n if (\n record.transitionStatus === 'starting' &&\n !this.activationFrames.has(record.id)\n ) {\n const manager = this.attachedManager;\n const frame = requestAnimationFrame(() => {\n this.activationFrames.delete(record.id);\n manager?.activate(record.id);\n });\n this.activationFrames.set(record.id, frame);\n }\n\n if (record.transitionStatus === 'ending') {\n this.clearToastTimer(record.id);\n this.beginRemoval(record);\n } else {\n const watchdog = this.closeWatchdogs.get(record.id);\n if (watchdog) clearTimeout(watchdog);\n this.closeWatchdogs.delete(record.id);\n const closeFrame = this.closeFrames.get(record.id);\n if (closeFrame) cancelAnimationFrame(closeFrame);\n this.closeFrames.delete(record.id);\n this.closeEvents.delete(record.id);\n }\n }\n\n this.syncTimers();\n }\n\n private syncTimers() {\n const activeIds = new Set<string>();\n for (const record of this.records) {\n if (record.transitionStatus !== 'active') continue;\n activeIds.add(record.id);\n const duration = this.resolveToastTimeout(record);\n if (duration <= 0) {\n this.clearToastTimer(record.id);\n continue;\n }\n\n const existing = this.timers.get(record.id);\n if (!existing || existing.timerKey !== record.timerKey) {\n this.clearToastTimer(record.id);\n this.timers.set(record.id, {\n timerKey: record.timerKey,\n remaining: duration,\n startedAt: 0,\n handle: null,\n });\n }\n if (!this.isPaused) this.startToastTimer(record.id);\n }\n\n for (const id of this.timers.keys()) {\n if (!activeIds.has(id)) this.clearToastTimer(id);\n }\n }\n\n private resolveToastTimeout(record: ToastRecord): number {\n const value = record.timeout ?? this.timeout;\n if (typeof value === 'number') return Math.max(0, value);\n return resolveCssTimeMs(value, TOKEN_DEFAULTS.animationDelayLong2);\n }\n\n private startToastTimer(id: string) {\n const timer = this.timers.get(id);\n if (!timer || timer.handle || timer.remaining <= 0) return;\n timer.startedAt = Date.now();\n timer.handle = setTimeout(() => {\n timer.handle = null;\n timer.remaining = 0;\n this.manager.close(id, 'timeout');\n }, timer.remaining);\n }\n\n private pauseTimers(reason: string) {\n const wasPaused = this.isPaused;\n this.pauseReasons.add(reason);\n if (wasPaused) return;\n const now = Date.now();\n for (const timer of this.timers.values()) {\n if (!timer.handle) continue;\n clearTimeout(timer.handle);\n timer.handle = null;\n timer.remaining = Math.max(0, timer.remaining - (now - timer.startedAt));\n }\n }\n\n private resumeTimers(reason: string) {\n this.pauseReasons.delete(reason);\n if (this.isPaused) return;\n for (const id of this.timers.keys()) this.startToastTimer(id);\n }\n\n private get isPaused(): boolean {\n return this.pauseReasons.size > 0;\n }\n\n private clearToastTimer(id: string) {\n const timer = this.timers.get(id);\n if (timer?.handle) clearTimeout(timer.handle);\n this.timers.delete(id);\n }\n\n private clearAllTimers() {\n for (const timer of this.timers.values()) {\n if (timer.handle) clearTimeout(timer.handle);\n }\n for (const watchdog of this.closeWatchdogs.values()) clearTimeout(watchdog);\n this.timers.clear();\n this.closeWatchdogs.clear();\n }\n\n private beginRemoval(record: ToastRecord) {\n if (!this.closeEvents.has(record.id)) {\n this.closeEvents.add(record.id);\n this.dsToastClose.emit({\n id: record.id,\n reason: record.closeReason ?? 'programmatic',\n toast: record,\n });\n this.moveFocusAfterClose(record.id);\n }\n if (\n this.closeWatchdogs.has(record.id) ||\n this.closeFrames.has(record.id)\n ) return;\n\n const duration = resolveMotionTimeMs(\n TOKEN_DEFAULTS.motionShort3,\n TOKEN_DEFAULTS.animationDurationShort3,\n );\n if (duration <= 0) {\n queueMicrotask(() => this.completeRemoval(record));\n return;\n }\n const frame = requestAnimationFrame(() => {\n this.closeFrames.delete(record.id);\n const current = this.records.find(candidate => candidate.id === record.id);\n if (!current || current.transitionStatus !== 'ending') return;\n this.closeWatchdogs.set(\n record.id,\n setTimeout(() => this.completeRemoval(current), duration),\n );\n });\n this.closeFrames.set(record.id, frame);\n }\n\n private completeRemoval(record: ToastRecord) {\n const current = this.records.find(candidate => candidate.id === record.id);\n if (!current || current.transitionStatus !== 'ending') return;\n const watchdog = this.closeWatchdogs.get(record.id);\n if (watchdog) clearTimeout(watchdog);\n this.closeWatchdogs.delete(record.id);\n const closeFrame = this.closeFrames.get(record.id);\n if (closeFrame) cancelAnimationFrame(closeFrame);\n this.closeFrames.delete(record.id);\n const manager = this.attachedManager;\n const context = manager?.remove(record.id);\n if (manager && context) this.pendingRemovals.push({ manager, context });\n }\n\n private handleAnimationComplete(record: ToastRecord) {\n if (record.transitionStatus === 'ending') this.completeRemoval(record);\n }\n\n private bindAnimationCancel(element: HTMLDivElement | undefined, id: string) {\n if (!element || element.dataset['cancelBound'] === 'true') return;\n element.dataset['cancelBound'] = 'true';\n element.addEventListener('animationcancel', () => {\n const record = this.records.find(candidate => candidate.id === id);\n if (record) this.handleAnimationComplete(record);\n });\n }\n\n private handleWindowBlur = () => this.pauseTimers('window');\n private handleWindowFocus = () => this.resumeTimers('window');\n private handleVisibilityChange = () => {\n if (document.hidden) this.pauseTimers('document');\n else this.resumeTimers('document');\n };\n\n private handlePointerEnter(id: string) {\n // eslint-disable-next-line @typescript-eslint/no-this-alias -- pointer interaction selects the viewport that receives F6\n activeToastRegion = this;\n this.pendingHoverLeave = false;\n this.hoveredIds.add(id);\n this.expanded = true;\n this.pauseTimers('hover');\n }\n\n private handlePointerLeave(id: string) {\n this.hoveredIds.delete(id);\n if (this.hoveredIds.size) return;\n if (this.records.some(record => record.transitionStatus === 'ending')) {\n this.pendingHoverLeave = true;\n return;\n }\n this.expanded = false;\n this.resumeTimers('hover');\n }\n\n private handleFocusIn = () => {\n const active = document.activeElement;\n if (!(active instanceof HTMLElement) || !active.matches(':focus-visible')) return;\n // eslint-disable-next-line @typescript-eslint/no-this-alias -- module coordinator routes F6 to the last focused viewport\n activeToastRegion = this;\n this.keyboardActive = true;\n this.expanded = true;\n this.pauseTimers('focus');\n };\n\n private handleFocusOut = (event: FocusEvent) => {\n const next = event.relatedTarget;\n if (next instanceof Node && this.el.contains(next)) return;\n if (!this.keyboardActive) return;\n this.keyboardActive = false;\n this.expanded = this.hoveredIds.size > 0;\n this.resumeTimers('focus');\n };\n\n private handleDocumentKeyDown = (event: KeyboardEvent) => {\n if (event.key !== 'F6') return;\n const eligible = Array.from(connectedToastRegions).filter(\n region => region.activeRecords.length > 0,\n );\n const target =\n activeToastRegion && activeToastRegion.activeRecords.length\n ? activeToastRegion\n : eligible[eligible.length - 1];\n if (target !== this) return;\n event.preventDefault();\n if (!this.el.contains(document.activeElement)) {\n this.previousFocus = document.activeElement as HTMLElement | null;\n }\n this.keyboardActive = true;\n this.regionElement?.focus();\n };\n\n private handleRegionKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n const target = event.target as HTMLElement;\n const positioner = target.closest<HTMLElement>('.toast-positioner');\n const id = positioner?.dataset['toastId'] ?? this.activeRecords[0]?.id;\n if (id) {\n event.preventDefault();\n this.manager.close(id, 'escape');\n }\n return;\n }\n\n if (event.key === 'Tab' && event.shiftKey && event.target === this.regionElement) {\n event.preventDefault();\n this.restorePreviousFocus();\n }\n };\n\n private get regionElement(): HTMLElement | null {\n return this.el.querySelector<HTMLElement>('.toast-region');\n }\n\n private restorePreviousFocus() {\n this.previousFocus?.focus?.();\n this.previousFocus = null;\n this.keyboardActive = false;\n }\n\n private moveFocusAfterClose(id: string) {\n const active = document.activeElement;\n const closing = this.positionerElement(id);\n if (!active || !closing?.contains(active)) return;\n requestAnimationFrame(() => {\n const next = this.activeRecords.find(record => record.id !== id);\n const control = next\n ? this.positionerElement(next.id)?.querySelector<\n HTMLElement & { setFocus?: () => Promise<void> }\n >('ds-button-unfilled')\n : null;\n if (control?.setFocus) void control.setFocus();\n else if (next) this.regionElement?.focus();\n else this.restorePreviousFocus();\n });\n }\n\n private handleAction(record: ToastRecord, event: CustomEvent<MouseEvent>) {\n const detail: ToastActionEventDetail = {\n id: record.id,\n toast: record,\n originalEvent: event.detail,\n };\n this.dsToastAction.emit(detail);\n record.action?.onAction?.({\n id: record.id,\n data: record.data,\n manager: this.manager,\n originalEvent: event.detail,\n });\n }\n\n private handlePointerDown(record: ToastRecord, event: PointerEvent) {\n if (event.button !== 0) return;\n const target = event.target as Element;\n if (target.closest(INTERACTIVE_SWIPE_SELECTOR)) return;\n\n const surface = event.currentTarget as HTMLElement;\n surface.setPointerCapture(event.pointerId);\n this.activeSwipe = {\n id: record.id,\n pointerId: event.pointerId,\n pointerType: event.pointerType,\n startX: event.clientX,\n startY: event.clientY,\n maxDistance: 0,\n cancelled: false,\n axis: null,\n };\n this.swipe = { id: record.id, x: 0, y: 0 };\n if (event.pointerType === 'touch') this.pauseTimers('swipe');\n }\n\n private handlePointerMove(event: PointerEvent) {\n const active = this.activeSwipe;\n if (!active || active.pointerId !== event.pointerId || active.cancelled) return;\n const dx = event.clientX - active.startX;\n const dy = event.clientY - active.startY;\n if (!active.axis && Math.max(Math.abs(dx), Math.abs(dy)) > 1) {\n active.axis = Math.abs(dx) >= Math.abs(dy) ? 'x' : 'y';\n }\n if (!active.axis) return;\n const horizontal = active.axis === 'x';\n const direction: ToastSwipeDirection = horizontal\n ? dx >= 0 ? 'right' : 'left'\n : dy >= 0 ? 'down' : 'up';\n const allowed = this.swipeDirections.includes(direction);\n const rawDistance = horizontal ? Math.abs(dx) : Math.abs(dy);\n const distance = allowed ? rawDistance : Math.sqrt(rawDistance);\n const signed = (horizontal ? dx : dy) < 0 ? -distance : distance;\n const x = horizontal ? signed : 0;\n const y = horizontal ? 0 : signed;\n\n if (active.maxDistance - distance >= SWIPE_REVERSE_CANCEL_THRESHOLD) {\n active.cancelled = true;\n this.cancelSwipe();\n return;\n }\n active.maxDistance = Math.max(active.maxDistance, distance);\n if (rawDistance > 1) event.preventDefault();\n this.swipe = { id: active.id, x, y, direction: allowed ? direction : undefined };\n }\n\n private handlePointerUp(event: PointerEvent) {\n const active = this.activeSwipe;\n if (!active || active.pointerId !== event.pointerId) return;\n const swipe = this.swipe;\n const distance = swipe ? Math.max(Math.abs(swipe.x), Math.abs(swipe.y)) : 0;\n const shouldDismiss =\n !active.cancelled &&\n !!swipe?.direction &&\n distance >= SWIPE_THRESHOLD;\n const id = active.id;\n const wasTouch = active.pointerType === 'touch';\n this.activeSwipe = null;\n this.swipe = null;\n if (wasTouch) this.resumeTimers('swipe');\n if (shouldDismiss && swipe.direction) {\n this.dismissedSwipeDirections.set(id, swipe.direction);\n this.manager.close(id, 'swipe');\n }\n }\n\n private handlePointerCancel(event: PointerEvent) {\n if (this.activeSwipe?.pointerId !== event.pointerId) return;\n this.cancelSwipe();\n }\n\n private cancelSwipe() {\n const wasTouch = this.activeSwipe?.pointerType === 'touch';\n this.activeSwipe = null;\n this.swipe = null;\n if (wasTouch) this.resumeTimers('swipe');\n }\n\n private get touchAction(): string {\n const horizontal =\n this.swipeDirections.includes('left') || this.swipeDirections.includes('right');\n const vertical =\n this.swipeDirections.includes('up') || this.swipeDirections.includes('down');\n if (horizontal && vertical) return 'none';\n if (horizontal) return 'pan-y';\n if (vertical) return 'pan-x';\n return 'auto';\n }\n\n private handleWindowResize = () => {\n this.heights.clear();\n this.measuredUpdateKeys.clear();\n this.layoutVersion += 1;\n this.scheduleLayout();\n };\n\n private scheduleLayout = () => {\n if (this.layoutFrame !== null) return;\n this.layoutFrame = requestAnimationFrame(() => {\n this.layoutFrame = null;\n this.measureLayout();\n });\n };\n\n private measureNaturalHeight(element: HTMLElement, expandedWidth: boolean): number {\n const surface = element.querySelector<HTMLElement>('.toast-surface');\n if (!surface) return 0;\n const previousWidth = element.style.width;\n const previousTransition = element.style.transition;\n const previousHeight = surface.style.height;\n\n if (expandedWidth) {\n element.style.transition = 'none';\n element.style.width = '100%';\n }\n surface.style.height = 'auto';\n const height = surface.getBoundingClientRect().height;\n\n surface.style.height = previousHeight;\n if (expandedWidth) {\n element.style.width = previousWidth;\n element.getBoundingClientRect();\n element.style.transition = previousTransition;\n }\n return height;\n }\n\n private measureLayout() {\n let changed = false;\n for (const record of this.records) {\n const element = this.positionerElement(record.id);\n if (!element) continue;\n let height = this.heights.get(record.id) ?? 0;\n const needsHeightMeasurement =\n height <= 0 || this.measuredUpdateKeys.get(record.id) !== record.updateKey;\n if (needsHeightMeasurement) {\n height = this.measureNaturalHeight(element, !record.positioner);\n if (height > 0) {\n if (this.heights.get(record.id) !== height) changed = true;\n this.heights.set(record.id, height);\n this.measuredUpdateKeys.set(record.id, record.updateKey);\n }\n }\n\n if (!record.positioner) continue;\n const anchor = this.resolveAnchor(record.positioner.anchor);\n if (!anchor || height <= 0) {\n if (this.anchoredPositions.delete(record.id)) changed = true;\n continue;\n }\n const rect = element.getBoundingClientRect();\n const sideOffset = resolveCssLengthPx(\n record.positioner.sideOffset,\n TOKEN_DEFAULTS.space050,\n );\n const alignOffset = resolveCssLengthPx(record.positioner.alignOffset, 0);\n const position = computeTooltipPosition({\n anchorRect: anchor.getBoundingClientRect(),\n popupWidth: rect.width,\n popupHeight: height,\n side: record.positioner.side ?? 'top',\n align: record.positioner.align ?? 'center',\n sideOffsetPx: sideOffset,\n alignOffsetPx: alignOffset,\n viewportPadPx: resolveCssLengthPx(undefined, TOKEN_DEFAULTS.space200),\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n });\n const current = this.anchoredPositions.get(record.id);\n if (\n !current ||\n current.x !== position.x ||\n current.y !== position.y ||\n current.side !== position.resolvedSide\n ) {\n this.anchoredPositions.set(record.id, {\n x: position.x,\n y: position.y,\n side: position.resolvedSide,\n });\n changed = true;\n }\n }\n if (changed) this.layoutVersion += 1;\n }\n\n private resolveAnchor(anchor: HTMLElement | string): HTMLElement | null {\n return typeof anchor === 'string' ? document.getElementById(anchor) : anchor;\n }\n\n private positionerElement(id: string): HTMLElement | null {\n return (\n Array.from(this.el.querySelectorAll<HTMLElement>('.toast-positioner'))\n .find(element => element.dataset['toastId'] === id) ?? null\n );\n }\n\n private ensureDomId(id: string): number {\n const current = this.domIds.get(id);\n if (current) return current;\n const next = ++this.nextDomId;\n this.domIds.set(id, next);\n return next;\n }\n\n private get activeRecords(): ToastRecord[] {\n return this.records.filter(record => record.transitionStatus !== 'ending');\n }\n\n private globalLayout() {\n const globalRecords = this.records.filter(record => !record.positioner);\n let offset = 0;\n let visibleIndex = 0;\n const gap = resolveCssLengthPx(undefined, TOKEN_DEFAULTS.space200);\n const layouts = new Map<string, { index: number; offset: number; limited: boolean }>();\n\n globalRecords.forEach((record, domIndex) => {\n if (record.transitionStatus === 'ending') {\n layouts.set(record.id, { index: domIndex, offset, limited: false });\n return;\n }\n\n const limited = visibleIndex >= Math.max(1, this.limit);\n layouts.set(record.id, { index: visibleIndex, offset, limited });\n offset += (this.heights.get(record.id) ?? 0) + gap;\n visibleIndex += 1;\n });\n return layouts;\n }\n\n private renderToast(\n record: ToastRecord,\n layout: { index: number; offset: number; limited: boolean },\n ) {\n const domId = this.ensureDomId(record.id);\n const titleId = `ds-toast-${this.instanceId}-${domId}-title`;\n const descriptionId = `ds-toast-${this.instanceId}-${domId}-description`;\n const swipe = this.swipe?.id === record.id ? this.swipe : null;\n const swipeDirection = swipe?.direction ?? this.dismissedSwipeDirections.get(record.id);\n const anchored = !!record.positioner;\n const anchoredPosition = this.anchoredPositions.get(record.id);\n const hiddenHighPriority = record.priority === 'high' && !this.keyboardActive;\n const focusTabIndex = this.keyboardActive && !layout.limited ? 0 : -1;\n const toastHeight = this.heights.get(record.id);\n\n return (\n <div\n ref={(element?: HTMLDivElement) => this.bindAnimationCancel(element, record.id)}\n class={{\n 'toast-positioner': true,\n 'toast-positioner--anchored': anchored,\n 'toast-positioner--ending': record.transitionStatus === 'ending',\n }}\n data-toast-id={record.id}\n data-behind={!anchored && layout.index > 0 ? '' : undefined}\n data-limited={layout.limited ? '' : undefined}\n data-positioned={anchored ? String(!!anchoredPosition) : undefined}\n data-side={anchoredPosition?.side}\n inert={layout.limited ? true : undefined}\n aria-hidden={layout.limited ? 'true' : undefined}\n style={{\n '--toast-index': String(layout.index),\n '--toast-offset-y': `${layout.offset}px`,\n '--toast-height': toastHeight ? `${toastHeight}px` : undefined,\n zIndex: String(this.records.length - layout.index),\n left: anchoredPosition ? `${anchoredPosition.x}px` : undefined,\n top: anchoredPosition ? `${anchoredPosition.y}px` : undefined,\n }}\n onAnimationEnd={() => this.handleAnimationComplete(record)}\n >\n <div\n class={{\n 'toast-surface': true,\n 'toast-surface--swiping': !!swipe,\n }}\n role={record.priority === 'high' ? 'alertdialog' : 'dialog'}\n aria-modal=\"false\"\n aria-labelledby={\n record.title ? titleId : record.description ? descriptionId : undefined\n }\n aria-describedby={record.title && record.description ? descriptionId : undefined}\n aria-hidden={hiddenHighPriority ? 'true' : undefined}\n data-type={record.type}\n data-priority={record.priority}\n data-transition-status={record.transitionStatus}\n data-swiping={swipe ? '' : undefined}\n data-swipe-direction={swipeDirection}\n style={{\n '--toast-swipe-movement-x': `${swipe?.x ?? 0}px`,\n '--toast-swipe-movement-y': `${swipe?.y ?? 0}px`,\n touchAction: this.touchAction,\n }}\n onPointerDown={(event: PointerEvent) => this.handlePointerDown(record, event)}\n onPointerMove={(event: PointerEvent) => this.handlePointerMove(event)}\n onPointerUp={(event: PointerEvent) => this.handlePointerUp(event)}\n onPointerCancel={(event: PointerEvent) => this.handlePointerCancel(event)}\n onPointerEnter={\n anchored ? () => this.handlePointerEnter(record.id) : undefined\n }\n onPointerLeave={\n anchored ? () => this.handlePointerLeave(record.id) : undefined\n }\n >\n <div class=\"toast-content\">\n <div class=\"toast-copy\">\n {record.title && (\n <ds-text\n class=\"toast-title\"\n as=\"div\"\n variant=\"text-title-small\"\n color=\"primary\"\n textId={titleId}\n >\n {record.title}\n </ds-text>\n )}\n {record.description && (\n <ds-text\n class=\"toast-description\"\n as=\"div\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n textId={descriptionId}\n >\n {record.description}\n </ds-text>\n )}\n </div>\n {record.action && (\n <ds-button-unfilled\n class=\"toast-action\"\n variant=\"label\"\n size=\"md\"\n type=\"button\"\n label={record.action.label}\n aria-label={record.action.ariaLabel}\n hasBorder\n focusTabIndex={focusTabIndex}\n aria-hidden={!this.keyboardActive ? 'true' : undefined}\n onDsClick={(event: CustomEvent<MouseEvent>) =>\n this.handleAction(record, event)\n }\n />\n )}\n </div>\n <ds-button-unfilled\n class=\"toast-close\"\n variant=\"icon\"\n size=\"md\"\n type=\"button\"\n icon=\"Cross\"\n aria-label={this.closeLabel}\n hasBorder={false}\n activeFill={false}\n focusTabIndex={focusTabIndex}\n aria-hidden={!this.keyboardActive ? 'true' : undefined}\n onDsClick={() => this.manager.close(record.id, 'close-button')}\n />\n </div>\n </div>\n );\n }\n\n render() {\n void this.layoutVersion;\n const globalLayouts = this.globalLayout();\n const globalRecords = this.records.filter(record => !record.positioner);\n const anchoredRecords = this.records.filter(record => !!record.positioner);\n const highPriority = this.records.filter(\n record =>\n record.priority === 'high' &&\n record.transitionStatus !== 'ending' &&\n !globalLayouts.get(record.id)?.limited,\n );\n\n return (\n <Host>\n <section\n class=\"toast-region\"\n role=\"region\"\n aria-label={this.label}\n aria-live=\"polite\"\n aria-atomic=\"false\"\n aria-relevant=\"additions text\"\n tabIndex={-1}\n onFocusin={this.handleFocusIn}\n onFocusout={(event: FocusEvent) => this.handleFocusOut(event)}\n onKeyDown={(event: KeyboardEvent) => this.handleRegionKeyDown(event)}\n >\n <div\n class=\"toast-viewport\"\n data-expanded={this.expanded ? '' : undefined}\n onPointerEnter={() => this.handlePointerEnter(GLOBAL_STACK_HOVER)}\n onPointerLeave={() => this.handlePointerLeave(GLOBAL_STACK_HOVER)}\n >\n {globalRecords.map(record =>\n this.renderToast(\n record,\n globalLayouts.get(record.id) ?? { index: 0, offset: 0, limited: false },\n )\n )}\n </div>\n <div class=\"toast-anchored-layer\">\n {anchoredRecords.map(record =>\n this.renderToast(record, { index: 0, offset: 0, limited: false })\n )}\n </div>\n <div class=\"toast-assertive-announcer ds-visually-hidden\">\n {highPriority.map(record => (\n <div role=\"alert\" aria-atomic=\"true\">\n {[record.title, record.description].filter(Boolean).join('. ')}\n </div>\n ))}\n </div>\n </section>\n </Host>\n );\n }\n}\n",
334
334
  "type": "registry:ui"
335
335
  }
336
336
  ]
@@ -43,6 +43,10 @@ export { DsMessageBubble } from "./ds-message-bubble.js";
43
43
  export { DsMessageComposer } from "./ds-message-composer.js";
44
44
  export { DsMessageScroller } from "./ds-message-scroller.js";
45
45
  export { DsMessage } from "./ds-message.js";
46
+ export { DsMobileBarNav } from "./ds-mobile-bar-nav.js";
47
+ export { DsMobileHeader } from "./ds-mobile-header.js";
48
+ export { DsMobileSectionSwitcher } from "./ds-mobile-section-switcher.js";
49
+ export { DsMobileSheetNav } from "./ds-mobile-sheet-nav.js";
46
50
  export { DsModal } from "./ds-modal.js";
47
51
  export { DsPanelNav } from "./ds-panel-nav.js";
48
52
  export { DsPanelSubNav } from "./ds-panel-sub-nav.js";
@@ -56,9 +60,6 @@ export { DsSelect } from "./ds-select.js";
56
60
  export { DsShellApp } from "./ds-shell-app.js";
57
61
  export { DsShellGradientPicker } from "./ds-shell-gradient-picker.js";
58
62
  export { DsShellGradientSwatch } from "./ds-shell-gradient-swatch.js";
59
- export { DsShellMobileBar } from "./ds-shell-mobile-bar.js";
60
- export { DsShellMobileNav } from "./ds-shell-mobile-nav.js";
61
- export { DsShellMobileSectionNav } from "./ds-shell-mobile-section-nav.js";
62
63
  export { DsShellPage } from "./ds-shell-page.js";
63
64
  export { DsShellTools } from "./ds-shell-tools.js";
64
65
  export { DsSkeleton } from "./ds-skeleton.js";
@@ -44,6 +44,10 @@ export { DsMessageBubble } from "./ds-message-bubble.js";
44
44
  export { DsMessageComposer } from "./ds-message-composer.js";
45
45
  export { DsMessageScroller } from "./ds-message-scroller.js";
46
46
  export { DsMessage } from "./ds-message.js";
47
+ export { DsMobileBarNav } from "./ds-mobile-bar-nav.js";
48
+ export { DsMobileHeader } from "./ds-mobile-header.js";
49
+ export { DsMobileSectionSwitcher } from "./ds-mobile-section-switcher.js";
50
+ export { DsMobileSheetNav } from "./ds-mobile-sheet-nav.js";
47
51
  export { DsModal } from "./ds-modal.js";
48
52
  export { DsPanelNav } from "./ds-panel-nav.js";
49
53
  export { DsPanelSubNav } from "./ds-panel-sub-nav.js";
@@ -57,9 +61,6 @@ export { DsSelect } from "./ds-select.js";
57
61
  export { DsShellApp } from "./ds-shell-app.js";
58
62
  export { DsShellGradientPicker } from "./ds-shell-gradient-picker.js";
59
63
  export { DsShellGradientSwatch } from "./ds-shell-gradient-swatch.js";
60
- export { DsShellMobileBar } from "./ds-shell-mobile-bar.js";
61
- export { DsShellMobileNav } from "./ds-shell-mobile-nav.js";
62
- export { DsShellMobileSectionNav } from "./ds-shell-mobile-section-nav.js";
63
64
  export { DsShellPage } from "./ds-shell-page.js";
64
65
  export { DsShellTools } from "./ds-shell-tools.js";
65
66
  export { DsSkeleton } from "./ds-skeleton.js";
@@ -0,0 +1,13 @@
1
+ /**
2
+ * This file was automatically generated by the Stencil React Output Target.
3
+ * Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
4
+ */
5
+ import type { EventName, StencilReactComponent } from '@stencil/react-output-target/runtime';
6
+ import { type DsMobileBarNavCustomEvent, type MobileBarNavDestinationDetail } from "@ds-mo/ui";
7
+ import type { Components } from "@ds-mo/ui/dist/components";
8
+ import { DsMobileBarNav as DsMobileBarNavElement } from "@ds-mo/ui/dist/components/ds-mobile-bar-nav.js";
9
+ export type DsMobileBarNavEvents = {
10
+ onDsSheetNavToggle: EventName<DsMobileBarNavCustomEvent<boolean>>;
11
+ onDsDestinationChange: EventName<DsMobileBarNavCustomEvent<MobileBarNavDestinationDetail>>;
12
+ };
13
+ export declare const DsMobileBarNav: StencilReactComponent<DsMobileBarNavElement, DsMobileBarNavEvents, Components.DsMobileBarNav>;