@ds-mo/ui 14.18.1 → 14.19.1

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 (359) hide show
  1. package/dist/.package-ready.json +1 -1
  2. package/dist/agent-patterns.json +1 -1
  3. package/dist/agent.json +16 -9
  4. package/dist/angular/ds-pagination.d.ts +1 -1
  5. package/dist/angular/ds-pagination.js +3 -3
  6. package/dist/components/ds-agent-activity.js +1 -1
  7. package/dist/components/ds-agent-questionnaire.js +1 -1
  8. package/dist/components/ds-agent-response.js +1 -1
  9. package/dist/components/ds-agent-source-list.js +1 -1
  10. package/dist/components/ds-agent-tool-call.js +1 -1
  11. package/dist/components/ds-attachment-list.js +1 -1
  12. package/dist/components/ds-avatar.js +1 -1
  13. package/dist/components/ds-badge.js +1 -1
  14. package/dist/components/ds-banner.js +1 -1
  15. package/dist/components/ds-banner.js.map +1 -1
  16. package/dist/components/ds-bar-action.js +1 -1
  17. package/dist/components/ds-bar-action.js.map +1 -1
  18. package/dist/components/ds-bar-nav.js +1 -1
  19. package/dist/components/ds-bar-title.js +1 -1
  20. package/dist/components/ds-bar-workflow.js +1 -1
  21. package/dist/components/ds-bar-workflow.js.map +1 -1
  22. package/dist/components/ds-breadcrumb.js +1 -1
  23. package/dist/components/ds-button-filled.js +1 -1
  24. package/dist/components/ds-button-unfilled.js +1 -1
  25. package/dist/components/ds-card-chart.js +1 -1
  26. package/dist/components/ds-card-chart.js.map +1 -1
  27. package/dist/components/ds-card-overview.js +1 -1
  28. package/dist/components/ds-card-overview.js.map +1 -1
  29. package/dist/components/ds-card-setting.js +1 -1
  30. package/dist/components/ds-card-setting.js.map +1 -1
  31. package/dist/components/ds-chart-legend.js +1 -1
  32. package/dist/components/ds-chart-legend.js.map +1 -1
  33. package/dist/components/ds-chart.js +1 -1
  34. package/dist/components/ds-chart.js.map +1 -1
  35. package/dist/components/ds-checkbox.js +1 -1
  36. package/dist/components/ds-chip.js +1 -1
  37. package/dist/components/ds-chip.js.map +1 -1
  38. package/dist/components/ds-code-block.js +1 -1
  39. package/dist/components/ds-conversation-list-item.js +1 -1
  40. package/dist/components/ds-conversation-list-item.js.map +1 -1
  41. package/dist/components/ds-conversation-list-section.js +1 -1
  42. package/dist/components/ds-conversation-list.js +1 -1
  43. package/dist/components/ds-conversation-list.js.map +1 -1
  44. package/dist/components/ds-divider.js +1 -1
  45. package/dist/components/ds-empty-state.js +1 -1
  46. package/dist/components/ds-field.js +1 -1
  47. package/dist/components/ds-filter-menu.js +1 -1
  48. package/dist/components/ds-icon.js +1 -1
  49. package/dist/components/ds-input.js +1 -1
  50. package/dist/components/ds-loader.js +1 -1
  51. package/dist/components/ds-markdown.js +1 -1
  52. package/dist/components/ds-menu.js +1 -1
  53. package/dist/components/ds-message-actions.js +1 -1
  54. package/dist/components/ds-message-bubble.js +1 -1
  55. package/dist/components/ds-message-bubble.js.map +1 -1
  56. package/dist/components/ds-message-composer.js +1 -1
  57. package/dist/components/ds-message-composer.js.map +1 -1
  58. package/dist/components/ds-message-scroller.js +1 -1
  59. package/dist/components/ds-message-scroller.js.map +1 -1
  60. package/dist/components/ds-message.js +1 -1
  61. package/dist/components/ds-mobile-bar-nav.js +1 -1
  62. package/dist/components/ds-mobile-header.js +1 -1
  63. package/dist/components/ds-mobile-section-switcher.js +1 -1
  64. package/dist/components/ds-mobile-sheet-nav.js +1 -1
  65. package/dist/components/ds-modal.js +1 -1
  66. package/dist/components/ds-pagination.js +1 -1
  67. package/dist/components/ds-panel-nav.js +1 -1
  68. package/dist/components/ds-panel-sub-nav.js +1 -1
  69. package/dist/components/ds-panel-sub-nav.js.map +1 -1
  70. package/dist/components/ds-panel-tool-header.js +1 -1
  71. package/dist/components/ds-panel-tool-search.js +1 -1
  72. package/dist/components/ds-panel-tool-search.js.map +1 -1
  73. package/dist/components/ds-panel-tools.js +1 -1
  74. package/dist/components/ds-paper-texture.js +1 -1
  75. package/dist/components/ds-radio.js +1 -1
  76. package/dist/components/ds-scroll-overlay.js +1 -1
  77. package/dist/components/ds-scroll-overlay.js.map +1 -1
  78. package/dist/components/ds-select.js +1 -1
  79. package/dist/components/ds-shell-app.js +1 -1
  80. package/dist/components/ds-shell-app.js.map +1 -1
  81. package/dist/components/ds-shell-page.js +1 -1
  82. package/dist/components/ds-shell-tools.js +1 -1
  83. package/dist/components/ds-skeleton.js +1 -1
  84. package/dist/components/ds-slider.js +1 -1
  85. package/dist/components/ds-slider.js.map +1 -1
  86. package/dist/components/ds-swatch-picker.js +1 -1
  87. package/dist/components/ds-switch.js +1 -1
  88. package/dist/components/ds-tab-group.js +1 -1
  89. package/dist/components/ds-table-filter.js +1 -1
  90. package/dist/components/ds-table-saved-views.js +1 -1
  91. package/dist/components/ds-table-sort.js +1 -1
  92. package/dist/components/ds-table-sort.js.map +1 -1
  93. package/dist/components/ds-table-toolbar.js +1 -1
  94. package/dist/components/ds-table.js +1 -1
  95. package/dist/components/ds-table.js.map +1 -1
  96. package/dist/components/ds-tag.js +1 -1
  97. package/dist/components/ds-text.js +1 -1
  98. package/dist/components/ds-toast.js +1 -1
  99. package/dist/components/ds-toast.js.map +1 -1
  100. package/dist/components/ds-tooltip-chart.js +1 -1
  101. package/dist/components/ds-tooltip.js +1 -1
  102. package/dist/components/ds-typing-indicator.js +1 -1
  103. package/dist/components/p-0XTyQNRG.js +2 -0
  104. package/dist/components/{p-JPTkZ3Ji.js.map → p-0XTyQNRG.js.map} +1 -1
  105. package/dist/components/{p-DFQ9mcbK.js → p-1YHFQeI7.js} +1 -1
  106. package/dist/components/p-1YHFQeI7.js.map +1 -0
  107. package/dist/components/p-B-HsfQOK.js +2 -0
  108. package/dist/components/p-B-HsfQOK.js.map +1 -0
  109. package/dist/components/p-B4wTnsUL.js +2 -0
  110. package/dist/components/p-B4wTnsUL.js.map +1 -0
  111. package/dist/components/p-B6WQre5D.js +2 -0
  112. package/dist/components/p-B6WQre5D.js.map +1 -0
  113. package/dist/components/{p-B1JRCDHS.js → p-BHhmB9-4.js} +2 -2
  114. package/dist/components/{p-B1JRCDHS.js.map → p-BHhmB9-4.js.map} +1 -1
  115. package/dist/components/{p-CqQFowZd.js → p-BNq9fr9S.js} +2 -2
  116. package/dist/components/p-BNq9fr9S.js.map +1 -0
  117. package/dist/components/{p-BKp__CQX.js → p-BWwiCvep.js} +2 -2
  118. package/dist/components/p-BWwiCvep.js.map +1 -0
  119. package/dist/components/{p-BBG6Sswb.js → p-BYFS2qWb.js} +2 -2
  120. package/dist/components/{p-BBG6Sswb.js.map → p-BYFS2qWb.js.map} +1 -1
  121. package/dist/components/{p-uI6z53BE.js → p-Bb4KnRy0.js} +2 -2
  122. package/dist/components/{p-HB_cf_aR.js → p-BeA6Jv-3.js} +2 -2
  123. package/dist/components/{p-HB_cf_aR.js.map → p-BeA6Jv-3.js.map} +1 -1
  124. package/dist/components/{p-1Hu9jlju.js → p-Bf7PZJPT.js} +2 -2
  125. package/dist/components/{p-Bbb9A3NV.js → p-BjRD57w3.js} +2 -2
  126. package/dist/components/p-BjRD57w3.js.map +1 -0
  127. package/dist/components/{p-CKVHrOEp.js → p-BmlGeaY8.js} +2 -2
  128. package/dist/components/p-BmlGeaY8.js.map +1 -0
  129. package/dist/components/{p-CLVxgTme.js → p-BubgDQlb.js} +2 -2
  130. package/dist/components/p-BubgDQlb.js.map +1 -0
  131. package/dist/components/{p-BNkU9dcc.js → p-Bv6FSDPR.js} +2 -2
  132. package/dist/components/{p-BNkU9dcc.js.map → p-Bv6FSDPR.js.map} +1 -1
  133. package/dist/components/p-Bx1IHqJb.js +2 -0
  134. package/dist/components/p-Bx1IHqJb.js.map +1 -0
  135. package/dist/components/{p-D3fMaUJ3.js → p-Bx5BSd6F.js} +2 -2
  136. package/dist/components/p-Bx5BSd6F.js.map +1 -0
  137. package/dist/components/{p-B8q-6a03.js → p-C0qN3PSb.js} +1 -1
  138. package/dist/components/{p-B8q-6a03.js.map → p-C0qN3PSb.js.map} +1 -1
  139. package/dist/components/{p-H5T258T-.js → p-CFsLF-ts.js} +2 -2
  140. package/dist/components/p-CFsLF-ts.js.map +1 -0
  141. package/dist/components/p-CKtJ6FHM.js +2 -0
  142. package/dist/components/p-CKtJ6FHM.js.map +1 -0
  143. package/dist/components/p-CRSwNbPk.js +2 -0
  144. package/dist/components/p-CRSwNbPk.js.map +1 -0
  145. package/dist/components/{p-BFfLvMZd.js → p-Cea7rROU.js} +2 -2
  146. package/dist/components/{p-BFfLvMZd.js.map → p-Cea7rROU.js.map} +1 -1
  147. package/dist/components/{p-B0Byw6OX.js → p-CgGcgMjs.js} +1 -1
  148. package/dist/components/{p-B0Byw6OX.js.map → p-CgGcgMjs.js.map} +1 -1
  149. package/dist/components/{p-C-Y7gYgz.js → p-ChfVRPHw.js} +2 -2
  150. package/dist/components/p-ChfVRPHw.js.map +1 -0
  151. package/dist/components/{p-CJq3n5kA.js → p-Ck1hgD6g.js} +2 -2
  152. package/dist/components/p-Ck1hgD6g.js.map +1 -0
  153. package/dist/components/{p-BMc0DE59.js → p-CochmPyg.js} +1 -1
  154. package/dist/components/p-CochmPyg.js.map +1 -0
  155. package/dist/components/{p-iMuFgSoM.js → p-Cp5Hr7Vz.js} +2 -2
  156. package/dist/components/p-Cp5Hr7Vz.js.map +1 -0
  157. package/dist/components/p-CpUXubax.js +2 -0
  158. package/dist/components/{p-DtndC9f1.js.map → p-CpUXubax.js.map} +1 -1
  159. package/dist/components/p-Crz606uD.js +2 -0
  160. package/dist/components/p-Crz606uD.js.map +1 -0
  161. package/dist/components/p-Cxr0IQjC.js.map +1 -1
  162. package/dist/components/p-D-MWvpg6.js +2 -0
  163. package/dist/components/p-D-MWvpg6.js.map +1 -0
  164. package/dist/components/{p-Bu3dOjpW.js → p-D7v6Na7U.js} +2 -2
  165. package/dist/components/{p-Bu3dOjpW.js.map → p-D7v6Na7U.js.map} +1 -1
  166. package/dist/components/{p-C6DNhlES.js → p-DA5H_Iq_.js} +1 -1
  167. package/dist/components/p-DA5H_Iq_.js.map +1 -0
  168. package/dist/components/p-DBYCwq9L.js +2 -0
  169. package/dist/components/{p-BXN-e7vF.js.map → p-DBYCwq9L.js.map} +1 -1
  170. package/dist/components/{p-CY4YFAT3.js → p-DCbKsABt.js} +1 -1
  171. package/dist/components/p-DCbKsABt.js.map +1 -0
  172. package/dist/components/{p-C6R5Lft0.js → p-DFC0sRM-.js} +2 -2
  173. package/dist/components/p-DFC0sRM-.js.map +1 -0
  174. package/dist/components/{p-CsBICYmt.js → p-DGkn8FwV.js} +2 -2
  175. package/dist/components/p-DGkn8FwV.js.map +1 -0
  176. package/dist/components/p-DIqT1m2r.js +2 -0
  177. package/dist/components/p-DIqT1m2r.js.map +1 -0
  178. package/dist/components/p-DKt_8s-U.js +2 -0
  179. package/dist/components/p-DKt_8s-U.js.map +1 -0
  180. package/dist/components/p-DOUk4KoT.js.map +1 -1
  181. package/dist/components/p-DQBT1Jkf.js +2 -0
  182. package/dist/components/p-DQBT1Jkf.js.map +1 -0
  183. package/dist/components/p-DW5Ita-P.js +2 -0
  184. package/dist/components/{p-DUdOmSFR.js.map → p-DW5Ita-P.js.map} +1 -1
  185. package/dist/components/{p-DNpLDbWY.js → p-DZ58Sg5a.js} +1 -1
  186. package/dist/components/p-DZ58Sg5a.js.map +1 -0
  187. package/dist/components/{p-DDgeXGbB.js → p-DZv2g9Pe.js} +2 -2
  188. package/dist/components/p-DZv2g9Pe.js.map +1 -0
  189. package/dist/components/{p-BknXYAEc.js → p-DdH7b4JF.js} +2 -2
  190. package/dist/components/p-DdH7b4JF.js.map +1 -0
  191. package/dist/components/{p-DBue3ISk.js → p-Demfba7T.js} +2 -2
  192. package/dist/components/p-Demfba7T.js.map +1 -0
  193. package/dist/components/p-Djxw2FjZ.js +2 -0
  194. package/dist/components/{p-CwjVgUqy.js.map → p-Djxw2FjZ.js.map} +1 -1
  195. package/dist/components/p-DmWoFSGs.js +2 -0
  196. package/dist/components/p-DmWoFSGs.js.map +1 -0
  197. package/dist/components/p-Do8wbFff.js +2 -0
  198. package/dist/components/p-Do8wbFff.js.map +1 -0
  199. package/dist/components/p-Drnyq8z7.js +2 -0
  200. package/dist/components/p-Drnyq8z7.js.map +1 -0
  201. package/dist/components/p-DsgEY6mZ.js +2 -0
  202. package/dist/components/p-DsgEY6mZ.js.map +1 -0
  203. package/dist/components/p-Is1lFmwp.js +2 -0
  204. package/dist/components/p-Is1lFmwp.js.map +1 -0
  205. package/dist/components/{p-BUDAwJVV.js → p-caLOcQUK.js} +2 -2
  206. package/dist/components/p-caLOcQUK.js.map +1 -0
  207. package/dist/components/{p-D1eEgwcP.js → p-etg-Uk1u.js} +1 -1
  208. package/dist/components/{p-D1eEgwcP.js.map → p-etg-Uk1u.js.map} +1 -1
  209. package/dist/components/{p-C9gC4haA.js → p-fRDzpfi9.js} +2 -2
  210. package/dist/components/p-fRDzpfi9.js.map +1 -0
  211. package/dist/components/{p-C-vRkDyn.js → p-rCeztgVn.js} +1 -1
  212. package/dist/components/{p-C-vRkDyn.js.map → p-rCeztgVn.js.map} +1 -1
  213. package/dist/components/{p-B_jZIC9R.js → p-vrotWZxt.js} +2 -2
  214. package/dist/components/{p-B_jZIC9R.js.map → p-vrotWZxt.js.map} +1 -1
  215. package/dist/docs/components.json +35 -6
  216. package/dist/lib/shell/index.d.ts +2 -2
  217. package/dist/lib/shell/index.js +8 -6
  218. package/dist/lib/shell/index.js.map +2 -2
  219. package/dist/lib/toast/index.js +1 -4
  220. package/dist/lib/toast/index.js.map +2 -2
  221. package/dist/lib/utils/index.d.ts +1 -1
  222. package/dist/lib/utils/index.js +104 -22
  223. package/dist/lib/utils/index.js.map +2 -2
  224. package/dist/mcp/cli.js +70 -31
  225. package/dist/mcp-data/registry/agent-activity.json +1 -1
  226. package/dist/mcp-data/registry/agent-questionnaire.json +3 -3
  227. package/dist/mcp-data/registry/agent-source-list.json +2 -2
  228. package/dist/mcp-data/registry/agent-tool-call.json +2 -2
  229. package/dist/mcp-data/registry/attachment-list.json +1 -1
  230. package/dist/mcp-data/registry/badge.json +1 -1
  231. package/dist/mcp-data/registry/banner.json +2 -2
  232. package/dist/mcp-data/registry/bar-action.json +1 -1
  233. package/dist/mcp-data/registry/bar-nav.json +3 -3
  234. package/dist/mcp-data/registry/bar-workflow.json +1 -1
  235. package/dist/mcp-data/registry/breadcrumb.json +1 -1
  236. package/dist/mcp-data/registry/button-filled.json +1 -1
  237. package/dist/mcp-data/registry/button-unfilled.json +1 -1
  238. package/dist/mcp-data/registry/card-chart.json +2 -2
  239. package/dist/mcp-data/registry/card-overview.json +3 -3
  240. package/dist/mcp-data/registry/card-setting.json +1 -1
  241. package/dist/mcp-data/registry/chart-legend.json +1 -1
  242. package/dist/mcp-data/registry/chart.json +1 -1
  243. package/dist/mcp-data/registry/checkbox.json +1 -1
  244. package/dist/mcp-data/registry/chip.json +1 -1
  245. package/dist/mcp-data/registry/code-block.json +1 -1
  246. package/dist/mcp-data/registry/conversation-list-item.json +1 -1
  247. package/dist/mcp-data/registry/conversation-list.json +2 -2
  248. package/dist/mcp-data/registry/divider.json +1 -1
  249. package/dist/mcp-data/registry/empty-state.json +1 -1
  250. package/dist/mcp-data/registry/field.json +1 -1
  251. package/dist/mcp-data/registry/filter-menu.json +1 -1
  252. package/dist/mcp-data/registry/icon.json +2 -2
  253. package/dist/mcp-data/registry/input.json +2 -2
  254. package/dist/mcp-data/registry/loader.json +1 -1
  255. package/dist/mcp-data/registry/menu.json +3 -3
  256. package/dist/mcp-data/registry/message-bubble.json +1 -1
  257. package/dist/mcp-data/registry/message-composer.json +2 -2
  258. package/dist/mcp-data/registry/message-scroller.json +3 -3
  259. package/dist/mcp-data/registry/mobile-bar-nav.json +1 -1
  260. package/dist/mcp-data/registry/mobile-header.json +2 -2
  261. package/dist/mcp-data/registry/mobile-section-switcher.json +2 -2
  262. package/dist/mcp-data/registry/mobile-sheet-nav.json +1 -1
  263. package/dist/mcp-data/registry/modal.json +2 -2
  264. package/dist/mcp-data/registry/pagination.json +32 -11
  265. package/dist/mcp-data/registry/panel-nav.json +2 -2
  266. package/dist/mcp-data/registry/panel-sub-nav.json +1 -1
  267. package/dist/mcp-data/registry/panel-tool-header.json +1 -1
  268. package/dist/mcp-data/registry/panel-tool-search.json +1 -1
  269. package/dist/mcp-data/registry/panel-tools.json +2 -2
  270. package/dist/mcp-data/registry/radio.json +1 -1
  271. package/dist/mcp-data/registry/registry.json +15 -8
  272. package/dist/mcp-data/registry/select.json +1 -1
  273. package/dist/mcp-data/registry/shell-app.json +2 -2
  274. package/dist/mcp-data/registry/shell-page.json +1 -1
  275. package/dist/mcp-data/registry/shell-tools.json +1 -1
  276. package/dist/mcp-data/registry/skeleton.json +2 -2
  277. package/dist/mcp-data/registry/slider.json +2 -2
  278. package/dist/mcp-data/registry/swatch-picker.json +3 -3
  279. package/dist/mcp-data/registry/switch.json +2 -2
  280. package/dist/mcp-data/registry/tab-group.json +2 -2
  281. package/dist/mcp-data/registry/table-sort.json +1 -1
  282. package/dist/mcp-data/registry/table.json +18 -18
  283. package/dist/mcp-data/registry/tag.json +2 -2
  284. package/dist/mcp-data/registry/text.json +4 -4
  285. package/dist/mcp-data/registry/toast.json +2 -2
  286. package/dist/mcp-data/registry/tooltip-chart.json +2 -2
  287. package/dist/mcp-data/registry/tooltip.json +2 -2
  288. package/dist/styles/prose.css +42 -38
  289. package/dist/styles/table.css +39 -67
  290. package/dist/types/components/CardSetting/index.d.ts +1 -1
  291. package/dist/types/components/Pagination/Pagination.d.ts +2 -0
  292. package/dist/types/components/Pagination/pagination-types.d.ts +2 -0
  293. package/dist/types/components/TooltipChart/index.d.ts +1 -1
  294. package/dist/types/components.d.ts +11 -0
  295. package/dist/types/shell/index.d.ts +2 -2
  296. package/dist/types/utils/index.d.ts +1 -1
  297. package/dist/vue/ds-pagination.js +1 -0
  298. package/package.json +11 -1
  299. package/dist/components/p-B00PEE6t.js +0 -2
  300. package/dist/components/p-B00PEE6t.js.map +0 -1
  301. package/dist/components/p-BCOl1x0_.js +0 -2
  302. package/dist/components/p-BCOl1x0_.js.map +0 -1
  303. package/dist/components/p-BHVOiWz5.js +0 -2
  304. package/dist/components/p-BHVOiWz5.js.map +0 -1
  305. package/dist/components/p-BKp__CQX.js.map +0 -1
  306. package/dist/components/p-BMc0DE59.js.map +0 -1
  307. package/dist/components/p-BPG0r_mC.js +0 -2
  308. package/dist/components/p-BPG0r_mC.js.map +0 -1
  309. package/dist/components/p-BUDAwJVV.js.map +0 -1
  310. package/dist/components/p-BXN-e7vF.js +0 -2
  311. package/dist/components/p-Bbb9A3NV.js.map +0 -1
  312. package/dist/components/p-Bece5KH3.js +0 -2
  313. package/dist/components/p-Bece5KH3.js.map +0 -1
  314. package/dist/components/p-BhYS_Ump.js +0 -2
  315. package/dist/components/p-BhYS_Ump.js.map +0 -1
  316. package/dist/components/p-Bkh7scMQ.js +0 -2
  317. package/dist/components/p-Bkh7scMQ.js.map +0 -1
  318. package/dist/components/p-BknXYAEc.js.map +0 -1
  319. package/dist/components/p-C-Y7gYgz.js.map +0 -1
  320. package/dist/components/p-C6DNhlES.js.map +0 -1
  321. package/dist/components/p-C6R5Lft0.js.map +0 -1
  322. package/dist/components/p-C9gC4haA.js.map +0 -1
  323. package/dist/components/p-CAnnQL1F.js +0 -2
  324. package/dist/components/p-CAnnQL1F.js.map +0 -1
  325. package/dist/components/p-CE2IKOsy.js +0 -2
  326. package/dist/components/p-CE2IKOsy.js.map +0 -1
  327. package/dist/components/p-CJq3n5kA.js.map +0 -1
  328. package/dist/components/p-CKVHrOEp.js.map +0 -1
  329. package/dist/components/p-CLVxgTme.js.map +0 -1
  330. package/dist/components/p-CY4YFAT3.js.map +0 -1
  331. package/dist/components/p-Cg0kimXZ.js +0 -2
  332. package/dist/components/p-Cg0kimXZ.js.map +0 -1
  333. package/dist/components/p-CqQFowZd.js.map +0 -1
  334. package/dist/components/p-CsBICYmt.js.map +0 -1
  335. package/dist/components/p-CwjVgUqy.js +0 -2
  336. package/dist/components/p-D123Jvp-.js +0 -2
  337. package/dist/components/p-D123Jvp-.js.map +0 -1
  338. package/dist/components/p-D3fMaUJ3.js.map +0 -1
  339. package/dist/components/p-D7R4yE2C.js +0 -2
  340. package/dist/components/p-D7R4yE2C.js.map +0 -1
  341. package/dist/components/p-D7haeL5y.js +0 -2
  342. package/dist/components/p-D7haeL5y.js.map +0 -1
  343. package/dist/components/p-DAvh38E4.js +0 -2
  344. package/dist/components/p-DAvh38E4.js.map +0 -1
  345. package/dist/components/p-DBue3ISk.js.map +0 -1
  346. package/dist/components/p-DDgeXGbB.js.map +0 -1
  347. package/dist/components/p-DFQ9mcbK.js.map +0 -1
  348. package/dist/components/p-DNpLDbWY.js.map +0 -1
  349. package/dist/components/p-DUdOmSFR.js +0 -2
  350. package/dist/components/p-DtndC9f1.js +0 -2
  351. package/dist/components/p-H5T258T-.js.map +0 -1
  352. package/dist/components/p-JPTkZ3Ji.js +0 -2
  353. package/dist/components/p-LK-jpxwq.js +0 -2
  354. package/dist/components/p-LK-jpxwq.js.map +0 -1
  355. package/dist/components/p-ag6Io-kL.js +0 -2
  356. package/dist/components/p-ag6Io-kL.js.map +0 -1
  357. package/dist/components/p-iMuFgSoM.js.map +0 -1
  358. /package/dist/components/{p-uI6z53BE.js.map → p-Bb4KnRy0.js.map} +0 -0
  359. /package/dist/components/{p-1Hu9jlju.js.map → p-Bf7PZJPT.js.map} +0 -0
@@ -624,12 +624,12 @@
624
624
  },
625
625
  {
626
626
  "path": "src/wc/components/ShellApp/ShellApp.tsx",
627
- "content": "import {\n Component,\n Prop,\n Element,\n Event,\n EventEmitter,\n State,\n Watch,\n Listen,\n Method,\n h,\n Host,\n} from '@stencil/core';\nimport type { NavChromeStyle } from '../../shell/nav-chrome';\nimport {\n isEditableShortcutTarget,\n resolveShellShortcut,\n} from '../../shell/shell-shortcuts';\nimport type {\n PanelToolsToolId,\n PanelToolsHeaderAction,\n} from '../PanelTools/panel-tools-types';\nimport { PANEL_TOOLS_DEFAULT_ITEMS } from '../PanelTools/panel-tools-types';\nimport type { BarTitleSectionItem } from '../BarTitle/bar-title-types';\nimport type { BreadcrumbSelectDetail } from '../Breadcrumb/breadcrumb-types';\nimport {\n DEFAULT_SHELL_GRADIENT_PRESET,\n normalizeShellGradientPreset,\n type ShellGradientPreset,\n} from '../../shell/shell-gradient-presets';\nimport {\n CHROME_TRANSITION_END,\n CHROME_TRANSITION_START,\n ChromeTransitionDepth,\n createRafCoalescer,\n readChromeTransitionSource,\n} from '../../shell/chrome-transition';\nimport {\n isPanelNavCollapsed,\n panelWidthPxFromTokens,\n readPanelNavWidthTokens,\n type PanelNavWidthTokens,\n} from '../../shell/shell-chrome-metrics';\nimport {\n SHELL_GRADIENT_IMAGE_VAR,\n SHELL_GRADIENT_OPACITY_VAR,\n SHELL_GRADIENT_POSITION_BAR_VAR,\n SHELL_GRADIENT_POSITION_PANEL_VAR,\n SHELL_GRADIENT_SIZE_VAR,\n buildShellRadialGradient,\n readShellViewportDimensions,\n shellGradientPositionBar,\n shellGradientPositionPanel,\n shellGradientPresetOpacity,\n shellGradientSize,\n} from '../../shell/shell-gradient';\nimport {\n itemUsesShellInbox,\n resolveAvailableInboxTool,\n resolveManagedShellPageCapacity,\n resolveShellResponsiveMode,\n SHELL_DESKTOP_BREAKPOINT,\n shellMobileDestinationForTool,\n type MobileDestination,\n type ShellInboxToolId,\n type ShellResponsiveMode,\n} from '../../shell/shell-responsive';\nimport { deriveActiveIdFromUrl } from '../PanelNav/panel-nav-utils';\nimport type { PanelNavItem } from '../PanelNav/panel-nav-types';\nimport type { PanelNavUserActionDetail } from '../PanelNav/panel-nav-types';\nimport type { MobileBarNavDestinationDetail } from '../MobileBarNav/mobile-bar-nav-types';\nimport type {\n ShellAppComposition,\n ShellNavigationConfig,\n ShellPageChromeConfig,\n ShellToolsConfig,\n} from './shell-app-types';\nimport type { PaperTextureConfig } from '../PaperTexture/paper-texture-types';\n\n@Component({\n tag: 'ds-shell-app',\n styleUrl: 'ShellApp.css',\n scoped: true,\n})\nexport class ShellApp {\n private static readonly FOREGROUND_REFRESH_MS = 50;\n\n /** Managed renders the complete responsive chrome; slotted exposes the advanced composition. */\n @Prop({ reflect: true }) composition: ShellAppComposition = 'managed';\n\n /** Router-owned navigation data used by managed composition. */\n @Prop() navigation: ShellNavigationConfig = {};\n\n /** Route-owned page title and section data used by managed composition. */\n @Prop({ attribute: 'page-chrome' }) pageChrome: ShellPageChromeConfig = {};\n\n /** Product-owned global tool definitions used by managed composition. */\n @Prop() tools: ShellToolsConfig = {};\n\n /** Chrome style propagated to slotted `ds-panel-nav` and `ds-bar-nav`. */\n @Prop({ attribute: 'nav-style', reflect: true }) navStyle: NavChromeStyle = 'dashboard';\n\n /**\n * Shell chrome wash preset. `none` renders solid chrome; the remaining\n * presets use token-based washes that adapt to the active color theme.\n */\n @Prop({ attribute: 'gradient-preset', reflect: true }) gradientPreset: ShellGradientPreset =\n DEFAULT_SHELL_GRADIENT_PRESET;\n\n /** Optional decorative Paper Design texture layered above the shell wash. */\n @Prop({ attribute: 'paper-texture' }) paperTexture?: PaperTextureConfig;\n\n /** When `true` (default), registers global shell keyboard shortcuts. `[` toggles panel nav; `]` closes tools; K, A, S, M, N, and / toggle tool drawers. Modifiers are ignored so browser chords like ⌘N stay native. */\n @Prop({ attribute: 'shortcuts-enabled' }) shortcutsEnabled: boolean = true;\n\n /** Controlled mobile surface shown above the persistent bottom bar. */\n @Prop({ attribute: 'mobile-destination' })\n mobileDestination: MobileDestination = 'area';\n\n /** Controlled full-stage Mobile Sheet Nav state. */\n @Prop({ attribute: 'mobile-sheet-nav-open' })\n mobileSheetNavOpen: boolean = false;\n\n /** Emitted after crossing the fixed 768px or 1200px shell boundaries. */\n @Event() dsResponsiveModeChange!: EventEmitter<{ mode: ShellResponsiveMode }>;\n\n /** Managed primary-navigation intent; the application router performs navigation. */\n @Event() dsNavSelect!: EventEmitter<string>;\n\n /** Managed desktop BarNav or mobile section-selection intent. */\n @Event() dsTabChange!: EventEmitter<string>;\n\n /** Managed page-subsection intent from BarTitle or MobileHeader. */\n @Event() dsSubsectionChange!: EventEmitter<string>;\n\n /** Managed page-level Back intent. */\n @Event() dsPageBack!: EventEmitter<MouseEvent>;\n\n /** Managed breadcrumb intent from BarTitle. */\n @Event({ cancelable: true })\n dsBreadcrumbSelect!: EventEmitter<BreadcrumbSelectDetail>;\n\n /** Managed page action intent from BarTitle. */\n @Event() dsPageAction!: EventEmitter<string>;\n\n /** Managed tools selection state. */\n @Event() dsToolChange!: EventEmitter<{ id: PanelToolsToolId; selected: boolean }>;\n\n /** Managed tool-header Back intent. */\n @Event() dsHeaderBack!: EventEmitter<{ tool: PanelToolsToolId }>;\n\n /** Managed tool-header action intent. */\n @Event() dsHeaderAction!: EventEmitter<{ tool: PanelToolsToolId; id: string }>;\n\n /** Managed drawer/fullscreen presentation state. */\n @Event() dsPresentationChange!: EventEmitter<{ presentation: 'drawer' | 'fullscreen' }>;\n\n /** Managed mobile Dashboard/Settings browsing intent. */\n @Event() dsBrowseContextChange!: EventEmitter<NavChromeStyle>;\n\n /** Managed account-footer action intent. */\n @Event() dsNavFooterAction!: EventEmitter<void>;\n\n /** Managed user/account trigger intent. */\n @Event() dsNavUserAction!: EventEmitter<PanelNavUserActionDetail>;\n\n @Element() el!: HTMLElement;\n @State() private toolsFullscreen = false;\n @State() private resolvedMode: ShellResponsiveMode = 'desktop';\n @State() private managedToolsOpen = false;\n @State() private managedActiveTool: PanelToolsToolId | '' = '';\n @State() private managedToolPresentation: 'drawer' | 'fullscreen' = 'drawer';\n @State() private managedMobileDestination: MobileDestination = 'area';\n @State() private managedMobileSheetNavOpen = false;\n @State() private managedInboxTool: ShellInboxToolId | '' = '';\n @State() private managedBrowseContext: NavChromeStyle = 'dashboard';\n\n private resizeObserver: ResizeObserver | null = null;\n private hasLoaded = false;\n private foregroundRefreshTimer: number | null = null;\n private readonly panelNavTransition = new ChromeTransitionDepth();\n private readonly chromeSyncCoalescer = createRafCoalescer(() => this.syncChrome());\n private panelWidthTokens: PanelNavWidthTokens = { expandedPx: 0, collapsedPx: 0 };\n private cachedViewportWidth = 0;\n private cachedViewportHeight = 0;\n private onWindowResize = () => {\n this.updateResponsiveMode();\n if (this.panelNavTransition.isActive) return;\n this.scheduleChromeSync();\n };\n private onVisualViewportChange = () => {\n if (this.panelNavTransition.isActive) return;\n this.scheduleChromeSync();\n };\n\n private get managed(): boolean {\n return this.composition === 'managed';\n }\n\n private get effectiveMobileDestination(): MobileDestination {\n return this.managed ? this.managedMobileDestination : this.mobileDestination;\n }\n\n private get effectiveMobileSheetNavOpen(): boolean {\n return this.managed ? this.managedMobileSheetNavOpen : this.mobileSheetNavOpen;\n }\n\n private get resolvedNavigationGroups() {\n if (this.navigation.groups?.length) return this.navigation.groups;\n return this.managedBrowseContext === 'settings'\n ? (this.navigation.settingsGroups ?? [])\n : (this.navigation.dashboardGroups ?? []);\n }\n\n private get resolvedToolItems() {\n return this.tools.items ?? PANEL_TOOLS_DEFAULT_ITEMS;\n }\n\n private get availableInboxTools(): ShellInboxToolId[] {\n return this.resolvedToolItems\n .filter(\n item =>\n !item.isInactive &&\n itemUsesShellInbox(item)\n )\n .map(item => item.id as ShellInboxToolId);\n }\n\n componentWillLoad() {\n if (typeof window !== 'undefined') {\n this.resolvedMode = resolveShellResponsiveMode(window.innerWidth);\n }\n this.managedBrowseContext = this.navigation.browseContext ?? this.navStyle;\n this.managedMobileDestination = this.mobileDestination;\n this.managedMobileSheetNavOpen = this.mobileSheetNavOpen;\n this.managedInboxTool = resolveAvailableInboxTool('', this.availableInboxTools);\n }\n\n componentDidLoad() {\n this.hasLoaded = true;\n this.syncSlottedNavStyle();\n this.syncSlottedMobileState();\n this.connectMetricsObserver();\n this.connectViewportListeners();\n this.connectPageLifecycleListeners();\n this.el.addEventListener(CHROME_TRANSITION_START, this.onChromeTransitionStart);\n this.el.addEventListener(CHROME_TRANSITION_END, this.onChromeTransitionEnd);\n requestAnimationFrame(() => {\n this.cachePanelWidthTokens();\n this.scheduleChromeSync();\n });\n const tools = this.el.querySelector('ds-shell-tools, ds-panel-tools');\n this.toolsFullscreen = tools?.getAttribute('presentation') === 'fullscreen';\n }\n\n @Listen('dsPresentationChange')\n handleToolsPresentation(event: CustomEvent<{ presentation?: string }>) {\n this.toolsFullscreen = event.detail?.presentation === 'fullscreen';\n }\n\n disconnectedCallback() {\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n this.disconnectViewportListeners();\n this.disconnectPageLifecycleListeners();\n this.cancelForegroundRefresh();\n this.chromeSyncCoalescer.cancel();\n this.el.removeEventListener(CHROME_TRANSITION_START, this.onChromeTransitionStart);\n this.el.removeEventListener(CHROME_TRANSITION_END, this.onChromeTransitionEnd);\n }\n\n @Watch('navStyle')\n @Watch('gradientPreset')\n @Watch('paperTexture')\n onShellPropsChange() {\n this.syncSlottedNavStyle();\n this.scheduleChromeSync();\n }\n\n @Watch('navigation')\n handleNavigationChange() {\n this.managedBrowseContext = this.navigation.browseContext ?? this.navStyle;\n }\n\n @Watch('tools')\n handleToolsChange() {\n if (\n this.managedActiveTool &&\n !this.resolvedToolItems.some(item => item.id === this.managedActiveTool)\n ) {\n this.managedToolsOpen = false;\n this.managedActiveTool = '';\n this.managedMobileDestination = 'area';\n }\n this.managedInboxTool = resolveAvailableInboxTool(\n this.managedInboxTool,\n this.availableInboxTools\n );\n }\n\n @Watch('mobileDestination')\n @Watch('mobileSheetNavOpen')\n onMobileStateChange(_next: unknown, previous: unknown) {\n if (this.managed) {\n this.managedMobileDestination = this.mobileDestination;\n this.managedMobileSheetNavOpen = this.mobileSheetNavOpen;\n }\n this.syncSlottedMobileState();\n if (\n previous === true &&\n !this.effectiveMobileSheetNavOpen &&\n this.resolvedMode === 'mobile'\n ) {\n requestAnimationFrame(() => {\n const bar = this.el.querySelector('ds-mobile-bar-nav') as\n | (HTMLElement & {\n focusDestination?: (\n destination: MobileDestination | 'sheet-nav'\n ) => Promise<void>;\n })\n | null;\n const activeElement = document.activeElement;\n if (activeElement instanceof HTMLElement && bar?.contains(activeElement)) return;\n void bar?.focusDestination?.('sheet-nav');\n });\n }\n }\n\n private updateResponsiveMode() {\n if (typeof window === 'undefined') return;\n const next = resolveShellResponsiveMode(window.innerWidth);\n if (next === this.resolvedMode) return;\n this.resolvedMode = next;\n this.syncSlottedMobileState();\n this.scheduleChromeSync();\n if (this.hasLoaded) this.dsResponsiveModeChange.emit({ mode: next });\n }\n\n private onChromeTransitionStart = (event: Event) => {\n const source = readChromeTransitionSource(event);\n if (source !== 'panel-nav') return;\n\n this.panelNavTransition.enter();\n const viewport = readShellViewportDimensions();\n this.cachedViewportWidth = viewport.width;\n this.cachedViewportHeight = viewport.height;\n this.syncChrome();\n };\n\n private onChromeTransitionEnd = (event: Event) => {\n const source = readChromeTransitionSource(event);\n if (source !== 'panel-nav') return;\n\n this.panelNavTransition.exit();\n if (!this.panelNavTransition.isActive) {\n this.scheduleChromeSync();\n }\n };\n\n /** Coalesce ResizeObserver bursts to one layout read per frame. */\n private scheduleChromeSync() {\n this.chromeSyncCoalescer.schedule();\n }\n\n private syncSlottedNavStyle() {\n const panel = this.el.querySelector('ds-panel-nav') as (HTMLElement & { navStyle: NavChromeStyle }) | null;\n const bar = this.el.querySelector('ds-bar-nav') as (HTMLElement & { navStyle: NavChromeStyle }) | null;\n if (panel) {\n panel.setAttribute('nav-style', this.navStyle);\n panel.navStyle = this.navStyle;\n }\n if (bar) {\n bar.setAttribute('nav-style', this.navStyle);\n bar.navStyle = this.navStyle;\n }\n }\n\n private syncSlottedMobileState() {\n const tools = this.el.querySelector('ds-shell-tools') as\n | (HTMLElement & { responsiveMode: ShellResponsiveMode })\n | null;\n const bar = this.el.querySelector('ds-mobile-bar-nav') as\n | (HTMLElement & {\n activeDestination: MobileDestination;\n sheetNavExpanded: boolean;\n })\n | null;\n const sheetNav = this.el.querySelector('ds-mobile-sheet-nav') as\n | (HTMLElement & { open: boolean })\n | null;\n\n if (tools) {\n tools.setAttribute('responsive-mode', this.resolvedMode);\n tools.responsiveMode = this.resolvedMode;\n }\n if (bar) {\n bar.activeDestination = this.effectiveMobileDestination;\n bar.sheetNavExpanded = this.effectiveMobileSheetNavOpen;\n }\n if (sheetNav) {\n sheetNav.open = this.effectiveMobileSheetNavOpen;\n }\n }\n\n private connectMetricsObserver() {\n this.resizeObserver = new ResizeObserver(() => {\n if (this.panelNavTransition.isActive) return;\n this.scheduleChromeSync();\n });\n this.resizeObserver.observe(this.el);\n const panelWrap = this.el.querySelector('.shell-app__panel');\n if (panelWrap) this.resizeObserver.observe(panelWrap);\n }\n\n private connectViewportListeners() {\n if (typeof window === 'undefined') return;\n\n window.addEventListener('resize', this.onWindowResize, { passive: true });\n\n const visual = window.visualViewport;\n if (visual) {\n visual.addEventListener('resize', this.onVisualViewportChange, { passive: true });\n visual.addEventListener('scroll', this.onVisualViewportChange, { passive: true });\n }\n }\n\n private disconnectViewportListeners() {\n if (typeof window === 'undefined') return;\n\n window.removeEventListener('resize', this.onWindowResize);\n\n const visual = window.visualViewport;\n if (visual) {\n visual.removeEventListener('resize', this.onVisualViewportChange);\n visual.removeEventListener('scroll', this.onVisualViewportChange);\n }\n }\n\n private connectPageLifecycleListeners() {\n if (typeof window === 'undefined' || typeof document === 'undefined') return;\n document.addEventListener('visibilitychange', this.onDocumentVisibilityChange);\n window.addEventListener('pageshow', this.onPageShow);\n }\n\n private disconnectPageLifecycleListeners() {\n if (typeof window === 'undefined' || typeof document === 'undefined') return;\n document.removeEventListener('visibilitychange', this.onDocumentVisibilityChange);\n window.removeEventListener('pageshow', this.onPageShow);\n }\n\n private onDocumentVisibilityChange = () => {\n if (!document.hidden) this.refreshAfterForegroundRestore();\n };\n\n private onPageShow = (event: PageTransitionEvent) => {\n if (event.persisted) this.refreshAfterForegroundRestore();\n };\n\n private cancelForegroundRefresh() {\n if (this.foregroundRefreshTimer !== null) {\n window.clearTimeout(this.foregroundRefreshTimer);\n this.foregroundRefreshTimer = null;\n }\n this.el.classList.remove('shell-app--foreground-refresh');\n }\n\n private refreshAfterForegroundRestore() {\n this.updateResponsiveMode();\n this.syncSlottedMobileState();\n this.scheduleChromeSync();\n\n if (this.resolvedMode !== 'mobile') return;\n\n /*\n * Mobile WebKit can restore a live, correctly sized DOM without repainting\n * its suspended shell layer. Re-promote the complete mobile stage for one\n * painted frame; routed and tool owners remain mounted and keep their state.\n */\n this.cancelForegroundRefresh();\n this.el.classList.add('shell-app--foreground-refresh');\n void this.el.offsetHeight;\n // A short timer spans a real compositor commit; resumed WebKit may coalesce\n // nested animation frames before presenting either one.\n this.foregroundRefreshTimer = window.setTimeout(() => {\n this.foregroundRefreshTimer = null;\n this.el.classList.remove('shell-app--foreground-refresh');\n // WebKit otherwise may keep presenting the retired composite layer.\n void this.el.querySelector<HTMLElement>('.shell-app__main')?.offsetHeight;\n }, ShellApp.FOREGROUND_REFRESH_MS);\n }\n\n private cachePanelWidthTokens() {\n const navRoot = this.el.querySelector('ds-panel-nav .panel-nav') as HTMLElement | null;\n if (!navRoot) return;\n this.panelWidthTokens = readPanelNavWidthTokens(navRoot);\n }\n\n private applyGradientVars(target: HTMLElement, vars: Record<string, string | null>) {\n const style = target.style;\n for (const [name, value] of Object.entries(vars)) {\n if (value === null) style.removeProperty(name);\n else style.setProperty(name, value);\n }\n }\n\n private clearGradientPaintVars(targets: HTMLElement[]) {\n const keys = [\n SHELL_GRADIENT_IMAGE_VAR,\n SHELL_GRADIENT_SIZE_VAR,\n SHELL_GRADIENT_OPACITY_VAR,\n ];\n for (const target of targets) {\n for (const key of keys) target.style.removeProperty(key);\n }\n }\n\n /** Panel/bar wash offsets for scroll fades and badge rings. */\n private syncChromeLayoutVars(\n panel: HTMLElement | null,\n bar: HTMLElement | null,\n panelPosition: string,\n barPosition: string,\n ) {\n if (panel) {\n panel.style.setProperty(SHELL_GRADIENT_POSITION_PANEL_VAR, panelPosition);\n }\n if (bar) {\n bar.style.setProperty(SHELL_GRADIENT_POSITION_BAR_VAR, barPosition);\n }\n }\n\n private resolvePanelWidthPx(panelNav: HTMLElement | null): number {\n const navRoot = panelNav?.querySelector('.panel-nav') as HTMLElement | null;\n\n if (this.panelNavTransition.isActive && this.panelWidthTokens.expandedPx > 0) {\n const collapsed = isPanelNavCollapsed(panelNav, navRoot);\n return panelWidthPxFromTokens(this.panelWidthTokens, collapsed);\n }\n\n const panelWrap = this.el.querySelector('.shell-app__panel') as HTMLElement | null;\n const measured = panelWrap?.getBoundingClientRect().width ?? 0;\n if (measured > 0) return measured;\n\n if (this.panelWidthTokens.expandedPx > 0) {\n const collapsed = isPanelNavCollapsed(panelNav, navRoot);\n return panelWidthPxFromTokens(this.panelWidthTokens, collapsed);\n }\n\n return 0;\n }\n\n /** Fixed-attachment wash/grid size — always the viewport, never the shell element box. */\n private resolveViewportDimensions(): { width: number; height: number } {\n if (this.panelNavTransition.isActive && this.cachedViewportWidth > 0) {\n return {\n width: this.cachedViewportWidth,\n height: this.cachedViewportHeight,\n };\n }\n\n return readShellViewportDimensions();\n }\n\n private chromeLayerActive(): boolean {\n return this.gradientLayerActive() || this.paperTextureLayerActive();\n }\n\n private gradientLayerActive(): boolean {\n return (\n this.resolvedMode !== 'mobile' &&\n normalizeShellGradientPreset(this.gradientPreset) !== 'none'\n );\n }\n\n private paperTextureLayerActive(): boolean {\n return this.resolvedMode !== 'mobile' && this.paperTexture != null;\n }\n\n @Listen('keydown', { target: 'window', capture: true })\n handleWindowKeyDown(e: KeyboardEvent) {\n if (!this.shortcutsEnabled) return;\n if (isEditableShortcutTarget(e.target)) return;\n\n const action = resolveShellShortcut(e);\n if (!action) return;\n\n e.preventDefault();\n\n const panel = this.el.querySelector('ds-panel-nav') as HTMLDsPanelNavElement | null;\n const tools = this.el.querySelector('ds-shell-tools, ds-panel-tools') as\n | (HTMLElement & {\n closeDrawer?: () => Promise<void>;\n activateTool?: (id: PanelToolsToolId) => Promise<void>;\n })\n | null;\n\n if (action === 'toggle-panel-nav') {\n void panel?.toggleCollapsed();\n return;\n }\n\n if (action === 'close-panel-tools') {\n void tools?.closeDrawer?.();\n return;\n }\n\n if (action.startsWith('open-tool:') && tools) {\n const toolId = action.slice('open-tool:'.length) as PanelToolsToolId;\n void tools.activateTool?.(toolId);\n }\n }\n\n /** Open or toggle a managed global tool from application-owned UI. */\n @Method()\n async activateTool(id: PanelToolsToolId) {\n const tools = this.el.querySelector('ds-shell-tools, ds-panel-tools') as\n | (HTMLElement & { activateTool?: (tool: PanelToolsToolId) => Promise<void> })\n | null;\n await tools?.activateTool?.(id);\n }\n\n /** Close whichever managed global surface is currently visible. */\n @Method()\n async closeGlobalSurface() {\n if (this.managedMobileSheetNavOpen) {\n this.managedMobileSheetNavOpen = false;\n return;\n }\n const tools = this.el.querySelector('ds-shell-tools, ds-panel-tools') as\n | (HTMLElement & { closeDrawer?: () => Promise<void> })\n | null;\n await tools?.closeDrawer?.();\n }\n\n /** Request drawer or fullscreen presentation for the active managed tool. */\n @Method()\n async setToolPresentation(presentation: 'drawer' | 'fullscreen') {\n this.managedToolPresentation = presentation;\n this.toolsFullscreen = presentation === 'fullscreen';\n }\n\n private handleManagedNavSelect = (event: CustomEvent<string>) => {\n event.stopPropagation();\n this.dsNavSelect.emit(event.detail);\n };\n\n private handleManagedTabChange = (event: CustomEvent<string>) => {\n event.stopPropagation();\n this.dsTabChange.emit(event.detail);\n };\n\n private handleManagedPageBack = (event: CustomEvent<MouseEvent>) => {\n event.stopPropagation();\n this.dsPageBack.emit(event.detail);\n };\n\n private handleManagedBreadcrumbSelect = (\n event: CustomEvent<BreadcrumbSelectDetail>\n ) => {\n event.stopPropagation();\n const forwarded = this.dsBreadcrumbSelect.emit(event.detail);\n if (forwarded.defaultPrevented) event.preventDefault();\n };\n\n private handleManagedSubsectionChange = (event: CustomEvent<string>) => {\n event.stopPropagation();\n this.dsSubsectionChange.emit(event.detail);\n };\n\n private handleManagedPageAction = (event: CustomEvent<string>) => {\n event.stopPropagation();\n this.dsPageAction.emit(event.detail);\n };\n\n private handleManagedToolChange = (\n event: CustomEvent<{ id: PanelToolsToolId; selected: boolean }>\n ) => {\n event.stopPropagation();\n const { id, selected } = event.detail;\n this.managedActiveTool = id;\n this.managedToolsOpen = selected;\n const item = this.resolvedToolItems.find(candidate => candidate.id === id);\n if (item && itemUsesShellInbox(item)) {\n this.managedInboxTool = id;\n }\n this.managedMobileDestination = selected\n ? (item?.mobileDestination ?? shellMobileDestinationForTool(true, id))\n : 'area';\n this.managedMobileSheetNavOpen = false;\n this.dsToolChange.emit(event.detail);\n };\n\n private handleManagedPresentationChange = (\n event: CustomEvent<{ presentation: 'drawer' | 'fullscreen' }>\n ) => {\n event.stopPropagation();\n this.managedToolPresentation = event.detail.presentation;\n this.toolsFullscreen = event.detail.presentation === 'fullscreen';\n this.dsPresentationChange.emit(event.detail);\n };\n\n private handleManagedHeaderBack = (event: CustomEvent<{ tool: PanelToolsToolId }>) => {\n event.stopPropagation();\n this.dsHeaderBack.emit(event.detail);\n };\n\n private handleManagedHeaderAction = (\n event: CustomEvent<{ tool: PanelToolsToolId; id: string }>\n ) => {\n event.stopPropagation();\n this.dsHeaderAction.emit(event.detail);\n };\n\n private handleManagedSheetToggle = (event: CustomEvent<boolean>) => {\n event.stopPropagation();\n this.managedMobileSheetNavOpen = event.detail;\n };\n\n private activateManagedMobileTool(destination: Exclude<MobileDestination, 'area'>): boolean {\n const canonicalId =\n destination === 'inbox'\n ? resolveAvailableInboxTool(this.managedInboxTool, this.availableInboxTools)\n : destination;\n if (!canonicalId) return false;\n\n const item = this.resolvedToolItems.find(\n candidate =>\n candidate.id === canonicalId ||\n (destination !== 'inbox' && candidate.mobileDestination === destination)\n );\n if (!item || item.isInactive) return false;\n\n const id = item.id;\n this.managedActiveTool = id;\n this.managedToolsOpen = true;\n if (itemUsesShellInbox(item)) this.managedInboxTool = id;\n this.dsToolChange.emit({ id: id as PanelToolsToolId, selected: true });\n return true;\n }\n\n private handleManagedMobileDestination = (\n event: CustomEvent<MobileBarNavDestinationDetail>\n ) => {\n event.stopPropagation();\n const destination = event.detail.destination;\n this.managedMobileSheetNavOpen = false;\n this.managedMobileDestination = destination;\n\n if (destination === 'area') {\n this.managedToolsOpen = false;\n return;\n }\n\n this.activateManagedMobileTool(destination);\n };\n\n private handleManagedAreaSelect = (event: CustomEvent<string>) => {\n event.stopPropagation();\n if (event.detail === 'help') {\n this.managedMobileSheetNavOpen = false;\n this.managedMobileDestination = 'help';\n if (this.activateManagedMobileTool('help')) return;\n }\n\n this.managedMobileSheetNavOpen = false;\n this.managedMobileDestination = 'area';\n this.managedToolsOpen = false;\n this.dsNavSelect.emit(event.detail);\n };\n\n private handleManagedBrowseContext = (event: CustomEvent<NavChromeStyle>) => {\n event.stopPropagation();\n this.managedBrowseContext = event.detail;\n this.dsBrowseContextChange.emit(event.detail);\n };\n\n private handleManagedSheetClose = (event: CustomEvent<void>) => {\n event.stopPropagation();\n this.managedMobileSheetNavOpen = false;\n };\n\n private handleManagedNavFooterAction = (event: CustomEvent<void>) => {\n event.stopPropagation();\n this.dsNavFooterAction.emit();\n };\n\n private handleManagedNavUserAction = (\n event: CustomEvent<PanelNavUserActionDetail>\n ) => {\n event.stopPropagation();\n this.dsNavUserAction.emit(event.detail);\n };\n\n private get currentArea(): PanelNavItem {\n const groups = [\n ...(this.navigation.dashboardGroups ?? []),\n ...(this.navigation.settingsGroups ?? []),\n ...this.resolvedNavigationGroups,\n ];\n const items = groups.flatMap(group => group.items);\n const activeId =\n this.navigation.activeId ||\n deriveActiveIdFromUrl(\n this.navigation.currentUrl ?? this.pageChrome.currentUrl ?? '',\n items\n );\n return (\n items.find(item => item.id === activeId) ??\n items[0] ?? {\n id: 'area',\n icon: 'MapPage',\n label: this.pageChrome.heading || 'Area',\n }\n );\n }\n\n private toolDot(id: PanelToolsToolId): boolean {\n return this.resolvedToolItems.find(item => item.id === id)?.dot ?? false;\n }\n\n private syncChrome() {\n const panelNav = this.el.querySelector('ds-panel-nav') as HTMLElement | null;\n const bar = this.el.querySelector('ds-bar-nav') as HTMLElement | null;\n const targets = [this.el, panelNav, bar].filter((el): el is HTMLElement => el !== null);\n\n const clearLayoutVars = () => {\n if (panelNav) panelNav.style.removeProperty(SHELL_GRADIENT_POSITION_PANEL_VAR);\n if (bar) bar.style.removeProperty(SHELL_GRADIENT_POSITION_BAR_VAR);\n };\n\n const preset = normalizeShellGradientPreset(this.gradientPreset);\n if (this.resolvedMode === 'mobile') {\n this.clearGradientPaintVars(targets);\n clearLayoutVars();\n return;\n }\n\n const viewport = this.resolveViewportDimensions();\n if (preset === 'none') {\n this.clearGradientPaintVars(targets);\n clearLayoutVars();\n this.syncPaperTextureLayout(viewport);\n return;\n }\n\n const panelWidth = this.resolvePanelWidthPx(panelNav);\n\n const panelPosition = shellGradientPositionPanel();\n const barPosition = shellGradientPositionBar(panelWidth);\n\n this.syncChromeLayoutVars(panelNav, bar, panelPosition, barPosition);\n\n const image = buildShellRadialGradient(preset);\n\n const size = shellGradientSize({\n width: viewport.width,\n height: viewport.height,\n });\n const opacity = shellGradientPresetOpacity(preset);\n\n for (const target of targets) {\n if (target === this.el) {\n this.applyGradientVars(target, {\n [SHELL_GRADIENT_IMAGE_VAR]: image,\n [SHELL_GRADIENT_SIZE_VAR]: size,\n [SHELL_GRADIENT_POSITION_PANEL_VAR]: panelPosition,\n [SHELL_GRADIENT_POSITION_BAR_VAR]: barPosition,\n [SHELL_GRADIENT_OPACITY_VAR]: opacity,\n });\n continue;\n }\n\n const isBar = target.tagName.toLowerCase() === 'ds-bar-nav';\n this.applyGradientVars(target, {\n [SHELL_GRADIENT_IMAGE_VAR]: image,\n [SHELL_GRADIENT_SIZE_VAR]: size,\n [SHELL_GRADIENT_OPACITY_VAR]: opacity,\n [SHELL_GRADIENT_POSITION_PANEL_VAR]: isBar ? null : panelPosition,\n [SHELL_GRADIENT_POSITION_BAR_VAR]: isBar ? barPosition : null,\n });\n }\n\n this.syncPaperTextureLayout(viewport);\n }\n\n private syncPaperTextureLayout(viewport: { width: number; height: number }) {\n const chrome = this.el.querySelector<HTMLElement>('.shell-app__chrome');\n const paperTexture = this.el.querySelector<HTMLElement>(\n '.shell-app__chrome > ds-paper-texture',\n );\n if (!chrome || !paperTexture) return;\n\n const chromeRect = chrome.getBoundingClientRect();\n paperTexture.style.setProperty('width', `${Math.round(viewport.width)}px`);\n paperTexture.style.setProperty('height', `${Math.round(viewport.height)}px`);\n paperTexture.style.setProperty('left', `${-Math.round(chromeRect.left)}px`);\n paperTexture.style.setProperty('top', `${-Math.round(chromeRect.top)}px`);\n }\n\n private renderPaperTexture() {\n if (!this.paperTextureLayerActive()) return null;\n return <ds-paper-texture class=\"shell-app__paper-texture\" config={this.paperTexture} />;\n }\n\n private renderManagedPanelNav() {\n const navigation = this.navigation;\n return (\n <ds-panel-nav\n navStyle={this.navStyle}\n groups={this.resolvedNavigationGroups}\n breakpoint={SHELL_DESKTOP_BREAKPOINT}\n routerMode={navigation.routerMode ?? 'event'}\n activeId={navigation.activeId ?? ''}\n currentUrl={navigation.currentUrl ?? this.pageChrome.currentUrl ?? ''}\n storageKey={navigation.storageKey ?? ''}\n userName={navigation.userName ?? ''}\n userInitial={navigation.userInitial ?? ''}\n accountMenuExpanded={navigation.accountMenuExpanded ?? false}\n dashboardLabel={navigation.dashboardLabel ?? 'Dashboard'}\n settingsLabel={navigation.settingsLabel ?? 'Settings'}\n accountLabel={navigation.accountLabel ?? 'Account'}\n dashboardNavigationLabel={\n navigation.dashboardNavigationLabel ?? 'Dashboard navigation'\n }\n settingsNavigationLabel={\n navigation.settingsNavigationLabel ?? 'Settings navigation'\n }\n navigationItemsLabel={navigation.navigationLabel ?? 'Navigation items'}\n onDsNavSelect={this.handleManagedNavSelect}\n onDsNavFooterAction={this.handleManagedNavFooterAction}\n onDsNavUserAction={this.handleManagedNavUserAction}\n />\n );\n }\n\n private renderManagedBarNav() {\n return (\n <ds-bar-nav\n navStyle={this.navStyle}\n tabs={this.pageChrome.tabs ?? []}\n value={this.pageChrome.value ?? ''}\n heading={this.pageChrome.routeHeading ?? this.pageChrome.heading}\n basePath={this.pageChrome.basePath ?? ''}\n currentUrl={this.pageChrome.currentUrl ?? this.navigation.currentUrl ?? ''}\n onDsTabChange={this.handleManagedTabChange}\n />\n );\n }\n\n private renderManagedToolSlots() {\n const ids = [...new Set(this.resolvedToolItems.map(item => item.id))].sort();\n return ids.flatMap(id => [\n <div key={`tool:${id}`} class=\"shell-app__tool-slot-proxy\" slot={id}>\n <slot name={id} />\n </div>,\n <div\n key={`tool-view:${id}`}\n class=\"shell-app__tool-slot-proxy\"\n slot={`${id}-view`}\n >\n <slot name={`${id}-view`} />\n </div>,\n ]);\n }\n\n private renderManagedTools() {\n const tools = this.tools;\n return (\n <ds-shell-tools\n responsiveMode={this.resolvedMode}\n open={this.managedToolsOpen}\n activeTool={this.managedActiveTool}\n presentation={this.managedToolPresentation}\n fullscreenHeaderMode={tools.fullscreenHeaderMode ?? 'shared'}\n items={this.resolvedToolItems}\n headers={tools.headers ?? {}}\n storageKey={tools.storageKey ?? ''}\n toolsLabel={tools.toolsLabel ?? 'Tools'}\n toolShortcutsLabel={tools.toolShortcutsLabel ?? 'Tool shortcuts'}\n inboxLabel={tools.inboxLabel ?? 'Inbox'}\n inboxNavigationLabel={tools.inboxNavigationLabel ?? 'Inbox sections'}\n onDsToolChange={this.handleManagedToolChange}\n onDsPresentationChange={this.handleManagedPresentationChange}\n onDsHeaderBack={this.handleManagedHeaderBack}\n onDsHeaderAction={this.handleManagedHeaderAction}\n >\n {this.renderManagedToolSlots()}\n </ds-shell-tools>\n );\n }\n\n private renderManagedMobileSheetNav() {\n const navigation = this.navigation;\n return (\n <ds-mobile-sheet-nav\n open={this.managedMobileSheetNavOpen}\n browseContext={this.managedBrowseContext}\n dashboardGroups={navigation.dashboardGroups ?? navigation.groups ?? []}\n settingsGroups={navigation.settingsGroups ?? []}\n currentUrl={navigation.currentUrl ?? this.pageChrome.currentUrl ?? ''}\n routeSelectionActive={this.managedMobileDestination === 'area'}\n navigationLabel={navigation.navigationLabel ?? 'Application navigation'}\n dashboardLabel={navigation.dashboardLabel ?? 'Dashboard'}\n settingsLabel={navigation.settingsLabel ?? 'Settings'}\n accountLabel={navigation.accountLabel ?? 'Account'}\n showAccount={navigation.showMobileAccount ?? true}\n onDsAreaSelect={this.handleManagedAreaSelect}\n onDsBrowseContextChange={this.handleManagedBrowseContext}\n onDsClose={this.handleManagedSheetClose}\n />\n );\n }\n\n private renderManagedMobileBarNav() {\n const item = (id: PanelToolsToolId) =>\n this.resolvedToolItems.find(candidate => candidate.id === id);\n const label = (id: PanelToolsToolId, fallback: string) =>\n item(id)?.ariaLabel ?? item(id)?.label ?? fallback;\n const directActivity = this.resolvedToolItems.some(\n candidate => !candidate.isInactive && candidate.mobileDestination === 'activity'\n );\n\n return (\n <ds-mobile-bar-nav\n hidden={this.tools.mobileBarHidden ?? false}\n activeDestination={this.managedMobileDestination}\n currentArea={this.currentArea}\n sheetNavExpanded={this.managedMobileSheetNavOpen}\n searchLabel={label('search', 'Search')}\n activityMode={directActivity ? 'direct' : 'inbox'}\n activityLabel={label('activity', 'Activity')}\n inboxLabel={this.tools.inboxLabel ?? 'Inbox'}\n messagesLabel={label('messages', 'Messages')}\n agentsLabel={label('agents', 'Agents')}\n helpLabel={label('help', 'Help & Support')}\n searchDot={this.toolDot('search')}\n activityDot={this.toolDot('activity')}\n inboxDot={\n this.toolDot('stacks') || this.toolDot('activity')\n }\n messagesDot={this.toolDot('messages')}\n agentsDot={this.toolDot('agents')}\n onDsSheetNavToggle={this.handleManagedSheetToggle}\n onDsDestinationChange={this.handleManagedMobileDestination}\n />\n );\n }\n\n private renderManagedMobileLeading() {\n if (!this.pageChrome.showBack) {\n return <slot name=\"page-header-leading\" slot=\"leading\" />;\n }\n return [\n <ds-tooltip\n slot=\"leading\"\n label={this.pageChrome.backAriaLabel ?? 'Back'}\n side=\"bottom\"\n size=\"sm\"\n >\n <ds-button-unfilled\n variant=\"icon\"\n icon={this.pageChrome.backIcon ?? 'ChevronLeft'}\n size=\"md\"\n aria-label={this.pageChrome.backAriaLabel ?? 'Back'}\n activeFill={false}\n hasBorder={false}\n onDsClick={(event: CustomEvent<MouseEvent>) =>\n this.dsPageBack.emit(event.detail)\n }\n />\n </ds-tooltip>,\n <slot name=\"page-header-leading\" slot=\"leading\" />,\n ];\n }\n\n private renderManagedMobileActions() {\n const actions = this.pageChrome.mobileActions ?? [];\n return [\n ...actions.map((action: PanelToolsHeaderAction) => (\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 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 isInactive={action.isInactive}\n activeFill={false}\n hasBorder={false}\n onDsClick={() => this.dsPageAction.emit(action.id)}\n />\n </ds-tooltip>\n )),\n <slot name=\"page-header-trailing\" slot=\"trailing\" />,\n ];\n }\n\n private renderManagedPage() {\n const page = this.pageChrome;\n const mobileSections = page.showBack ? [] : (page.tabs ?? []);\n const barTitleSections: BarTitleSectionItem[] = (page.subsections ?? []).map(item =>\n 'type' in item\n ? { type: 'divider' }\n : {\n id: item.id,\n label: item.label,\n isInactive: item.isInactive,\n }\n );\n return (\n <ds-shell-page\n responsiveMode={this.resolvedMode}\n headerCapacity={resolveManagedShellPageCapacity(this.resolvedMode)}\n contentInset={page.contentInset ?? 'default'}\n contentInsetBlockStart={page.contentInsetBlockStart ?? 'default'}\n contentInsetBlockStartSize={page.contentInsetBlockStartSize}\n compactContentInsetBlockStartSize={page.compactContentInsetBlockStartSize}\n scrollCompaction={page.scrollCompaction ?? true}\n contentSurface={page.contentSurface ?? 'primary'}\n >\n <ds-bar-title\n slot=\"header\"\n heading={page.heading ?? ''}\n description={page.description ?? ''}\n showBack={page.showBack ?? false}\n backAriaLabel={page.backAriaLabel ?? 'Back'}\n backLabel={page.backLabel ?? 'Back'}\n breadcrumbs={page.breadcrumbs ?? []}\n breadcrumbAriaLabel={page.breadcrumbAriaLabel ?? 'Breadcrumb'}\n sections={barTitleSections}\n value={page.subvalue ?? ''}\n sectionsAriaLabel={page.subsectionsAriaLabel ?? 'Change page subsection'}\n primaryAction={page.primaryAction ?? null}\n actions={page.actions ?? []}\n actionsAriaLabel={page.actionsAriaLabel ?? 'More page actions'}\n showDivider={page.showHeaderDivider ?? true}\n showCompactDivider={page.showCompactHeaderDivider}\n onDsBack={this.handleManagedPageBack}\n onDsBreadcrumbSelect={this.handleManagedBreadcrumbSelect}\n onDsSectionChange={this.handleManagedSubsectionChange}\n onDsAction={this.handleManagedPageAction}\n />\n <ds-mobile-header\n slot=\"mobile-header\"\n heading={page.heading ?? ''}\n headingLevel={page.headingLevel ?? 'h1'}\n sections={mobileSections}\n value={page.showBack ? '' : (page.value ?? '')}\n sectionsAriaLabel={page.sectionsAriaLabel ?? 'Change page section'}\n subsections={page.subsections ?? []}\n subvalue={page.subvalue ?? ''}\n subsectionsAriaLabel={\n page.subsectionsAriaLabel ?? 'Change page subsection'\n }\n tone={page.tone ?? 'default'}\n onDsSectionChange={this.handleManagedTabChange}\n onDsSubsectionChange={this.handleManagedSubsectionChange}\n >\n {this.renderManagedMobileLeading()}\n {this.renderManagedMobileActions()}\n </ds-mobile-header>\n <slot />\n </ds-shell-page>\n );\n }\n\n private renderSlottedShell(\n shellCls: Record<string, boolean>,\n mobile: boolean,\n mobileToolActive: boolean,\n mobileStageBlocked: boolean,\n fullscreen: boolean\n ) {\n return (\n <Host class={shellCls} responsive-mode={this.resolvedMode}>\n <div class=\"shell-app__banner\">\n <slot name=\"banner\" />\n </div>\n <div class=\"shell-app__row\">\n <div class=\"shell-app__chrome\" aria-hidden=\"true\">\n {this.renderPaperTexture()}\n </div>\n <div\n class=\"shell-app__panel\"\n aria-hidden={fullscreen ? 'true' : undefined}\n inert={fullscreen ? true : undefined}\n >\n <slot name=\"panel\" />\n </div>\n <div class=\"shell-app__main\">\n <div\n class=\"shell-app__bar\"\n aria-hidden={fullscreen ? 'true' : undefined}\n inert={fullscreen ? true : undefined}\n >\n <slot name=\"bar\" />\n </div>\n <div\n class=\"shell-app__tools\"\n data-ds-overlay-boundary\n aria-hidden={\n mobile && (!mobileToolActive || this.mobileSheetNavOpen)\n ? 'true'\n : undefined\n }\n inert={\n mobile && (!mobileToolActive || this.mobileSheetNavOpen)\n ? true\n : undefined\n }\n hidden={mobile && !mobileToolActive}\n >\n <slot name=\"tools\" />\n </div>\n <div\n class=\"shell-app__content\"\n data-ds-overlay-boundary\n aria-hidden={fullscreen || mobileStageBlocked ? 'true' : undefined}\n inert={fullscreen || mobileStageBlocked ? true : undefined}\n tabIndex={0}\n >\n <slot />\n </div>\n <div\n class=\"shell-app__mobile-sheet-nav\"\n hidden={!mobile || !this.mobileSheetNavOpen}\n aria-hidden={\n mobile && this.mobileSheetNavOpen ? undefined : 'true'\n }\n inert={mobile && this.mobileSheetNavOpen ? undefined : true}\n >\n <slot name=\"mobile-sheet-nav\" />\n </div>\n <div class=\"shell-app__mobile-bar-nav\" hidden={!mobile}>\n <slot name=\"mobile-bar-nav\" />\n </div>\n </div>\n </div>\n </Host>\n );\n }\n\n render() {\n const chromeActive = this.chromeLayerActive();\n const gradientActive = this.gradientLayerActive();\n const paperTextureActive = this.paperTextureLayerActive();\n const mobile = this.resolvedMode === 'mobile';\n const mobileToolActive = mobile && this.effectiveMobileDestination !== 'area';\n const mobileStageBlocked =\n mobile && (mobileToolActive || this.effectiveMobileSheetNavOpen);\n const fullscreen = !mobile && this.toolsFullscreen;\n const shellCls: Record<string, boolean> = {\n 'shell-app': true,\n 'shell-app--chrome': chromeActive,\n 'shell-app--gradient': gradientActive,\n 'shell-app--paper-texture': paperTextureActive,\n [`shell-app--${this.navStyle}`]: true,\n [`shell-app--${this.resolvedMode}`]: true,\n 'shell-app--tools-fullscreen': fullscreen,\n 'shell-app--mobile-tool-active': mobileToolActive,\n 'shell-app--mobile-sheet-nav-open':\n mobile && this.effectiveMobileSheetNavOpen,\n 'shell-app--managed': this.managed,\n 'shell-app--slotted': !this.managed,\n };\n\n if (!this.managed) {\n return this.renderSlottedShell(\n shellCls,\n mobile,\n mobileToolActive,\n mobileStageBlocked,\n fullscreen\n );\n }\n\n return (\n <Host class={shellCls} responsive-mode={this.resolvedMode}>\n <div class=\"shell-app__banner\">\n <slot name=\"banner\" />\n </div>\n <div class=\"shell-app__row\">\n <div class=\"shell-app__chrome\" aria-hidden=\"true\">\n {this.renderPaperTexture()}\n </div>\n <div class=\"shell-app__panel\" aria-hidden={fullscreen ? 'true' : undefined} inert={fullscreen ? true : undefined}>\n {this.renderManagedPanelNav()}\n </div>\n <div class=\"shell-app__main\">\n <div class=\"shell-app__bar\" aria-hidden={fullscreen ? 'true' : undefined} inert={fullscreen ? true : undefined}>\n {this.renderManagedBarNav()}\n </div>\n <div\n class=\"shell-app__tools\"\n data-ds-overlay-boundary\n aria-hidden={\n mobile && (!mobileToolActive || this.effectiveMobileSheetNavOpen)\n ? 'true'\n : undefined\n }\n inert={\n mobile && (!mobileToolActive || this.effectiveMobileSheetNavOpen)\n ? true\n : undefined\n }\n hidden={mobile && !mobileToolActive}\n >\n {this.renderManagedTools()}\n </div>\n <div\n class=\"shell-app__content\"\n data-ds-overlay-boundary\n aria-hidden={fullscreen || mobileStageBlocked ? 'true' : undefined}\n inert={fullscreen || mobileStageBlocked ? true : undefined}\n tabIndex={0}\n >\n {this.renderManagedPage()}\n </div>\n <div\n class=\"shell-app__mobile-sheet-nav\"\n hidden={!mobile || !this.effectiveMobileSheetNavOpen}\n aria-hidden={\n mobile && this.effectiveMobileSheetNavOpen ? undefined : 'true'\n }\n inert={mobile && this.effectiveMobileSheetNavOpen ? undefined : true}\n >\n {this.renderManagedMobileSheetNav()}\n </div>\n <div class=\"shell-app__mobile-bar-nav\" hidden={!mobile}>\n {this.renderManagedMobileBarNav()}\n </div>\n </div>\n </div>\n </Host>\n );\n }\n}\n",
627
+ "content": "import {\n Component,\n Prop,\n Element,\n Event,\n EventEmitter,\n State,\n Watch,\n Listen,\n Method,\n h,\n Host,\n} from '@stencil/core';\nimport type { NavChromeStyle } from '../../shell/nav-chrome';\nimport { isEditableShortcutTarget, resolveShellShortcut } from '../../shell/shell-shortcuts';\nimport type { PanelToolsToolId, PanelToolsHeaderAction } from '../PanelTools/panel-tools-types';\nimport { PANEL_TOOLS_DEFAULT_ITEMS } from '../PanelTools/panel-tools-types';\nimport type { BarTitleSectionItem } from '../BarTitle/bar-title-types';\nimport type { BreadcrumbSelectDetail } from '../Breadcrumb/breadcrumb-types';\nimport {\n DEFAULT_SHELL_GRADIENT_PRESET,\n normalizeShellGradientPreset,\n type ShellGradientPreset,\n} from '../../shell/shell-gradient-presets';\nimport {\n CHROME_TRANSITION_END,\n CHROME_TRANSITION_START,\n ChromeTransitionDepth,\n createRafCoalescer,\n readChromeTransitionSource,\n} from '../../shell/chrome-transition';\nimport {\n isPanelNavCollapsed,\n panelWidthPxFromTokens,\n readPanelNavWidthTokens,\n type PanelNavWidthTokens,\n} from '../../shell/shell-chrome-metrics';\nimport {\n SHELL_GRADIENT_IMAGE_VAR,\n SHELL_GRADIENT_OPACITY_VAR,\n SHELL_GRADIENT_POSITION_BAR_VAR,\n SHELL_GRADIENT_POSITION_PANEL_VAR,\n SHELL_GRADIENT_SIZE_VAR,\n buildShellRadialGradient,\n readShellViewportDimensions,\n shellGradientPositionBar,\n shellGradientPositionPanel,\n shellGradientPresetOpacity,\n shellGradientSize,\n} from '../../shell/shell-gradient';\nimport {\n itemUsesShellInbox,\n resolveAvailableInboxTool,\n resolveManagedShellPageCapacity,\n resolveShellResponsiveMode,\n SHELL_DESKTOP_BREAKPOINT,\n shellMobileDestinationForTool,\n type MobileDestination,\n type ShellInboxToolId,\n type ShellResponsiveMode,\n} from '../../shell/shell-responsive';\nimport { deriveActiveIdFromUrl } from '../PanelNav/panel-nav-utils';\nimport type { PanelNavItem } from '../PanelNav/panel-nav-types';\nimport type { PanelNavUserActionDetail } from '../PanelNav/panel-nav-types';\nimport type { MobileBarNavDestinationDetail } from '../MobileBarNav/mobile-bar-nav-types';\nimport type {\n ShellAppComposition,\n ShellNavigationConfig,\n ShellPageChromeConfig,\n ShellToolsConfig,\n} from './shell-app-types';\nimport type { PaperTextureConfig } from '../PaperTexture/paper-texture-types';\n\n@Component({\n tag: 'ds-shell-app',\n styleUrl: 'ShellApp.css',\n scoped: true,\n})\nexport class ShellApp {\n private static readonly FOREGROUND_REFRESH_MS = 50;\n\n /** Managed renders the complete responsive chrome; slotted exposes the advanced composition. */\n @Prop({ reflect: true }) composition: ShellAppComposition = 'managed';\n\n /** Router-owned navigation data used by managed composition. */\n @Prop() navigation: ShellNavigationConfig = {};\n\n /** Route-owned page title and section data used by managed composition. */\n @Prop({ attribute: 'page-chrome' }) pageChrome: ShellPageChromeConfig = {};\n\n /** Product-owned global tool definitions used by managed composition. */\n @Prop() tools: ShellToolsConfig = {};\n\n /** Chrome style propagated to slotted `ds-panel-nav` and `ds-bar-nav`. */\n @Prop({ attribute: 'nav-style', reflect: true }) navStyle: NavChromeStyle = 'dashboard';\n\n /**\n * Shell chrome wash preset. `none` renders solid chrome; the remaining\n * presets use token-based washes that adapt to the active color theme.\n */\n @Prop({ attribute: 'gradient-preset', reflect: true }) gradientPreset: ShellGradientPreset =\n DEFAULT_SHELL_GRADIENT_PRESET;\n\n /** Optional decorative Paper Design texture layered above the shell wash. */\n @Prop({ attribute: 'paper-texture' }) paperTexture?: PaperTextureConfig;\n\n /** When `true` (default), registers global shell keyboard shortcuts. `[` toggles panel nav; `]` closes tools; K, A, S, M, N, and / toggle tool drawers. Modifiers are ignored so browser chords like ⌘N stay native. */\n @Prop({ attribute: 'shortcuts-enabled' }) shortcutsEnabled: boolean = true;\n\n /** Controlled mobile surface shown above the persistent bottom bar. */\n @Prop({ attribute: 'mobile-destination' })\n mobileDestination: MobileDestination = 'area';\n\n /** Controlled full-stage Mobile Sheet Nav state. */\n @Prop({ attribute: 'mobile-sheet-nav-open' })\n mobileSheetNavOpen: boolean = false;\n\n /** Emitted after crossing the fixed 768px or 1200px shell boundaries. */\n @Event() dsResponsiveModeChange!: EventEmitter<{ mode: ShellResponsiveMode }>;\n\n /** Managed primary-navigation intent; the application router performs navigation. */\n @Event() dsNavSelect!: EventEmitter<string>;\n\n /** Managed desktop BarNav or mobile section-selection intent. */\n @Event() dsTabChange!: EventEmitter<string>;\n\n /** Managed page-subsection intent from BarTitle or MobileHeader. */\n @Event() dsSubsectionChange!: EventEmitter<string>;\n\n /** Managed page-level Back intent. */\n @Event() dsPageBack!: EventEmitter<MouseEvent>;\n\n /** Managed breadcrumb intent from BarTitle. */\n @Event({ cancelable: true })\n dsBreadcrumbSelect!: EventEmitter<BreadcrumbSelectDetail>;\n\n /** Managed page action intent from BarTitle. */\n @Event() dsPageAction!: EventEmitter<string>;\n\n /** Managed tools selection state. */\n @Event() dsToolChange!: EventEmitter<{ id: PanelToolsToolId; selected: boolean }>;\n\n /** Managed tool-header Back intent. */\n @Event() dsHeaderBack!: EventEmitter<{ tool: PanelToolsToolId }>;\n\n /** Managed tool-header action intent. */\n @Event() dsHeaderAction!: EventEmitter<{ tool: PanelToolsToolId; id: string }>;\n\n /** Managed drawer/fullscreen presentation state. */\n @Event() dsPresentationChange!: EventEmitter<{ presentation: 'drawer' | 'fullscreen' }>;\n\n /** Managed mobile Dashboard/Settings browsing intent. */\n @Event() dsBrowseContextChange!: EventEmitter<NavChromeStyle>;\n\n /** Managed account-footer action intent. */\n @Event() dsNavFooterAction!: EventEmitter<void>;\n\n /** Managed user/account trigger intent. */\n @Event() dsNavUserAction!: EventEmitter<PanelNavUserActionDetail>;\n\n @Element() el!: HTMLElement;\n @State() private toolsFullscreen = false;\n @State() private resolvedMode: ShellResponsiveMode = 'desktop';\n @State() private managedToolsOpen = false;\n @State() private managedActiveTool: PanelToolsToolId | '' = '';\n @State() private managedToolPresentation: 'drawer' | 'fullscreen' = 'drawer';\n @State() private managedMobileDestination: MobileDestination = 'area';\n @State() private managedMobileSheetNavOpen = false;\n @State() private managedInboxTool: ShellInboxToolId | '' = '';\n @State() private managedBrowseContext: NavChromeStyle = 'dashboard';\n\n private resizeObserver: ResizeObserver | null = null;\n private hasLoaded = false;\n private foregroundRefreshTimer: number | null = null;\n private readonly panelNavTransition = new ChromeTransitionDepth();\n private readonly chromeSyncCoalescer = createRafCoalescer(() => this.syncChrome());\n private panelWidthTokens: PanelNavWidthTokens = { expandedPx: 0, collapsedPx: 0 };\n private cachedViewportWidth = 0;\n private cachedViewportHeight = 0;\n private onWindowResize = () => {\n this.updateResponsiveMode();\n if (this.panelNavTransition.isActive) return;\n this.scheduleChromeSync();\n };\n private onVisualViewportChange = () => {\n if (this.panelNavTransition.isActive) return;\n this.scheduleChromeSync();\n };\n\n private get managed(): boolean {\n return this.composition === 'managed';\n }\n\n private get effectiveMobileDestination(): MobileDestination {\n return this.managed ? this.managedMobileDestination : this.mobileDestination;\n }\n\n private get effectiveMobileSheetNavOpen(): boolean {\n return this.managed ? this.managedMobileSheetNavOpen : this.mobileSheetNavOpen;\n }\n\n private get resolvedNavigationGroups() {\n if (this.navigation.groups?.length) return this.navigation.groups;\n return this.managedBrowseContext === 'settings'\n ? (this.navigation.settingsGroups ?? [])\n : (this.navigation.dashboardGroups ?? []);\n }\n\n private get resolvedToolItems() {\n return this.tools.items ?? PANEL_TOOLS_DEFAULT_ITEMS;\n }\n\n private get availableInboxTools(): ShellInboxToolId[] {\n return this.resolvedToolItems\n .filter(item => !item.isInactive && itemUsesShellInbox(item))\n .map(item => item.id as ShellInboxToolId);\n }\n\n componentWillLoad() {\n if (typeof window !== 'undefined') {\n this.resolvedMode = resolveShellResponsiveMode(window.innerWidth);\n }\n this.managedBrowseContext = this.navigation.browseContext ?? this.navStyle;\n this.managedMobileDestination = this.mobileDestination;\n this.managedMobileSheetNavOpen = this.mobileSheetNavOpen;\n this.managedInboxTool = resolveAvailableInboxTool('', this.availableInboxTools);\n }\n\n componentDidLoad() {\n this.hasLoaded = true;\n this.syncSlottedNavStyle();\n this.syncSlottedMobileState();\n this.connectMetricsObserver();\n this.connectViewportListeners();\n this.connectPageLifecycleListeners();\n this.el.addEventListener(CHROME_TRANSITION_START, this.onChromeTransitionStart);\n this.el.addEventListener(CHROME_TRANSITION_END, this.onChromeTransitionEnd);\n requestAnimationFrame(() => {\n this.cachePanelWidthTokens();\n this.scheduleChromeSync();\n });\n const tools = this.el.querySelector('ds-shell-tools, ds-panel-tools');\n this.toolsFullscreen = tools?.getAttribute('presentation') === 'fullscreen';\n }\n\n @Listen('dsPresentationChange')\n handleToolsPresentation(event: CustomEvent<{ presentation?: string }>) {\n this.toolsFullscreen = event.detail?.presentation === 'fullscreen';\n }\n\n disconnectedCallback() {\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n this.disconnectViewportListeners();\n this.disconnectPageLifecycleListeners();\n this.cancelForegroundRefresh();\n this.chromeSyncCoalescer.cancel();\n this.el.removeEventListener(CHROME_TRANSITION_START, this.onChromeTransitionStart);\n this.el.removeEventListener(CHROME_TRANSITION_END, this.onChromeTransitionEnd);\n }\n\n @Watch('navStyle')\n @Watch('gradientPreset')\n @Watch('paperTexture')\n onShellPropsChange() {\n this.syncSlottedNavStyle();\n this.scheduleChromeSync();\n }\n\n @Watch('navigation')\n handleNavigationChange() {\n this.managedBrowseContext = this.navigation.browseContext ?? this.navStyle;\n }\n\n @Watch('tools')\n handleToolsChange() {\n if (\n this.managedActiveTool &&\n !this.resolvedToolItems.some(item => item.id === this.managedActiveTool)\n ) {\n this.managedToolsOpen = false;\n this.managedActiveTool = '';\n this.managedMobileDestination = 'area';\n }\n this.managedInboxTool = resolveAvailableInboxTool(\n this.managedInboxTool,\n this.availableInboxTools\n );\n }\n\n @Watch('mobileDestination')\n @Watch('mobileSheetNavOpen')\n onMobileStateChange(_next: unknown, previous: unknown) {\n if (this.managed) {\n this.managedMobileDestination = this.mobileDestination;\n this.managedMobileSheetNavOpen = this.mobileSheetNavOpen;\n }\n this.syncSlottedMobileState();\n if (previous === true && !this.effectiveMobileSheetNavOpen && this.resolvedMode === 'mobile') {\n requestAnimationFrame(() => {\n const bar = this.el.querySelector('ds-mobile-bar-nav') as\n | (HTMLElement & {\n focusDestination?: (destination: MobileDestination | 'sheet-nav') => Promise<void>;\n })\n | null;\n const activeElement = document.activeElement;\n if (activeElement instanceof HTMLElement && bar?.contains(activeElement)) return;\n void bar?.focusDestination?.('sheet-nav');\n });\n }\n }\n\n private updateResponsiveMode() {\n if (typeof window === 'undefined') return;\n const next = resolveShellResponsiveMode(window.innerWidth);\n if (next === this.resolvedMode) return;\n this.resolvedMode = next;\n this.syncSlottedMobileState();\n this.scheduleChromeSync();\n if (this.hasLoaded) this.dsResponsiveModeChange.emit({ mode: next });\n }\n\n private onChromeTransitionStart = (event: Event) => {\n const source = readChromeTransitionSource(event);\n if (source !== 'panel-nav') return;\n\n this.panelNavTransition.enter();\n const viewport = readShellViewportDimensions();\n this.cachedViewportWidth = viewport.width;\n this.cachedViewportHeight = viewport.height;\n this.syncChrome();\n };\n\n private onChromeTransitionEnd = (event: Event) => {\n const source = readChromeTransitionSource(event);\n if (source !== 'panel-nav') return;\n\n this.panelNavTransition.exit();\n if (!this.panelNavTransition.isActive) {\n this.scheduleChromeSync();\n }\n };\n\n /** Coalesce ResizeObserver bursts to one layout read per frame. */\n private scheduleChromeSync() {\n this.chromeSyncCoalescer.schedule();\n }\n\n private syncSlottedNavStyle() {\n const panel = this.el.querySelector('ds-panel-nav') as\n | (HTMLElement & { navStyle: NavChromeStyle })\n | null;\n const bar = this.el.querySelector('ds-bar-nav') as\n | (HTMLElement & { navStyle: NavChromeStyle })\n | null;\n if (panel) {\n panel.setAttribute('nav-style', this.navStyle);\n panel.navStyle = this.navStyle;\n }\n if (bar) {\n bar.setAttribute('nav-style', this.navStyle);\n bar.navStyle = this.navStyle;\n }\n }\n\n private syncSlottedMobileState() {\n const tools = this.el.querySelector('ds-shell-tools') as\n | (HTMLElement & { responsiveMode: ShellResponsiveMode })\n | null;\n const bar = this.el.querySelector('ds-mobile-bar-nav') as\n | (HTMLElement & {\n activeDestination: MobileDestination;\n sheetNavExpanded: boolean;\n })\n | null;\n const sheetNav = this.el.querySelector('ds-mobile-sheet-nav') as\n | (HTMLElement & { open: boolean })\n | null;\n\n if (tools) {\n tools.setAttribute('responsive-mode', this.resolvedMode);\n tools.responsiveMode = this.resolvedMode;\n }\n if (bar) {\n bar.activeDestination = this.effectiveMobileDestination;\n bar.sheetNavExpanded = this.effectiveMobileSheetNavOpen;\n }\n if (sheetNav) {\n sheetNav.open = this.effectiveMobileSheetNavOpen;\n }\n }\n\n private connectMetricsObserver() {\n this.resizeObserver = new ResizeObserver(() => {\n if (this.panelNavTransition.isActive) return;\n this.scheduleChromeSync();\n });\n this.resizeObserver.observe(this.el);\n const panelWrap = this.el.querySelector('.shell-app__panel');\n if (panelWrap) this.resizeObserver.observe(panelWrap);\n }\n\n private connectViewportListeners() {\n if (typeof window === 'undefined') return;\n\n window.addEventListener('resize', this.onWindowResize, { passive: true });\n\n const visual = window.visualViewport;\n if (visual) {\n visual.addEventListener('resize', this.onVisualViewportChange, { passive: true });\n visual.addEventListener('scroll', this.onVisualViewportChange, { passive: true });\n }\n }\n\n private disconnectViewportListeners() {\n if (typeof window === 'undefined') return;\n\n window.removeEventListener('resize', this.onWindowResize);\n\n const visual = window.visualViewport;\n if (visual) {\n visual.removeEventListener('resize', this.onVisualViewportChange);\n visual.removeEventListener('scroll', this.onVisualViewportChange);\n }\n }\n\n private connectPageLifecycleListeners() {\n if (typeof window === 'undefined' || typeof document === 'undefined') return;\n document.addEventListener('visibilitychange', this.onDocumentVisibilityChange);\n window.addEventListener('pageshow', this.onPageShow);\n }\n\n private disconnectPageLifecycleListeners() {\n if (typeof window === 'undefined' || typeof document === 'undefined') return;\n document.removeEventListener('visibilitychange', this.onDocumentVisibilityChange);\n window.removeEventListener('pageshow', this.onPageShow);\n }\n\n private onDocumentVisibilityChange = () => {\n if (!document.hidden) this.refreshAfterForegroundRestore();\n };\n\n private onPageShow = (event: PageTransitionEvent) => {\n if (event.persisted) this.refreshAfterForegroundRestore();\n };\n\n private cancelForegroundRefresh() {\n if (this.foregroundRefreshTimer !== null) {\n window.clearTimeout(this.foregroundRefreshTimer);\n this.foregroundRefreshTimer = null;\n }\n this.el.classList.remove('shell-app--foreground-refresh');\n }\n\n private refreshAfterForegroundRestore() {\n this.updateResponsiveMode();\n this.syncSlottedMobileState();\n this.scheduleChromeSync();\n\n if (this.resolvedMode !== 'mobile') return;\n\n /*\n * Mobile WebKit can restore a live, correctly sized DOM without repainting\n * its suspended shell layer. Re-promote the complete mobile stage for one\n * painted frame; routed and tool owners remain mounted and keep their state.\n */\n this.cancelForegroundRefresh();\n this.el.classList.add('shell-app--foreground-refresh');\n void this.el.offsetHeight;\n // A short timer spans a real compositor commit; resumed WebKit may coalesce\n // nested animation frames before presenting either one.\n this.foregroundRefreshTimer = window.setTimeout(() => {\n this.foregroundRefreshTimer = null;\n this.el.classList.remove('shell-app--foreground-refresh');\n // WebKit otherwise may keep presenting the retired composite layer.\n void this.el.querySelector<HTMLElement>('.shell-app__main')?.offsetHeight;\n }, ShellApp.FOREGROUND_REFRESH_MS);\n }\n\n private cachePanelWidthTokens() {\n const navRoot = this.el.querySelector('ds-panel-nav .panel-nav') as HTMLElement | null;\n if (!navRoot) return;\n this.panelWidthTokens = readPanelNavWidthTokens(navRoot);\n }\n\n private applyGradientVars(target: HTMLElement, vars: Record<string, string | null>) {\n const style = target.style;\n for (const [name, value] of Object.entries(vars)) {\n if (value === null) style.removeProperty(name);\n else style.setProperty(name, value);\n }\n }\n\n private clearGradientPaintVars(targets: HTMLElement[]) {\n const keys = [SHELL_GRADIENT_IMAGE_VAR, SHELL_GRADIENT_SIZE_VAR, SHELL_GRADIENT_OPACITY_VAR];\n for (const target of targets) {\n for (const key of keys) target.style.removeProperty(key);\n }\n }\n\n /** Panel/bar wash offsets for scroll fades and badge rings. */\n private syncChromeLayoutVars(\n panel: HTMLElement | null,\n bar: HTMLElement | null,\n panelPosition: string,\n barPosition: string\n ) {\n if (panel) {\n panel.style.setProperty(SHELL_GRADIENT_POSITION_PANEL_VAR, panelPosition);\n }\n if (bar) {\n bar.style.setProperty(SHELL_GRADIENT_POSITION_BAR_VAR, barPosition);\n }\n }\n\n private resolvePanelWidthPx(panelNav: HTMLElement | null): number {\n const navRoot = panelNav?.querySelector('.panel-nav') as HTMLElement | null;\n\n if (this.panelNavTransition.isActive && this.panelWidthTokens.expandedPx > 0) {\n const collapsed = isPanelNavCollapsed(panelNav, navRoot);\n return panelWidthPxFromTokens(this.panelWidthTokens, collapsed);\n }\n\n const panelWrap = this.el.querySelector('.shell-app__panel') as HTMLElement | null;\n const measured = panelWrap?.getBoundingClientRect().width ?? 0;\n if (measured > 0) return measured;\n\n if (this.panelWidthTokens.expandedPx > 0) {\n const collapsed = isPanelNavCollapsed(panelNav, navRoot);\n return panelWidthPxFromTokens(this.panelWidthTokens, collapsed);\n }\n\n return 0;\n }\n\n /** Fixed-attachment wash/grid size — always the viewport, never the shell element box. */\n private resolveViewportDimensions(): { width: number; height: number } {\n if (this.panelNavTransition.isActive && this.cachedViewportWidth > 0) {\n return {\n width: this.cachedViewportWidth,\n height: this.cachedViewportHeight,\n };\n }\n\n return readShellViewportDimensions();\n }\n\n private chromeLayerActive(): boolean {\n return this.gradientLayerActive() || this.paperTextureLayerActive();\n }\n\n private gradientLayerActive(): boolean {\n return (\n this.resolvedMode !== 'mobile' && normalizeShellGradientPreset(this.gradientPreset) !== 'none'\n );\n }\n\n private paperTextureLayerActive(): boolean {\n return this.resolvedMode !== 'mobile' && this.paperTexture != null;\n }\n\n @Listen('keydown', { target: 'window', capture: true })\n handleWindowKeyDown(e: KeyboardEvent) {\n if (!this.shortcutsEnabled) return;\n if (isEditableShortcutTarget(e.target)) return;\n\n const action = resolveShellShortcut(e);\n if (!action) return;\n\n e.preventDefault();\n\n const panel = this.el.querySelector('ds-panel-nav') as HTMLDsPanelNavElement | null;\n const tools = this.el.querySelector('ds-shell-tools, ds-panel-tools') as\n | (HTMLElement & {\n closeDrawer?: () => Promise<void>;\n activateTool?: (id: PanelToolsToolId) => Promise<void>;\n })\n | null;\n\n if (action === 'toggle-panel-nav') {\n void panel?.toggleCollapsed();\n return;\n }\n\n if (action === 'close-panel-tools') {\n void tools?.closeDrawer?.();\n return;\n }\n\n if (action.startsWith('open-tool:') && tools) {\n const toolId = action.slice('open-tool:'.length) as PanelToolsToolId;\n void tools.activateTool?.(toolId);\n }\n }\n\n /** Open or toggle a managed global tool from application-owned UI. */\n @Method()\n async activateTool(id: PanelToolsToolId) {\n const tools = this.el.querySelector('ds-shell-tools, ds-panel-tools') as\n | (HTMLElement & { activateTool?: (tool: PanelToolsToolId) => Promise<void> })\n | null;\n await tools?.activateTool?.(id);\n }\n\n /** Close whichever managed global surface is currently visible. */\n @Method()\n async closeGlobalSurface() {\n if (this.managedMobileSheetNavOpen) {\n this.managedMobileSheetNavOpen = false;\n return;\n }\n const tools = this.el.querySelector('ds-shell-tools, ds-panel-tools') as\n | (HTMLElement & { closeDrawer?: () => Promise<void> })\n | null;\n await tools?.closeDrawer?.();\n }\n\n /** Request drawer or fullscreen presentation for the active managed tool. */\n @Method()\n async setToolPresentation(presentation: 'drawer' | 'fullscreen') {\n this.managedToolPresentation = presentation;\n this.toolsFullscreen = presentation === 'fullscreen';\n }\n\n private handleManagedNavSelect = (event: CustomEvent<string>) => {\n event.stopPropagation();\n this.dsNavSelect.emit(event.detail);\n };\n\n private handleManagedTabChange = (event: CustomEvent<string>) => {\n event.stopPropagation();\n this.dsTabChange.emit(event.detail);\n };\n\n private handleManagedPageBack = (event: CustomEvent<MouseEvent>) => {\n event.stopPropagation();\n this.dsPageBack.emit(event.detail);\n };\n\n private handleManagedBreadcrumbSelect = (event: CustomEvent<BreadcrumbSelectDetail>) => {\n event.stopPropagation();\n const forwarded = this.dsBreadcrumbSelect.emit(event.detail);\n if (forwarded.defaultPrevented) event.preventDefault();\n };\n\n private handleManagedSubsectionChange = (event: CustomEvent<string>) => {\n event.stopPropagation();\n this.dsSubsectionChange.emit(event.detail);\n };\n\n private handleManagedPageAction = (event: CustomEvent<string>) => {\n event.stopPropagation();\n this.dsPageAction.emit(event.detail);\n };\n\n private handleManagedToolChange = (\n event: CustomEvent<{ id: PanelToolsToolId; selected: boolean }>\n ) => {\n event.stopPropagation();\n const { id, selected } = event.detail;\n this.managedActiveTool = id;\n this.managedToolsOpen = selected;\n const item = this.resolvedToolItems.find(candidate => candidate.id === id);\n if (item && itemUsesShellInbox(item)) {\n this.managedInboxTool = id;\n }\n this.managedMobileDestination = selected\n ? (item?.mobileDestination ?? shellMobileDestinationForTool(true, id))\n : 'area';\n this.managedMobileSheetNavOpen = false;\n this.dsToolChange.emit(event.detail);\n };\n\n private handleManagedPresentationChange = (\n event: CustomEvent<{ presentation: 'drawer' | 'fullscreen' }>\n ) => {\n event.stopPropagation();\n this.managedToolPresentation = event.detail.presentation;\n this.toolsFullscreen = event.detail.presentation === 'fullscreen';\n this.dsPresentationChange.emit(event.detail);\n };\n\n private handleManagedHeaderBack = (event: CustomEvent<{ tool: PanelToolsToolId }>) => {\n event.stopPropagation();\n this.dsHeaderBack.emit(event.detail);\n };\n\n private handleManagedHeaderAction = (\n event: CustomEvent<{ tool: PanelToolsToolId; id: string }>\n ) => {\n event.stopPropagation();\n this.dsHeaderAction.emit(event.detail);\n };\n\n private handleManagedSheetToggle = (event: CustomEvent<boolean>) => {\n event.stopPropagation();\n this.managedMobileSheetNavOpen = event.detail;\n };\n\n private activateManagedMobileTool(destination: Exclude<MobileDestination, 'area'>): boolean {\n const canonicalId =\n destination === 'inbox'\n ? resolveAvailableInboxTool(this.managedInboxTool, this.availableInboxTools)\n : destination;\n if (!canonicalId) return false;\n\n const item = this.resolvedToolItems.find(\n candidate =>\n candidate.id === canonicalId ||\n (destination !== 'inbox' && candidate.mobileDestination === destination)\n );\n if (!item || item.isInactive) return false;\n\n const id = item.id;\n this.managedActiveTool = id;\n this.managedToolsOpen = true;\n if (itemUsesShellInbox(item)) this.managedInboxTool = id;\n this.dsToolChange.emit({ id: id as PanelToolsToolId, selected: true });\n return true;\n }\n\n private handleManagedMobileDestination = (event: CustomEvent<MobileBarNavDestinationDetail>) => {\n event.stopPropagation();\n const destination = event.detail.destination;\n this.managedMobileSheetNavOpen = false;\n this.managedMobileDestination = destination;\n\n if (destination === 'area') {\n this.managedToolsOpen = false;\n return;\n }\n\n this.activateManagedMobileTool(destination);\n };\n\n private handleManagedAreaSelect = (event: CustomEvent<string>) => {\n event.stopPropagation();\n if (event.detail === 'help') {\n this.managedMobileSheetNavOpen = false;\n this.managedMobileDestination = 'help';\n if (this.activateManagedMobileTool('help')) return;\n }\n\n this.managedMobileSheetNavOpen = false;\n this.managedMobileDestination = 'area';\n this.managedToolsOpen = false;\n this.dsNavSelect.emit(event.detail);\n };\n\n private handleManagedBrowseContext = (event: CustomEvent<NavChromeStyle>) => {\n event.stopPropagation();\n this.managedBrowseContext = event.detail;\n this.dsBrowseContextChange.emit(event.detail);\n };\n\n private handleManagedSheetClose = (event: CustomEvent<void>) => {\n event.stopPropagation();\n this.managedMobileSheetNavOpen = false;\n };\n\n private handleManagedNavFooterAction = (event: CustomEvent<void>) => {\n event.stopPropagation();\n this.dsNavFooterAction.emit();\n };\n\n private handleManagedNavUserAction = (event: CustomEvent<PanelNavUserActionDetail>) => {\n event.stopPropagation();\n this.dsNavUserAction.emit(event.detail);\n };\n\n private get currentArea(): PanelNavItem {\n const groups = [\n ...(this.navigation.dashboardGroups ?? []),\n ...(this.navigation.settingsGroups ?? []),\n ...this.resolvedNavigationGroups,\n ];\n const items = groups.flatMap(group => group.items);\n const activeId =\n this.navigation.activeId ||\n deriveActiveIdFromUrl(this.navigation.currentUrl ?? this.pageChrome.currentUrl ?? '', items);\n return (\n items.find(item => item.id === activeId) ??\n items[0] ?? {\n id: 'area',\n icon: 'MapPage',\n label: this.pageChrome.heading || 'Area',\n }\n );\n }\n\n private toolDot(id: PanelToolsToolId): boolean {\n return this.resolvedToolItems.find(item => item.id === id)?.dot ?? false;\n }\n\n private syncChrome() {\n const panelNav = this.el.querySelector('ds-panel-nav') as HTMLElement | null;\n const bar = this.el.querySelector('ds-bar-nav') as HTMLElement | null;\n const targets = [this.el, panelNav, bar].filter((el): el is HTMLElement => el !== null);\n\n const clearLayoutVars = () => {\n if (panelNav) panelNav.style.removeProperty(SHELL_GRADIENT_POSITION_PANEL_VAR);\n if (bar) bar.style.removeProperty(SHELL_GRADIENT_POSITION_BAR_VAR);\n };\n\n const preset = normalizeShellGradientPreset(this.gradientPreset);\n if (this.resolvedMode === 'mobile') {\n this.clearGradientPaintVars(targets);\n clearLayoutVars();\n return;\n }\n\n const viewport = this.resolveViewportDimensions();\n if (preset === 'none') {\n this.clearGradientPaintVars(targets);\n clearLayoutVars();\n this.syncPaperTextureLayout(viewport);\n return;\n }\n\n const panelWidth = this.resolvePanelWidthPx(panelNav);\n\n const panelPosition = shellGradientPositionPanel();\n const barPosition = shellGradientPositionBar(panelWidth);\n\n this.syncChromeLayoutVars(panelNav, bar, panelPosition, barPosition);\n\n const image = buildShellRadialGradient(preset);\n\n const size = shellGradientSize({\n width: viewport.width,\n height: viewport.height,\n });\n const opacity = shellGradientPresetOpacity(preset);\n\n for (const target of targets) {\n if (target === this.el) {\n this.applyGradientVars(target, {\n [SHELL_GRADIENT_IMAGE_VAR]: image,\n [SHELL_GRADIENT_SIZE_VAR]: size,\n [SHELL_GRADIENT_POSITION_PANEL_VAR]: panelPosition,\n [SHELL_GRADIENT_POSITION_BAR_VAR]: barPosition,\n [SHELL_GRADIENT_OPACITY_VAR]: opacity,\n });\n continue;\n }\n\n const isBar = target.tagName.toLowerCase() === 'ds-bar-nav';\n this.applyGradientVars(target, {\n [SHELL_GRADIENT_IMAGE_VAR]: image,\n [SHELL_GRADIENT_SIZE_VAR]: size,\n [SHELL_GRADIENT_OPACITY_VAR]: opacity,\n [SHELL_GRADIENT_POSITION_PANEL_VAR]: isBar ? null : panelPosition,\n [SHELL_GRADIENT_POSITION_BAR_VAR]: isBar ? barPosition : null,\n });\n }\n\n this.syncPaperTextureLayout(viewport);\n }\n\n private syncPaperTextureLayout(viewport: { width: number; height: number }) {\n const chrome = this.el.querySelector<HTMLElement>('.shell-app__chrome');\n const paperTexture = this.el.querySelector<HTMLElement>(\n '.shell-app__chrome > ds-paper-texture'\n );\n if (!chrome || !paperTexture) return;\n\n const chromeRect = chrome.getBoundingClientRect();\n paperTexture.style.setProperty('width', `${Math.round(viewport.width)}px`);\n paperTexture.style.setProperty('height', `${Math.round(viewport.height)}px`);\n paperTexture.style.setProperty('left', `${-Math.round(chromeRect.left)}px`);\n paperTexture.style.setProperty('top', `${-Math.round(chromeRect.top)}px`);\n }\n\n private renderPaperTexture() {\n if (!this.paperTextureLayerActive()) return null;\n return <ds-paper-texture class=\"shell-app__paper-texture\" config={this.paperTexture} />;\n }\n\n private renderManagedPanelNav() {\n const navigation = this.navigation;\n return (\n <ds-panel-nav\n navStyle={this.navStyle}\n groups={this.resolvedNavigationGroups}\n breakpoint={SHELL_DESKTOP_BREAKPOINT}\n routerMode={navigation.routerMode ?? 'event'}\n activeId={navigation.activeId ?? ''}\n currentUrl={navigation.currentUrl ?? this.pageChrome.currentUrl ?? ''}\n storageKey={navigation.storageKey ?? ''}\n userName={navigation.userName ?? ''}\n userInitial={navigation.userInitial ?? ''}\n accountMenuExpanded={navigation.accountMenuExpanded ?? false}\n dashboardLabel={navigation.dashboardLabel ?? 'Dashboard'}\n settingsLabel={navigation.settingsLabel ?? 'Settings'}\n accountLabel={navigation.accountLabel ?? 'Account'}\n dashboardNavigationLabel={navigation.dashboardNavigationLabel ?? 'Dashboard navigation'}\n settingsNavigationLabel={navigation.settingsNavigationLabel ?? 'Settings navigation'}\n navigationItemsLabel={navigation.navigationLabel ?? 'Navigation items'}\n onDsNavSelect={this.handleManagedNavSelect}\n onDsNavFooterAction={this.handleManagedNavFooterAction}\n onDsNavUserAction={this.handleManagedNavUserAction}\n />\n );\n }\n\n private renderManagedBarNav() {\n return (\n <ds-bar-nav\n navStyle={this.navStyle}\n tabs={this.pageChrome.tabs ?? []}\n value={this.pageChrome.value ?? ''}\n heading={this.pageChrome.routeHeading ?? this.pageChrome.heading}\n basePath={this.pageChrome.basePath ?? ''}\n currentUrl={this.pageChrome.currentUrl ?? this.navigation.currentUrl ?? ''}\n onDsTabChange={this.handleManagedTabChange}\n />\n );\n }\n\n private renderManagedToolSlots() {\n const ids = [...new Set(this.resolvedToolItems.map(item => item.id))].sort();\n return ids.flatMap(id => [\n <div key={`tool:${id}`} class=\"shell-app__tool-slot-proxy\" slot={id}>\n <slot name={id} />\n </div>,\n <div key={`tool-view:${id}`} class=\"shell-app__tool-slot-proxy\" slot={`${id}-view`}>\n <slot name={`${id}-view`} />\n </div>,\n ]);\n }\n\n private renderManagedTools() {\n const tools = this.tools;\n return (\n <ds-shell-tools\n responsiveMode={this.resolvedMode}\n open={this.managedToolsOpen}\n activeTool={this.managedActiveTool}\n presentation={this.managedToolPresentation}\n fullscreenHeaderMode={tools.fullscreenHeaderMode ?? 'shared'}\n items={this.resolvedToolItems}\n headers={tools.headers ?? {}}\n storageKey={tools.storageKey ?? ''}\n toolsLabel={tools.toolsLabel ?? 'Tools'}\n toolShortcutsLabel={tools.toolShortcutsLabel ?? 'Tool shortcuts'}\n inboxLabel={tools.inboxLabel ?? 'Inbox'}\n inboxNavigationLabel={tools.inboxNavigationLabel ?? 'Inbox sections'}\n onDsToolChange={this.handleManagedToolChange}\n onDsPresentationChange={this.handleManagedPresentationChange}\n onDsHeaderBack={this.handleManagedHeaderBack}\n onDsHeaderAction={this.handleManagedHeaderAction}\n >\n {this.renderManagedToolSlots()}\n </ds-shell-tools>\n );\n }\n\n private renderManagedMobileSheetNav() {\n const navigation = this.navigation;\n return (\n <ds-mobile-sheet-nav\n open={this.managedMobileSheetNavOpen}\n browseContext={this.managedBrowseContext}\n dashboardGroups={navigation.dashboardGroups ?? navigation.groups ?? []}\n settingsGroups={navigation.settingsGroups ?? []}\n currentUrl={navigation.currentUrl ?? this.pageChrome.currentUrl ?? ''}\n routeSelectionActive={this.managedMobileDestination === 'area'}\n navigationLabel={navigation.navigationLabel ?? 'Application navigation'}\n dashboardLabel={navigation.dashboardLabel ?? 'Dashboard'}\n settingsLabel={navigation.settingsLabel ?? 'Settings'}\n accountLabel={navigation.accountLabel ?? 'Account'}\n showAccount={navigation.showMobileAccount ?? true}\n onDsAreaSelect={this.handleManagedAreaSelect}\n onDsBrowseContextChange={this.handleManagedBrowseContext}\n onDsClose={this.handleManagedSheetClose}\n />\n );\n }\n\n private renderManagedMobileBarNav() {\n const item = (id: PanelToolsToolId) =>\n this.resolvedToolItems.find(candidate => candidate.id === id);\n const label = (id: PanelToolsToolId, fallback: string) =>\n item(id)?.ariaLabel ?? item(id)?.label ?? fallback;\n const directActivity = this.resolvedToolItems.some(\n candidate => !candidate.isInactive && candidate.mobileDestination === 'activity'\n );\n\n return (\n <ds-mobile-bar-nav\n hidden={this.tools.mobileBarHidden ?? false}\n activeDestination={this.managedMobileDestination}\n currentArea={this.currentArea}\n sheetNavExpanded={this.managedMobileSheetNavOpen}\n searchLabel={label('search', 'Search')}\n activityMode={directActivity ? 'direct' : 'inbox'}\n activityLabel={label('activity', 'Activity')}\n inboxLabel={this.tools.inboxLabel ?? 'Inbox'}\n messagesLabel={label('messages', 'Messages')}\n agentsLabel={label('agents', 'Agents')}\n helpLabel={label('help', 'Help & Support')}\n searchDot={this.toolDot('search')}\n activityDot={this.toolDot('activity')}\n inboxDot={this.toolDot('stacks') || this.toolDot('activity')}\n messagesDot={this.toolDot('messages')}\n agentsDot={this.toolDot('agents')}\n onDsSheetNavToggle={this.handleManagedSheetToggle}\n onDsDestinationChange={this.handleManagedMobileDestination}\n />\n );\n }\n\n private renderManagedMobileLeading() {\n if (!this.pageChrome.showBack) {\n return <slot name=\"page-header-leading\" slot=\"leading\" />;\n }\n return [\n <ds-tooltip\n slot=\"leading\"\n label={this.pageChrome.backAriaLabel ?? 'Back'}\n side=\"bottom\"\n size=\"sm\"\n >\n <ds-button-unfilled\n variant=\"icon\"\n icon={this.pageChrome.backIcon ?? 'ChevronLeft'}\n size=\"md\"\n aria-label={this.pageChrome.backAriaLabel ?? 'Back'}\n activeFill={false}\n hasBorder={false}\n onDsClick={(event: CustomEvent<MouseEvent>) => this.dsPageBack.emit(event.detail)}\n />\n </ds-tooltip>,\n <slot name=\"page-header-leading\" slot=\"leading\" />,\n ];\n }\n\n private renderManagedMobileActions() {\n const actions = this.pageChrome.mobileActions ?? [];\n return [\n ...actions.map((action: PanelToolsHeaderAction) => (\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 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 isInactive={action.isInactive}\n activeFill={false}\n hasBorder={false}\n onDsClick={() => this.dsPageAction.emit(action.id)}\n />\n </ds-tooltip>\n )),\n <slot name=\"page-header-trailing\" slot=\"trailing\" />,\n ];\n }\n\n private renderManagedPage() {\n const page = this.pageChrome;\n const mobileSections = page.showBack ? [] : (page.tabs ?? []);\n const barTitleSections: BarTitleSectionItem[] = (page.subsections ?? []).map(item =>\n 'type' in item\n ? { type: 'divider' }\n : {\n id: item.id,\n label: item.label,\n isInactive: item.isInactive,\n }\n );\n return (\n <ds-shell-page\n responsiveMode={this.resolvedMode}\n headerCapacity={resolveManagedShellPageCapacity(this.resolvedMode)}\n contentInset={page.contentInset ?? 'default'}\n contentInsetBlockStart={page.contentInsetBlockStart ?? 'default'}\n contentInsetBlockStartSize={page.contentInsetBlockStartSize}\n compactContentInsetBlockStartSize={page.compactContentInsetBlockStartSize}\n scrollCompaction={page.scrollCompaction ?? true}\n contentSurface={page.contentSurface ?? 'primary'}\n >\n <ds-bar-title\n slot=\"header\"\n heading={page.heading ?? ''}\n description={page.description ?? ''}\n showBack={page.showBack ?? false}\n backAriaLabel={page.backAriaLabel ?? 'Back'}\n backLabel={page.backLabel ?? 'Back'}\n breadcrumbs={page.breadcrumbs ?? []}\n breadcrumbAriaLabel={page.breadcrumbAriaLabel ?? 'Breadcrumb'}\n sections={barTitleSections}\n value={page.subvalue ?? ''}\n sectionsAriaLabel={page.subsectionsAriaLabel ?? 'Change page subsection'}\n primaryAction={page.primaryAction ?? null}\n actions={page.actions ?? []}\n actionsAriaLabel={page.actionsAriaLabel ?? 'More page actions'}\n showDivider={page.showHeaderDivider ?? true}\n showCompactDivider={page.showCompactHeaderDivider}\n onDsBack={this.handleManagedPageBack}\n onDsBreadcrumbSelect={this.handleManagedBreadcrumbSelect}\n onDsSectionChange={this.handleManagedSubsectionChange}\n onDsAction={this.handleManagedPageAction}\n />\n <ds-mobile-header\n slot=\"mobile-header\"\n heading={page.heading ?? ''}\n headingLevel={page.headingLevel ?? 'h1'}\n sections={mobileSections}\n value={page.showBack ? '' : (page.value ?? '')}\n sectionsAriaLabel={page.sectionsAriaLabel ?? 'Change page section'}\n subsections={page.subsections ?? []}\n subvalue={page.subvalue ?? ''}\n subsectionsAriaLabel={page.subsectionsAriaLabel ?? 'Change page subsection'}\n tone={page.tone ?? 'default'}\n onDsSectionChange={this.handleManagedTabChange}\n onDsSubsectionChange={this.handleManagedSubsectionChange}\n >\n {this.renderManagedMobileLeading()}\n {this.renderManagedMobileActions()}\n </ds-mobile-header>\n <slot />\n </ds-shell-page>\n );\n }\n\n private renderSlottedShell(\n shellCls: Record<string, boolean>,\n mobile: boolean,\n mobileToolActive: boolean,\n mobileStageBlocked: boolean,\n fullscreen: boolean\n ) {\n return (\n <Host class={shellCls} responsive-mode={this.resolvedMode}>\n <div class=\"shell-app__banner\">\n <slot name=\"banner\" />\n </div>\n <div class=\"shell-app__row\">\n <div class=\"shell-app__chrome\" aria-hidden=\"true\">\n {this.renderPaperTexture()}\n </div>\n <div\n class=\"shell-app__panel\"\n aria-hidden={fullscreen ? 'true' : undefined}\n inert={fullscreen ? true : undefined}\n >\n <slot name=\"panel\" />\n </div>\n <div class=\"shell-app__main\">\n <div\n class=\"shell-app__bar\"\n aria-hidden={fullscreen ? 'true' : undefined}\n inert={fullscreen ? true : undefined}\n >\n <slot name=\"bar\" />\n </div>\n <div\n class=\"shell-app__tools\"\n data-ds-overlay-boundary\n aria-hidden={\n mobile && (!mobileToolActive || this.mobileSheetNavOpen) ? 'true' : undefined\n }\n inert={mobile && (!mobileToolActive || this.mobileSheetNavOpen) ? true : undefined}\n hidden={mobile && !mobileToolActive}\n >\n <slot name=\"tools\" />\n </div>\n <div\n class=\"shell-app__content\"\n data-ds-overlay-boundary\n aria-hidden={fullscreen || mobileStageBlocked ? 'true' : undefined}\n inert={fullscreen || mobileStageBlocked ? true : undefined}\n tabIndex={0}\n >\n <slot />\n </div>\n <div\n class=\"shell-app__mobile-sheet-nav\"\n hidden={!mobile || !this.mobileSheetNavOpen}\n aria-hidden={mobile && this.mobileSheetNavOpen ? undefined : 'true'}\n inert={mobile && this.mobileSheetNavOpen ? undefined : true}\n >\n <slot name=\"mobile-sheet-nav\" />\n </div>\n <div class=\"shell-app__mobile-bar-nav\" hidden={!mobile}>\n <slot name=\"mobile-bar-nav\" />\n </div>\n </div>\n </div>\n </Host>\n );\n }\n\n render() {\n const chromeActive = this.chromeLayerActive();\n const gradientActive = this.gradientLayerActive();\n const paperTextureActive = this.paperTextureLayerActive();\n const mobile = this.resolvedMode === 'mobile';\n const mobileToolActive = mobile && this.effectiveMobileDestination !== 'area';\n const mobileStageBlocked = mobile && (mobileToolActive || this.effectiveMobileSheetNavOpen);\n const fullscreen = !mobile && this.toolsFullscreen;\n const shellCls: Record<string, boolean> = {\n 'shell-app': true,\n 'shell-app--chrome': chromeActive,\n 'shell-app--gradient': gradientActive,\n 'shell-app--paper-texture': paperTextureActive,\n [`shell-app--${this.navStyle}`]: true,\n [`shell-app--${this.resolvedMode}`]: true,\n 'shell-app--tools-fullscreen': fullscreen,\n 'shell-app--mobile-tool-active': mobileToolActive,\n 'shell-app--mobile-sheet-nav-open': mobile && this.effectiveMobileSheetNavOpen,\n 'shell-app--managed': this.managed,\n 'shell-app--slotted': !this.managed,\n };\n\n if (!this.managed) {\n return this.renderSlottedShell(\n shellCls,\n mobile,\n mobileToolActive,\n mobileStageBlocked,\n fullscreen\n );\n }\n\n return (\n <Host class={shellCls} responsive-mode={this.resolvedMode}>\n <div class=\"shell-app__banner\">\n <slot name=\"banner\" />\n </div>\n <div class=\"shell-app__row\">\n <div class=\"shell-app__chrome\" aria-hidden=\"true\">\n {this.renderPaperTexture()}\n </div>\n <div\n class=\"shell-app__panel\"\n aria-hidden={fullscreen ? 'true' : undefined}\n inert={fullscreen ? true : undefined}\n >\n {this.renderManagedPanelNav()}\n </div>\n <div class=\"shell-app__main\">\n <div\n class=\"shell-app__bar\"\n aria-hidden={fullscreen ? 'true' : undefined}\n inert={fullscreen ? true : undefined}\n >\n {this.renderManagedBarNav()}\n </div>\n <div\n class=\"shell-app__tools\"\n data-ds-overlay-boundary\n aria-hidden={\n mobile && (!mobileToolActive || this.effectiveMobileSheetNavOpen)\n ? 'true'\n : undefined\n }\n inert={\n mobile && (!mobileToolActive || this.effectiveMobileSheetNavOpen) ? true : undefined\n }\n hidden={mobile && !mobileToolActive}\n >\n {this.renderManagedTools()}\n </div>\n <div\n class=\"shell-app__content\"\n data-ds-overlay-boundary\n aria-hidden={fullscreen || mobileStageBlocked ? 'true' : undefined}\n inert={fullscreen || mobileStageBlocked ? true : undefined}\n tabIndex={0}\n >\n {this.renderManagedPage()}\n </div>\n <div\n class=\"shell-app__mobile-sheet-nav\"\n hidden={!mobile || !this.effectiveMobileSheetNavOpen}\n aria-hidden={mobile && this.effectiveMobileSheetNavOpen ? undefined : 'true'}\n inert={mobile && this.effectiveMobileSheetNavOpen ? undefined : true}\n >\n {this.renderManagedMobileSheetNav()}\n </div>\n <div class=\"shell-app__mobile-bar-nav\" hidden={!mobile}>\n {this.renderManagedMobileBarNav()}\n </div>\n </div>\n </div>\n </Host>\n );\n }\n}\n",
628
628
  "type": "registry:ui"
629
629
  },
630
630
  {
631
631
  "path": "src/wc/components/ShellApp/shell-app-types.ts",
632
- "content": "import type { BarNavTab } from '../BarNav/bar-nav-types';\nimport type {\n BarTitleActionItem,\n BarTitlePrimaryAction,\n} from '../BarTitle/bar-title-types';\nimport type { BreadcrumbItem } from '../Breadcrumb/breadcrumb-types';\nimport type { MobileHeaderTone } from '../MobileHeader/mobile-header-types';\nimport type {\n PanelNavGroup,\n PanelNavItem,\n PanelNavRouterMode,\n} from '../PanelNav/panel-nav-types';\nimport type {\n PanelToolsHeaders,\n PanelToolsHeaderAction,\n PanelToolsItem,\n} from '../PanelTools/panel-tools-types';\nimport type { NavChromeStyle } from '../../shell/nav-chrome';\nimport type {\n ShellPageContentInset,\n ShellPageContentSurface,\n} from '../ShellPage/shell-page-types';\n\nexport type ShellAppComposition = 'managed' | 'slotted';\n\n/**\n * Router-owned application navigation supplied to managed ShellApp.\n * ShellApp renders the responsive chrome but never performs route changes.\n */\nexport interface ShellNavigationConfig {\n /** Groups shown in the current desktop/tablet PanelNav context. */\n groups?: PanelNavGroup[];\n /** Complete Dashboard collection used by the mobile navigation sheet. */\n dashboardGroups?: PanelNavGroup[];\n /** Complete Settings collection used by the mobile navigation sheet. */\n settingsGroups?: PanelNavGroup[];\n /** Current application URL used for active-item matching. */\n currentUrl?: string;\n /** Optional explicit active destination when URL matching is unavailable. */\n activeId?: string;\n /** Current Dashboard or Settings browsing context. */\n browseContext?: NavChromeStyle;\n /** Managed shells default to event routing so the application router stays authoritative. */\n routerMode?: PanelNavRouterMode;\n /** Persisted desktop PanelNav collapse preference key. */\n storageKey?: string;\n userName?: string;\n userInitial?: string;\n accountMenuExpanded?: boolean;\n dashboardLabel?: string;\n settingsLabel?: string;\n accountLabel?: string;\n /** Hide the mobile-sheet Account shortcut when Settings already owns that destination. */\n showMobileAccount?: boolean;\n navigationLabel?: string;\n dashboardNavigationLabel?: string;\n settingsNavigationLabel?: string;\n}\n\n/**\n * Route-owned page chrome. The same data drives desktop BarNav/BarTitle and\n * the intentionally different mobile MobileHeader presentation.\n */\nexport interface ShellPageChromeConfig {\n heading?: string;\n /** Desktop/tablet route-navigation heading when it differs from the page h1. */\n routeHeading?: string;\n description?: string;\n headingLevel?: 'h1' | 'h2';\n tabs?: BarNavTab[];\n value?: string;\n basePath?: string;\n currentUrl?: string;\n sectionsAriaLabel?: string;\n subsections?: BarNavTab[];\n subvalue?: string;\n subsectionsAriaLabel?: string;\n showBack?: boolean;\n backAriaLabel?: string;\n backIcon?: string;\n backLabel?: string;\n breadcrumbs?: BreadcrumbItem[];\n breadcrumbAriaLabel?: string;\n primaryAction?: BarTitlePrimaryAction | null;\n actions?: BarTitleActionItem[];\n /** Icon actions rendered in the mobile header's trailing lane. */\n mobileActions?: PanelToolsHeaderAction[];\n actionsAriaLabel?: string;\n tone?: MobileHeaderTone;\n contentInset?: ShellPageContentInset;\n /** Remove only the content gutter adjacent to the page header. */\n contentInsetBlockStart?: ShellPageContentInset;\n /** Exact roomy block-start content inset when the responsive default is not appropriate. */\n contentInsetBlockStartSize?: string;\n /** Exact block-start content inset while page-title capacity is compact or constrained. */\n compactContentInsetBlockStartSize?: string;\n /** Allow a roomy page title to compact in response to page scrolling. */\n scrollCompaction?: boolean;\n /** Draw the divider beneath the desktop/tablet page title. */\n showHeaderDivider?: boolean;\n /** Override divider visibility while the page title is compact or constrained. */\n showCompactHeaderDivider?: boolean;\n /** Canvas surface painted by ShellPage around and beneath routed content. */\n contentSurface?: ShellPageContentSurface;\n}\n\n/**\n * Product-owned global tools supplied to managed ShellApp. Tool view content\n * remains mounted in the corresponding named `*-view` slots.\n */\nexport interface ShellToolsConfig {\n items?: PanelToolsItem[];\n headers?: PanelToolsHeaders;\n storageKey?: string;\n fullscreenHeaderMode?: 'shared' | 'split';\n toolsLabel?: string;\n toolShortcutsLabel?: string;\n inboxLabel?: string;\n inboxNavigationLabel?: string;\n /** Temporarily conceal the mobile bar while a tool-owned keyboard interaction needs the space. */\n mobileBarHidden?: boolean;\n}\n\nexport interface ShellNavigationSelectDetail {\n id: string;\n item?: PanelNavItem;\n}\n",
632
+ "content": "import type { BarNavTab } from '../BarNav/bar-nav-types';\nimport type { BarTitleActionItem, BarTitlePrimaryAction } from '../BarTitle/bar-title-types';\nimport type { BreadcrumbItem } from '../Breadcrumb/breadcrumb-types';\nimport type { MobileHeaderTone } from '../MobileHeader/mobile-header-types';\nimport type { PanelNavGroup, PanelNavItem, PanelNavRouterMode } from '../PanelNav/panel-nav-types';\nimport type {\n PanelToolsHeaders,\n PanelToolsHeaderAction,\n PanelToolsItem,\n} from '../PanelTools/panel-tools-types';\nimport type { NavChromeStyle } from '../../shell/nav-chrome';\nimport type { ShellPageContentInset, ShellPageContentSurface } from '../ShellPage/shell-page-types';\n\nexport type ShellAppComposition = 'managed' | 'slotted';\n\n/**\n * Router-owned application navigation supplied to managed ShellApp.\n * ShellApp renders the responsive chrome but never performs route changes.\n */\nexport interface ShellNavigationConfig {\n /** Groups shown in the current desktop/tablet PanelNav context. */\n groups?: PanelNavGroup[];\n /** Complete Dashboard collection used by the mobile navigation sheet. */\n dashboardGroups?: PanelNavGroup[];\n /** Complete Settings collection used by the mobile navigation sheet. */\n settingsGroups?: PanelNavGroup[];\n /** Current application URL used for active-item matching. */\n currentUrl?: string;\n /** Optional explicit active destination when URL matching is unavailable. */\n activeId?: string;\n /** Current Dashboard or Settings browsing context. */\n browseContext?: NavChromeStyle;\n /** Managed shells default to event routing so the application router stays authoritative. */\n routerMode?: PanelNavRouterMode;\n /** Persisted desktop PanelNav collapse preference key. */\n storageKey?: string;\n userName?: string;\n userInitial?: string;\n accountMenuExpanded?: boolean;\n dashboardLabel?: string;\n settingsLabel?: string;\n accountLabel?: string;\n /** Hide the mobile-sheet Account shortcut when Settings already owns that destination. */\n showMobileAccount?: boolean;\n navigationLabel?: string;\n dashboardNavigationLabel?: string;\n settingsNavigationLabel?: string;\n}\n\n/**\n * Route-owned page chrome. The same data drives desktop BarNav/BarTitle and\n * the intentionally different mobile MobileHeader presentation.\n */\nexport interface ShellPageChromeConfig {\n heading?: string;\n /** Desktop/tablet route-navigation heading when it differs from the page h1. */\n routeHeading?: string;\n description?: string;\n headingLevel?: 'h1' | 'h2';\n tabs?: BarNavTab[];\n value?: string;\n basePath?: string;\n currentUrl?: string;\n sectionsAriaLabel?: string;\n subsections?: BarNavTab[];\n subvalue?: string;\n subsectionsAriaLabel?: string;\n showBack?: boolean;\n backAriaLabel?: string;\n backIcon?: string;\n backLabel?: string;\n breadcrumbs?: BreadcrumbItem[];\n breadcrumbAriaLabel?: string;\n primaryAction?: BarTitlePrimaryAction | null;\n actions?: BarTitleActionItem[];\n /** Icon actions rendered in the mobile header's trailing lane. */\n mobileActions?: PanelToolsHeaderAction[];\n actionsAriaLabel?: string;\n tone?: MobileHeaderTone;\n contentInset?: ShellPageContentInset;\n /** Remove only the content gutter adjacent to the page header. */\n contentInsetBlockStart?: ShellPageContentInset;\n /** Exact roomy block-start content inset when the responsive default is not appropriate. */\n contentInsetBlockStartSize?: string;\n /** Exact block-start content inset while page-title capacity is compact or constrained. */\n compactContentInsetBlockStartSize?: string;\n /** Allow a roomy page title to compact in response to page scrolling. */\n scrollCompaction?: boolean;\n /** Draw the divider beneath the desktop/tablet page title. */\n showHeaderDivider?: boolean;\n /** Override divider visibility while the page title is compact or constrained. */\n showCompactHeaderDivider?: boolean;\n /** Canvas surface painted by ShellPage around and beneath routed content. */\n contentSurface?: ShellPageContentSurface;\n}\n\n/**\n * Product-owned global tools supplied to managed ShellApp. Tool view content\n * remains mounted in the corresponding named `*-view` slots.\n */\nexport interface ShellToolsConfig {\n items?: PanelToolsItem[];\n headers?: PanelToolsHeaders;\n storageKey?: string;\n fullscreenHeaderMode?: 'shared' | 'split';\n toolsLabel?: string;\n toolShortcutsLabel?: string;\n inboxLabel?: string;\n inboxNavigationLabel?: string;\n /** Temporarily conceal the mobile bar while a tool-owned keyboard interaction needs the space. */\n mobileBarHidden?: boolean;\n}\n\nexport interface ShellNavigationSelectDetail {\n id: string;\n item?: PanelNavItem;\n}\n",
633
633
  "type": "registry:ui"
634
634
  }
635
635
  ]
@@ -321,7 +321,7 @@
321
321
  "files": [
322
322
  {
323
323
  "path": "src/wc/components/ShellPage/ShellPage.css",
324
- "content": ":host {\n --_shell-page-content-background: var(--color-background-primary);\n --_shell-page-header-background: var(--color-background-primary);\n --_shell-page-responsive-content-block-start-inset: var(--dimension-space-400);\n --_shell-page-content-block-start-inset: var(\n --ds-shell-page-content-block-start-inset,\n var(--_shell-page-responsive-content-block-start-inset)\n );\n --ds-shell-page-sticky-header-block-size: var(--dimension-size-600);\n --ds-shell-page-content-block-start-offset: calc(\n var(--ds-shell-page-sticky-header-block-size) +\n var(--_shell-page-content-block-start-inset)\n );\n display: block;\n position: relative;\n min-width: 0;\n min-height: 100%;\n box-sizing: border-box;\n background: var(--_shell-page-content-background);\n}\n\n:host(.shell-page-host--surface-secondary) {\n --_shell-page-content-background: var(--color-background-secondary);\n}\n\n:host(.shell-page-host--surface-secondary[responsive-mode='desktop']),\n:host(.shell-page-host--surface-secondary[responsive-mode='tablet']) {\n --_shell-page-header-background: var(--color-background-secondary);\n}\n\n:host([responsive-mode='tablet']),\n:host([responsive-mode='mobile']) {\n --_shell-page-responsive-content-block-start-inset: var(--dimension-space-200);\n}\n\n:host([responsive-mode='mobile']) {\n --ds-shell-page-sticky-header-block-size: calc(\n var(--dimension-size-500) + var(--dimension-space-200)\n );\n}\n\n:host(.shell-page-host--header-compact),\n:host(.shell-page-host--header-constrained) {\n --_shell-page-content-block-start-inset: var(\n --ds-shell-page-compact-content-block-start-inset,\n var(--_shell-page-responsive-content-block-start-inset)\n );\n}\n\n:host(.shell-page-host--inset-none),\n:host(.shell-page-host--block-start-inset-none) {\n --_shell-page-content-block-start-inset: 0px;\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(--_shell-page-header-background);\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(--_shell-page-content-background);\n}\n\n.shell-page__content {\n min-width: 0;\n box-sizing: border-box;\n background: var(--_shell-page-content-background);\n}\n\n:host(.shell-page-host--inset-default) .shell-page__content {\n padding: var(--dimension-space-400);\n padding-block-start: var(--_shell-page-content-block-start-inset);\n}\n\n:host(.shell-page-host--inset-default[responsive-mode='tablet']) .shell-page__content,\n:host(.shell-page-host--inset-default[responsive-mode='mobile']) .shell-page__content {\n padding: var(--dimension-space-200);\n padding-block-start: var(--_shell-page-content-block-start-inset);\n}\n\n:host(.shell-page-host--inset-none) .shell-page__content {\n padding: 0;\n}\n",
324
+ "content": ":host {\n --_shell-page-content-background: var(--color-background-primary);\n --_shell-page-header-background: var(--color-background-primary);\n --_shell-page-responsive-content-block-start-inset: var(--dimension-space-400);\n --_shell-page-content-block-start-inset: var(\n --ds-shell-page-content-block-start-inset,\n var(--_shell-page-responsive-content-block-start-inset)\n );\n --ds-shell-page-sticky-header-block-size: var(--dimension-size-600);\n --ds-shell-page-content-block-start-offset: calc(\n var(--ds-shell-page-sticky-header-block-size) + var(--_shell-page-content-block-start-inset)\n );\n display: block;\n position: relative;\n min-width: 0;\n min-height: 100%;\n box-sizing: border-box;\n background: var(--_shell-page-content-background);\n}\n\n:host(.shell-page-host--surface-secondary) {\n --_shell-page-content-background: var(--color-background-secondary);\n}\n\n:host(.shell-page-host--surface-secondary[responsive-mode='desktop']),\n:host(.shell-page-host--surface-secondary[responsive-mode='tablet']) {\n --_shell-page-header-background: var(--color-background-secondary);\n}\n\n:host([responsive-mode='tablet']),\n:host([responsive-mode='mobile']) {\n --_shell-page-responsive-content-block-start-inset: var(--dimension-space-200);\n}\n\n:host([responsive-mode='mobile']) {\n --ds-shell-page-sticky-header-block-size: calc(\n var(--dimension-size-500) + var(--dimension-space-200)\n );\n}\n\n:host(.shell-page-host--header-compact),\n:host(.shell-page-host--header-constrained) {\n --_shell-page-content-block-start-inset: var(\n --ds-shell-page-compact-content-block-start-inset,\n var(--_shell-page-responsive-content-block-start-inset)\n );\n}\n\n:host(.shell-page-host--inset-none),\n:host(.shell-page-host--block-start-inset-none) {\n --_shell-page-content-block-start-inset: 0px;\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(--_shell-page-header-background);\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(--_shell-page-content-background);\n}\n\n.shell-page__content {\n min-width: 0;\n box-sizing: border-box;\n background: var(--_shell-page-content-background);\n}\n\n:host(.shell-page-host--inset-default) .shell-page__content {\n padding: var(--dimension-space-400);\n padding-block-start: var(--_shell-page-content-block-start-inset);\n}\n\n:host(.shell-page-host--inset-default[responsive-mode='tablet']) .shell-page__content,\n:host(.shell-page-host--inset-default[responsive-mode='mobile']) .shell-page__content {\n padding: var(--dimension-space-200);\n padding-block-start: var(--_shell-page-content-block-start-inset);\n}\n\n:host(.shell-page-host--inset-none) .shell-page__content {\n padding: 0;\n}\n",
325
325
  "type": "registry:ui"
326
326
  },
327
327
  {
@@ -420,7 +420,7 @@
420
420
  },
421
421
  {
422
422
  "path": "src/wc/components/ShellTools/ShellTools.tsx",
423
- "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 type PanelToolsHeaderConfig,\n type PanelToolsHeaders,\n type PanelToolsItem,\n type PanelToolsToolId,\n} from '../PanelTools/panel-tools-types';\nimport {\n orderPanelToolsItems,\n reconcilePanelToolsAvailability,\n resolvePanelToolActivation,\n} from '../PanelTools/panel-tools-utils';\nimport {\n itemUsesShellInbox,\n isShellInboxTool,\n resolveAvailableInboxTool,\n type ShellInboxToolId,\n type ShellResponsiveMode,\n} from '../../shell/shell-responsive';\n\n@Component({\n tag: 'ds-shell-tools',\n styleUrl: 'ShellTools.css',\n shadow: 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() headers: PanelToolsHeaders = {};\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 @Watch('presentation')\n presentationChanged(next: 'drawer' | 'fullscreen', previous: 'drawer' | 'fullscreen') {\n if (next === previous) return;\n\n /*\n * Consumers update ShellTools and their slotted master/detail layout in the\n * same render. Forward the request immediately so PanelTools can conceal\n * its surface until its matching render class commits on the next frame.\n */\n if (this.panelToolsEl && this.panelToolsEl.getAttribute('presentation') !== next) {\n this.panelToolsEl.setAttribute('presentation', next);\n }\n }\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 this.items ?? [];\n }\n\n private get availableInboxTools(): ShellInboxToolId[] {\n return this.resolvedItems\n .filter(\n item =>\n !item.isInactive &&\n itemUsesShellInbox(item)\n )\n .map(item => item.id as ShellInboxToolId);\n }\n\n private get mobileViewOrder(): PanelToolsToolId[] {\n return orderPanelToolsItems(this.resolvedItems).map(item => item.id);\n }\n\n private toolLabel(id: PanelToolsToolId): string {\n const item = this.resolvedItems.find(candidate => candidate.id === id);\n return (\n item?.label ??\n item?.ariaLabel ??\n PANEL_TOOLS_LABELS[id as keyof typeof PANEL_TOOLS_LABELS] ??\n id\n );\n }\n\n private get mobileActiveTool(): PanelToolsToolId | '' {\n const item = this.resolvedItems.find(candidate => candidate.id === this.activeTool);\n const inbox = item ? itemUsesShellInbox(item) : isShellInboxTool(this.activeTool);\n if (!inbox) return this.activeTool;\n return resolveAvailableInboxTool(this.activeTool, this.availableInboxTools);\n }\n\n componentWillLoad() {\n this.reconcileAvailability();\n }\n\n @Watch('items')\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 return this.headers ?? {};\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 (!this.availableInboxTools.includes(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.shadowRoot ?? this.el).querySelector(\n `[data-header-action-id=\"${CSS.escape(id)}\"]`\n ) as\n | (HTMLElement & { setFocus?: () => Promise<void> })\n | null;\n await action?.setFocus?.();\n }\n\n private renderForwardedSlots() {\n return this.mobileViewOrder.flatMap(id => [\n <div key={`tool:${id}`} class=\"shell-tools__slot-proxy\" slot={id}>\n <slot name={id} />\n </div>,\n <div\n key={`tool-view:${id}`}\n class=\"shell-tools__slot-proxy\"\n slot={`${id}-view`}\n >\n <slot name={`${id}-view`} />\n </div>,\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 || this.toolLabel(tool);\n const actions = (header.actions ?? []).filter(action => action.id !== 'fullscreen');\n const item = this.resolvedItems.find(candidate => candidate.id === tool);\n const inboxRoot = Boolean(item && itemUsesShellInbox(item) && !header.showBack);\n const sections =\n inboxRoot\n ? this.availableInboxTools.map(id => {\n const item = this.resolvedItems.find(candidate => candidate.id === id);\n return {\n id,\n label: this.toolLabel(id),\n variant: 'label' as const,\n dot: item?.dot,\n };\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 sectionsPresentation={inboxRoot ? 'segmented' : 'switcher'}\n sectionsSize=\"lg\"\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 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 {this.mobileViewOrder.map(id => {\n const active = this.open && id === tool;\n return (\n <div\n key={`mobile-tool-view:${id}`}\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",
423
+ "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 type PanelToolsHeaderConfig,\n type PanelToolsHeaders,\n type PanelToolsItem,\n type PanelToolsToolId,\n} from '../PanelTools/panel-tools-types';\nimport {\n orderPanelToolsItems,\n reconcilePanelToolsAvailability,\n resolvePanelToolActivation,\n} from '../PanelTools/panel-tools-utils';\nimport {\n itemUsesShellInbox,\n isShellInboxTool,\n resolveAvailableInboxTool,\n type ShellInboxToolId,\n type ShellResponsiveMode,\n} from '../../shell/shell-responsive';\n\n@Component({\n tag: 'ds-shell-tools',\n styleUrl: 'ShellTools.css',\n shadow: 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() headers: PanelToolsHeaders = {};\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 @Watch('presentation')\n presentationChanged(next: 'drawer' | 'fullscreen', previous: 'drawer' | 'fullscreen') {\n if (next === previous) return;\n\n /*\n * Consumers update ShellTools and their slotted master/detail layout in the\n * same render. Forward the request immediately so PanelTools can conceal\n * its surface until its matching render class commits on the next frame.\n */\n if (this.panelToolsEl && this.panelToolsEl.getAttribute('presentation') !== next) {\n this.panelToolsEl.setAttribute('presentation', next);\n }\n }\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 this.items ?? [];\n }\n\n private get availableInboxTools(): ShellInboxToolId[] {\n return this.resolvedItems\n .filter(item => !item.isInactive && itemUsesShellInbox(item))\n .map(item => item.id as ShellInboxToolId);\n }\n\n private get mobileViewOrder(): PanelToolsToolId[] {\n return orderPanelToolsItems(this.resolvedItems).map(item => item.id);\n }\n\n private toolLabel(id: PanelToolsToolId): string {\n const item = this.resolvedItems.find(candidate => candidate.id === id);\n return (\n item?.label ??\n item?.ariaLabel ??\n PANEL_TOOLS_LABELS[id as keyof typeof PANEL_TOOLS_LABELS] ??\n id\n );\n }\n\n private get mobileActiveTool(): PanelToolsToolId | '' {\n const item = this.resolvedItems.find(candidate => candidate.id === this.activeTool);\n const inbox = item ? itemUsesShellInbox(item) : isShellInboxTool(this.activeTool);\n if (!inbox) return this.activeTool;\n return resolveAvailableInboxTool(this.activeTool, this.availableInboxTools);\n }\n\n componentWillLoad() {\n this.reconcileAvailability();\n }\n\n @Watch('items')\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 return this.headers ?? {};\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 (!this.availableInboxTools.includes(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.shadowRoot ?? this.el).querySelector(\n `[data-header-action-id=\"${CSS.escape(id)}\"]`\n ) as (HTMLElement & { setFocus?: () => Promise<void> }) | null;\n await action?.setFocus?.();\n }\n\n private renderForwardedSlots() {\n return this.mobileViewOrder.flatMap(id => [\n <div key={`tool:${id}`} class=\"shell-tools__slot-proxy\" slot={id}>\n <slot name={id} />\n </div>,\n <div key={`tool-view:${id}`} class=\"shell-tools__slot-proxy\" slot={`${id}-view`}>\n <slot name={`${id}-view`} />\n </div>,\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 || this.toolLabel(tool);\n const actions = (header.actions ?? []).filter(action => action.id !== 'fullscreen');\n const item = this.resolvedItems.find(candidate => candidate.id === tool);\n const inboxRoot = Boolean(item && itemUsesShellInbox(item) && !header.showBack);\n const sections = inboxRoot\n ? this.availableInboxTools.map(id => {\n const item = this.resolvedItems.find(candidate => candidate.id === id);\n return {\n id,\n label: this.toolLabel(id),\n variant: 'label' as const,\n dot: item?.dot,\n };\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 sectionsPresentation={inboxRoot ? 'segmented' : 'switcher'}\n sectionsSize=\"lg\"\n onDsSectionChange={this.selectInboxTool}\n >\n {header.showBack ? (\n <ds-tooltip slot=\"leading\" label={header.backAriaLabel || 'Back'} side=\"bottom\" size=\"sm\">\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 isInactive={action.isInactive}\n activeFill={false}\n hasBorder={false}\n onDsClick={(event: CustomEvent<MouseEvent>) =>\n this.handleMobileHeaderAction(tool, action.id, event)\n }\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 {this.mobileViewOrder.map(id => {\n const active = this.open && id === tool;\n return (\n <div\n key={`mobile-tool-view:${id}`}\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 ? (this.presentation === 'fullscreen' ? 'dialog' : 'complementary') : 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",
424
424
  "type": "registry:ui"
425
425
  }
426
426
  ]
@@ -275,12 +275,12 @@
275
275
  "files": [
276
276
  {
277
277
  "path": "src/wc/components/Skeleton/Skeleton.css",
278
- "content": "@import '../../utils/control-density.css';\n@import '../../utils/shimmer.css';\n\n:host {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n min-width: 0;\n vertical-align: middle;\n --ds-skeleton-base: var(--color-foreground-quaternary);\n --ds-shimmer: var(--color-shimmer-shimmer);\n}\n\n.skeleton__shape {\n --ds-shimmer-surface-base: var(--ds-skeleton-base);\n\n display: block;\n width: 100%;\n height: 100%;\n flex: 0 0 auto;\n background-color: var(--ds-skeleton-base);\n border-radius: var(--dimension-radius-025);\n}\n\n/* ── Background context ────────────────────────────────────────────────── */\n\n:host(.skeleton--background-medium) {\n --ds-skeleton-base: var(--color-foreground-on-medium-background-quaternary);\n --ds-shimmer: var(--color-shimmer-shimmer-on-medium-background);\n}\n\n:host(.skeleton--background-bold) {\n --ds-skeleton-base: var(--color-foreground-on-bold-background-quaternary);\n --ds-shimmer: var(--color-shimmer-shimmer-on-bold-background);\n}\n\n:host(.skeleton--background-strong) {\n --ds-skeleton-base: var(--color-foreground-on-strong-background-quaternary);\n --ds-shimmer: var(--color-shimmer-shimmer-on-strong-background);\n}\n\n:host(.skeleton--background-translucent) {\n --ds-skeleton-base: var(--color-translucent-foreground-quaternary);\n --ds-shimmer: var(--color-translucent-shimmer);\n}\n\n:host(.skeleton--background-inverted) {\n --ds-skeleton-base: var(--color-inverted-foreground-quaternary);\n --ds-shimmer: var(--color-inverted-shimmer);\n}\n\n:host(.skeleton--background-media) {\n --ds-skeleton-base: var(--color-media-foreground-quaternary);\n --ds-shimmer: var(--color-media-shimmer);\n}\n\n:host(.skeleton--background-always-dark) {\n --ds-skeleton-base: var(--color-always-dark-foreground-quaternary);\n --ds-shimmer: var(--color-always-dark-shimmer);\n}\n\n:host(.skeleton--background-navigation) {\n --ds-skeleton-base: var(--color-navigation-foreground-quaternary);\n --ds-shimmer: var(--color-navigation-shimmer);\n}\n\n/* ── Text ──────────────────────────────────────────────────────────────── */\n\n:host(.skeleton--text) {\n width: 100%;\n height: var(--ds-skeleton-canvas-size);\n padding-block: 0;\n}\n\n:host(.skeleton--text) .skeleton__shape {\n height: 75%;\n}\n\n:host(.skeleton--text-text-display-medium) { --ds-skeleton-canvas-size: var(--typography-lineheight-3xl); }\n:host(.skeleton--text-text-display-small) { --ds-skeleton-canvas-size: var(--typography-lineheight-2xl); }\n:host(.skeleton--text-text-title-large) { --ds-skeleton-canvas-size: var(--typography-lineheight-xl); }\n:host(.skeleton--text-text-title-medium),\n:host(.skeleton--text-text-body-large) { --ds-skeleton-canvas-size: var(--typography-lineheight-lg); }\n:host(.skeleton--text-text-title-small),\n:host(.skeleton--text-text-body-medium) { --ds-skeleton-canvas-size: var(--typography-lineheight-md); }\n:host(.skeleton--text-text-body-small) { --ds-skeleton-canvas-size: var(--typography-lineheight-sm); }\n:host(.skeleton--text-text-caption) { --ds-skeleton-canvas-size: var(--typography-lineheight-xs); }\n\n/* ── Icon ──────────────────────────────────────────────────────────────── */\n\n:host(.skeleton--icon) {\n width: var(--ds-skeleton-canvas-size);\n height: var(--ds-skeleton-canvas-size);\n padding: 0;\n}\n\n:host(.skeleton--icon) .skeleton__shape {\n width: 75%;\n height: 75%;\n}\n\n:host(.skeleton--icon.skeleton--rounded) .skeleton__shape {\n border-radius: var(--dimension-radius-half);\n}\n\n:host(.skeleton--icon-xs) { --ds-skeleton-canvas-size: var(--dimension-iconography-xs); }\n:host(.skeleton--icon-sm) { --ds-skeleton-canvas-size: var(--dimension-iconography-sm); }\n:host(.skeleton--icon-md) { --ds-skeleton-canvas-size: var(--dimension-iconography-md); }\n:host(.skeleton--icon-lg) { --ds-skeleton-canvas-size: var(--dimension-iconography-lg); }\n:host(.skeleton--icon-xl) { --ds-skeleton-canvas-size: var(--dimension-iconography-xl); }\n:host(.skeleton--icon-2xl) { --ds-skeleton-canvas-size: var(--dimension-iconography-2xl); }\n:host(.skeleton--icon-3xl) { --ds-skeleton-canvas-size: var(--dimension-iconography-3xl); }\n\n/* ── Control ───────────────────────────────────────────────────────────── */\n\n:host(.skeleton--control) {\n width: 100%;\n height: var(--ds-control-height);\n}\n\n:host(.skeleton--control) .skeleton__shape {\n border-radius: var(--ds-control-radius, var(--dimension-radius-025));\n}\n\n:host(.skeleton--control.skeleton--rounded) .skeleton__shape {\n border-radius: var(--dimension-radius-half);\n}\n\n@media (forced-colors: active) {\n .skeleton__shape {\n background-color: var(--ds-forced-color-disabled);\n }\n}\n",
278
+ "content": "@import '../../utils/control-density.css';\n@import '../../utils/shimmer.css';\n\n:host {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n min-width: 0;\n vertical-align: middle;\n --ds-skeleton-base: var(--color-foreground-quaternary);\n --ds-shimmer: var(--color-shimmer-shimmer);\n}\n\n.skeleton__shape {\n --ds-shimmer-surface-base: var(--ds-skeleton-base);\n\n display: block;\n width: 100%;\n height: 100%;\n flex: 0 0 auto;\n background-color: var(--ds-skeleton-base);\n border-radius: var(--dimension-radius-025);\n}\n\n/* ── Background context ────────────────────────────────────────────────── */\n\n:host(.skeleton--background-medium) {\n --ds-skeleton-base: var(--color-foreground-on-medium-background-quaternary);\n --ds-shimmer: var(--color-shimmer-shimmer-on-medium-background);\n}\n\n:host(.skeleton--background-bold) {\n --ds-skeleton-base: var(--color-foreground-on-bold-background-quaternary);\n --ds-shimmer: var(--color-shimmer-shimmer-on-bold-background);\n}\n\n:host(.skeleton--background-strong) {\n --ds-skeleton-base: var(--color-foreground-on-strong-background-quaternary);\n --ds-shimmer: var(--color-shimmer-shimmer-on-strong-background);\n}\n\n:host(.skeleton--background-translucent) {\n --ds-skeleton-base: var(--color-translucent-foreground-quaternary);\n --ds-shimmer: var(--color-translucent-shimmer);\n}\n\n:host(.skeleton--background-inverted) {\n --ds-skeleton-base: var(--color-inverted-foreground-quaternary);\n --ds-shimmer: var(--color-inverted-shimmer);\n}\n\n:host(.skeleton--background-media) {\n --ds-skeleton-base: var(--color-media-foreground-quaternary);\n --ds-shimmer: var(--color-media-shimmer);\n}\n\n:host(.skeleton--background-always-dark) {\n --ds-skeleton-base: var(--color-always-dark-foreground-quaternary);\n --ds-shimmer: var(--color-always-dark-shimmer);\n}\n\n:host(.skeleton--background-navigation) {\n --ds-skeleton-base: var(--color-navigation-foreground-quaternary);\n --ds-shimmer: var(--color-navigation-shimmer);\n}\n\n/* ── Text ──────────────────────────────────────────────────────────────── */\n\n:host(.skeleton--text) {\n width: 100%;\n height: var(--ds-skeleton-canvas-size);\n padding-block: 0;\n}\n\n:host(.skeleton--text) .skeleton__shape {\n height: 75%;\n}\n\n:host(.skeleton--text-text-display-medium) {\n --ds-skeleton-canvas-size: var(--typography-lineheight-3xl);\n}\n:host(.skeleton--text-text-display-small) {\n --ds-skeleton-canvas-size: var(--typography-lineheight-2xl);\n}\n:host(.skeleton--text-text-title-large) {\n --ds-skeleton-canvas-size: var(--typography-lineheight-xl);\n}\n:host(.skeleton--text-text-title-medium),\n:host(.skeleton--text-text-body-large) {\n --ds-skeleton-canvas-size: var(--typography-lineheight-lg);\n}\n:host(.skeleton--text-text-title-small),\n:host(.skeleton--text-text-body-medium) {\n --ds-skeleton-canvas-size: var(--typography-lineheight-md);\n}\n:host(.skeleton--text-text-body-small) {\n --ds-skeleton-canvas-size: var(--typography-lineheight-sm);\n}\n:host(.skeleton--text-text-caption) {\n --ds-skeleton-canvas-size: var(--typography-lineheight-xs);\n}\n\n/* ── Icon ──────────────────────────────────────────────────────────────── */\n\n:host(.skeleton--icon) {\n width: var(--ds-skeleton-canvas-size);\n height: var(--ds-skeleton-canvas-size);\n padding: 0;\n}\n\n:host(.skeleton--icon) .skeleton__shape {\n width: 75%;\n height: 75%;\n}\n\n:host(.skeleton--icon.skeleton--rounded) .skeleton__shape {\n border-radius: var(--dimension-radius-half);\n}\n\n:host(.skeleton--icon-xs) {\n --ds-skeleton-canvas-size: var(--dimension-iconography-xs);\n}\n:host(.skeleton--icon-sm) {\n --ds-skeleton-canvas-size: var(--dimension-iconography-sm);\n}\n:host(.skeleton--icon-md) {\n --ds-skeleton-canvas-size: var(--dimension-iconography-md);\n}\n:host(.skeleton--icon-lg) {\n --ds-skeleton-canvas-size: var(--dimension-iconography-lg);\n}\n:host(.skeleton--icon-xl) {\n --ds-skeleton-canvas-size: var(--dimension-iconography-xl);\n}\n:host(.skeleton--icon-2xl) {\n --ds-skeleton-canvas-size: var(--dimension-iconography-2xl);\n}\n:host(.skeleton--icon-3xl) {\n --ds-skeleton-canvas-size: var(--dimension-iconography-3xl);\n}\n\n/* ── Control ───────────────────────────────────────────────────────────── */\n\n:host(.skeleton--control) {\n width: 100%;\n height: var(--ds-control-height);\n}\n\n:host(.skeleton--control) .skeleton__shape {\n border-radius: var(--ds-control-radius, var(--dimension-radius-025));\n}\n\n:host(.skeleton--control.skeleton--rounded) .skeleton__shape {\n border-radius: var(--dimension-radius-half);\n}\n\n@media (forced-colors: active) {\n .skeleton__shape {\n background-color: var(--ds-forced-color-disabled);\n }\n}\n",
279
279
  "type": "registry:ui"
280
280
  },
281
281
  {
282
282
  "path": "src/wc/components/Skeleton/Skeleton.tsx",
283
- "content": "import { Component, Prop, h, Host } from '@stencil/core';\nimport type { IconSize } from '../Icon/Icon';\nimport type { TextVariant } from '../Text/text-types';\nimport type { ControlSize } from '../../utils/control-text';\n\nexport type SkeletonVariant = 'text' | 'icon' | 'control';\nexport type SkeletonBackground =\n | 'faint'\n | 'medium'\n | 'bold'\n | 'strong'\n | 'translucent'\n | 'inverted'\n | 'media'\n | 'navigation'\n | 'always-dark';\n\n@Component({\n tag: 'ds-skeleton',\n styleUrl: 'Skeleton.css',\n shadow: true,\n})\nexport class Skeleton {\n @Prop() variant: SkeletonVariant = 'text';\n /** Text metric recipe whose line-height defines the text canvas. */\n @Prop() textVariant: TextVariant = 'text-body-medium';\n /** Iconography token whose square canvas defines the icon canvas. */\n @Prop() iconSize: IconSize = 'md';\n /** Shared control-density size whose height defines the control canvas. */\n @Prop() controlSize: ControlSize = 'md';\n /** Width of text and control skeleton canvases. Numbers resolve to px. Ignored for icons. */\n @Prop() width: string | number | undefined;\n /** Round icon skeletons into circles and control skeletons into pills. Ignored for text. */\n @Prop() rounded: boolean = false;\n /** Whether to show the shimmer animation. */\n @Prop() shimmer: boolean = true;\n /** Actual parent surface context. Omit on primary and secondary surfaces. */\n @Prop() background: SkeletonBackground | undefined;\n\n private get widthCss() {\n const v = this.width;\n if (v == null) return undefined;\n return typeof v === 'number' ? `${v}px` : v;\n }\n\n render() {\n return (\n <Host\n aria-hidden=\"true\"\n class={{\n 'skeleton': true,\n [`skeleton--${this.variant}`]: true,\n [`skeleton--text-${this.textVariant}`]: this.variant === 'text',\n [`skeleton--icon-${this.iconSize}`]: this.variant === 'icon',\n [`ds-control--${this.controlSize}`]: this.variant === 'control',\n [`skeleton--background-${this.background}`]: !!this.background,\n 'skeleton--rounded': this.variant !== 'text' && this.rounded,\n }}\n style={this.variant === 'icon' ? undefined : { width: this.widthCss }}\n >\n <span\n class={{\n 'skeleton__shape': true,\n 'ds-shimmer-surface': this.shimmer,\n }}\n />\n </Host>\n );\n }\n}\n",
283
+ "content": "import { Component, Prop, h, Host } from '@stencil/core';\nimport type { IconSize } from '../Icon/Icon';\nimport type { TextVariant } from '../Text/text-types';\nimport type { ControlSize } from '../../utils/control-text';\n\nexport type SkeletonVariant = 'text' | 'icon' | 'control';\nexport type SkeletonBackground =\n | 'faint'\n | 'medium'\n | 'bold'\n | 'strong'\n | 'translucent'\n | 'inverted'\n | 'media'\n | 'navigation'\n | 'always-dark';\n\n@Component({\n tag: 'ds-skeleton',\n styleUrl: 'Skeleton.css',\n shadow: true,\n})\nexport class Skeleton {\n @Prop() variant: SkeletonVariant = 'text';\n /** Text metric recipe whose line-height defines the text canvas. */\n @Prop() textVariant: TextVariant = 'text-body-medium';\n /** Iconography token whose square canvas defines the icon canvas. */\n @Prop() iconSize: IconSize = 'md';\n /** Shared control-density size whose height defines the control canvas. */\n @Prop() controlSize: ControlSize = 'md';\n /** Width of text and control skeleton canvases. Numbers resolve to px. Ignored for icons. */\n @Prop() width: string | number | undefined;\n /** Round icon skeletons into circles and control skeletons into pills. Ignored for text. */\n @Prop() rounded: boolean = false;\n /** Whether to show the shimmer animation. */\n @Prop() shimmer: boolean = true;\n /** Actual parent surface context. Omit on primary and secondary surfaces. */\n @Prop() background: SkeletonBackground | undefined;\n\n private get widthCss() {\n const v = this.width;\n if (v == null) return undefined;\n return typeof v === 'number' ? `${v}px` : v;\n }\n\n render() {\n return (\n <Host\n aria-hidden=\"true\"\n class={{\n skeleton: true,\n [`skeleton--${this.variant}`]: true,\n [`skeleton--text-${this.textVariant}`]: this.variant === 'text',\n [`skeleton--icon-${this.iconSize}`]: this.variant === 'icon',\n [`ds-control--${this.controlSize}`]: this.variant === 'control',\n [`skeleton--background-${this.background}`]: !!this.background,\n 'skeleton--rounded': this.variant !== 'text' && this.rounded,\n }}\n style={this.variant === 'icon' ? undefined : { width: this.widthCss }}\n >\n <span\n class={{\n skeleton__shape: true,\n 'ds-shimmer-surface': this.shimmer,\n }}\n />\n </Host>\n );\n }\n}\n",
284
284
  "type": "registry:ui"
285
285
  }
286
286
  ]
@@ -651,12 +651,12 @@
651
651
  "files": [
652
652
  {
653
653
  "path": "src/wc/components/Slider/Slider.css",
654
- "content": "@import '../../utils/control-inactive.css';\n@import '../../utils/field-stack.css';\n@import '../../utils/forced-colors.css';\n\n:host {\n --ds-slider-control-size: var(--dimension-size-400);\n --ds-slider-thumb-size: var(--dimension-size-200);\n --ds-slider-track-size: var(--dimension-size-100);\n --ds-slider-track-stroke: var(--dimension-stroke-width-012);\n --ds-slider-vertical-length: calc(var(--dimension-size-800) * 3);\n box-sizing: border-box;\n width: 100%;\n color: var(--color-foreground-primary);\n}\n\n:host(.slider--sm) {\n --ds-slider-control-size: var(--dimension-size-300);\n --ds-slider-thumb-size: var(--dimension-size-150);\n --ds-slider-track-size: var(--dimension-size-075);\n}\n\n:host(.slider--xs) {\n --ds-slider-control-size: var(--dimension-size-200);\n --ds-slider-thumb-size: var(--dimension-size-100);\n --ds-slider-track-size: var(--dimension-size-050);\n}\n\n:host(.slider--vertical) {\n width: fit-content;\n min-width: var(--dimension-form-width-xs);\n}\n\n.slider__label-row {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: var(--dimension-space-100);\n min-width: 0;\n}\n\n.slider__label {\n min-width: 0;\n}\n\n.slider__value {\n flex-shrink: 0;\n}\n\n.slider__control {\n position: relative;\n width: 100%;\n height: var(--ds-slider-control-size);\n border-radius: var(--dimension-radius-half);\n cursor: default;\n touch-action: none;\n user-select: none;\n}\n\n:host(.slider--vertical) .slider__control {\n align-self: center;\n width: var(--ds-slider-control-size);\n height: var(--ds-slider-vertical-length);\n}\n\n:host(.slider--readonly) .slider__control {\n cursor: default;\n}\n\n/* Inactive: `.ds-control-inactive` (50% opacity) on the host. */\n:host(.ds-control-inactive) .slider__control {\n cursor: not-allowed;\n}\n\n.slider__rail {\n position: absolute;\n top: 50%;\n right: 0;\n left: 0;\n height: var(--ds-slider-track-size);\n overflow: hidden;\n border-radius: var(--dimension-radius-025);\n background-color: transparent;\n box-shadow: inset 0 0 0 var(--ds-slider-track-stroke) var(--color-border-secondary);\n transform: translateY(-50%);\n pointer-events: none;\n}\n\n:host(.slider--thumb-center) .slider__rail {\n right: calc(var(--ds-slider-thumb-size) / 2);\n left: calc(var(--ds-slider-thumb-size) / 2);\n}\n\n.slider__indicator {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: calc(\n var(--ds-slider-thumb-size) / 2 +\n (var(--ds-slider-end-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size))\n );\n border-radius: inherit;\n background-color: var(--color-background-bold-brand);\n}\n\n:host(.slider--range) .slider__indicator {\n left: calc(\n var(--ds-slider-thumb-size) / 2 +\n (var(--ds-slider-start-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size))\n );\n width: calc(\n ((var(--ds-slider-end-pct, 0) - var(--ds-slider-start-pct, 0)) / 100) *\n (100% - var(--ds-slider-thumb-size))\n );\n}\n\n:host(.slider--thumb-center) .slider__indicator {\n left: calc(var(--ds-slider-start-pct, 0) * 1%);\n width: calc((var(--ds-slider-end-pct, 0) - var(--ds-slider-start-pct, 0)) * 1%);\n}\n\n.slider__thumb {\n position: absolute;\n top: 50%;\n left: calc((var(--ds-slider-thumb-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size)));\n width: var(--ds-slider-thumb-size);\n height: var(--ds-slider-thumb-size);\n transform: translateY(-50%);\n z-index: 1;\n}\n\n.slider__thumb--active {\n z-index: 2;\n}\n\n.slider__input {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n margin: 0;\n padding: 0;\n border: 0;\n opacity: 0;\n appearance: none;\n pointer-events: none;\n z-index: 2;\n}\n\n.slider__thumb-visual {\n position: absolute;\n inset: 0;\n box-sizing: border-box;\n overflow: visible;\n border-radius: var(--dimension-radius-025);\n background-color: var(--color-background-primary);\n box-shadow: var(--effect-elevation-elevated-sm);\n pointer-events: none;\n transition:\n background-color var(--effect-motion-short-2),\n box-shadow var(--effect-motion-short-2);\n}\n\n.slider__thumb-wash {\n position: absolute;\n inset: 0;\n border-radius: inherit;\n background-color: transparent;\n transition: background-color var(--effect-motion-short-2);\n}\n\n@media (hover: hover) and (pointer: fine) {\n :host(:not(.slider--readonly):not(.ds-control-inactive))\n .slider__thumb:hover\n .slider__thumb-wash {\n background-color: var(--color-interaction-hover);\n }\n}\n\n:host([data-dragging]:not(.slider--readonly):not(.ds-control-inactive))\n .slider__thumb--active .slider__thumb-wash {\n background-color: var(--color-interaction-pressed);\n}\n\n.slider__input:focus-visible + .slider__thumb-visual,\n:host([data-review-focus]) .slider__thumb[data-index='0'] .slider__thumb-visual {\n outline: var(--dimension-stroke-width-025) solid var(--color-interaction-focus);\n outline-offset: var(--dimension-space-025);\n}\n\n:host(.slider--vertical) .slider__rail {\n top: 0;\n right: auto;\n bottom: 0;\n left: 50%;\n width: var(--ds-slider-track-size);\n height: auto;\n transform: translateX(-50%);\n}\n\n:host(.slider--vertical.slider--thumb-center) .slider__rail {\n top: calc(var(--ds-slider-thumb-size) / 2);\n bottom: calc(var(--ds-slider-thumb-size) / 2);\n}\n\n:host(.slider--vertical) .slider__indicator {\n top: auto;\n right: 0;\n bottom: 0;\n left: 0;\n width: auto;\n height: calc(\n var(--ds-slider-thumb-size) / 2 +\n (var(--ds-slider-end-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size))\n );\n}\n\n:host(.slider--vertical.slider--range) .slider__indicator {\n bottom: calc(\n var(--ds-slider-thumb-size) / 2 +\n (var(--ds-slider-start-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size))\n );\n height: calc(\n ((var(--ds-slider-end-pct, 0) - var(--ds-slider-start-pct, 0)) / 100) *\n (100% - var(--ds-slider-thumb-size))\n );\n}\n\n:host(.slider--vertical.slider--thumb-center) .slider__indicator {\n bottom: calc(var(--ds-slider-start-pct, 0) * 1%);\n height: calc((var(--ds-slider-end-pct, 0) - var(--ds-slider-start-pct, 0)) * 1%);\n}\n\n:host(.slider--vertical) .slider__thumb {\n top: auto;\n bottom: calc((var(--ds-slider-thumb-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size)));\n left: 50%;\n transform: translateX(-50%);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .slider__thumb-visual,\n .slider__thumb-wash {\n transition: none;\n }\n}\n\n@media (forced-colors: active) {\n .slider__rail {\n border: var(--ds-slider-track-stroke) solid var(--ds-forced-color-content);\n box-shadow: none;\n }\n\n .slider__indicator {\n background-color: var(--ds-forced-color-selected);\n }\n\n .slider__thumb-visual {\n border:\n var(--dimension-stroke-width-012) solid\n var(--ds-forced-color-control-boundary);\n background-color: var(--ds-forced-color-surface);\n box-shadow: none;\n }\n\n .slider__input:focus-visible + .slider__thumb-visual,\n :host([data-review-focus]) .slider__thumb[data-index='0'] .slider__thumb-visual {\n outline-color: var(--ds-forced-color-selected);\n }\n}\n",
654
+ "content": "@import '../../utils/control-inactive.css';\n@import '../../utils/field-stack.css';\n@import '../../utils/forced-colors.css';\n\n:host {\n --ds-slider-control-size: var(--dimension-size-400);\n --ds-slider-thumb-size: var(--dimension-size-200);\n --ds-slider-track-size: var(--dimension-size-100);\n --ds-slider-track-stroke: var(--dimension-stroke-width-012);\n --ds-slider-vertical-length: calc(var(--dimension-size-800) * 3);\n box-sizing: border-box;\n width: 100%;\n color: var(--color-foreground-primary);\n}\n\n:host(.slider--sm) {\n --ds-slider-control-size: var(--dimension-size-300);\n --ds-slider-thumb-size: var(--dimension-size-150);\n --ds-slider-track-size: var(--dimension-size-075);\n}\n\n:host(.slider--xs) {\n --ds-slider-control-size: var(--dimension-size-200);\n --ds-slider-thumb-size: var(--dimension-size-100);\n --ds-slider-track-size: var(--dimension-size-050);\n}\n\n:host(.slider--vertical) {\n width: fit-content;\n min-width: var(--dimension-form-width-xs);\n}\n\n.slider__label-row {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: var(--dimension-space-100);\n min-width: 0;\n}\n\n.slider__label {\n min-width: 0;\n}\n\n.slider__value {\n flex-shrink: 0;\n}\n\n.slider__control {\n position: relative;\n width: 100%;\n height: var(--ds-slider-control-size);\n border-radius: var(--dimension-radius-half);\n cursor: default;\n touch-action: none;\n user-select: none;\n}\n\n:host(.slider--vertical) .slider__control {\n align-self: center;\n width: var(--ds-slider-control-size);\n height: var(--ds-slider-vertical-length);\n}\n\n:host(.slider--readonly) .slider__control {\n cursor: default;\n}\n\n/* Inactive: `.ds-control-inactive` (50% opacity) on the host. */\n:host(.ds-control-inactive) .slider__control {\n cursor: not-allowed;\n}\n\n.slider__rail {\n position: absolute;\n top: 50%;\n right: 0;\n left: 0;\n height: var(--ds-slider-track-size);\n overflow: hidden;\n border-radius: var(--dimension-radius-025);\n background-color: transparent;\n box-shadow: inset 0 0 0 var(--ds-slider-track-stroke) var(--color-border-secondary);\n transform: translateY(-50%);\n pointer-events: none;\n}\n\n:host(.slider--thumb-center) .slider__rail {\n right: calc(var(--ds-slider-thumb-size) / 2);\n left: calc(var(--ds-slider-thumb-size) / 2);\n}\n\n.slider__indicator {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: calc(\n var(--ds-slider-thumb-size) / 2 + (var(--ds-slider-end-pct, 0) / 100) *\n (100% - var(--ds-slider-thumb-size))\n );\n border-radius: inherit;\n background-color: var(--color-background-bold-brand);\n}\n\n:host(.slider--range) .slider__indicator {\n left: calc(\n var(--ds-slider-thumb-size) / 2 + (var(--ds-slider-start-pct, 0) / 100) *\n (100% - var(--ds-slider-thumb-size))\n );\n width: calc(\n ((var(--ds-slider-end-pct, 0) - var(--ds-slider-start-pct, 0)) / 100) *\n (100% - var(--ds-slider-thumb-size))\n );\n}\n\n:host(.slider--thumb-center) .slider__indicator {\n left: calc(var(--ds-slider-start-pct, 0) * 1%);\n width: calc((var(--ds-slider-end-pct, 0) - var(--ds-slider-start-pct, 0)) * 1%);\n}\n\n.slider__thumb {\n position: absolute;\n top: 50%;\n left: calc((var(--ds-slider-thumb-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size)));\n width: var(--ds-slider-thumb-size);\n height: var(--ds-slider-thumb-size);\n transform: translateY(-50%);\n z-index: 1;\n}\n\n.slider__thumb--active {\n z-index: 2;\n}\n\n.slider__input {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n margin: 0;\n padding: 0;\n border: 0;\n opacity: 0;\n appearance: none;\n pointer-events: none;\n z-index: 2;\n}\n\n.slider__thumb-visual {\n position: absolute;\n inset: 0;\n box-sizing: border-box;\n overflow: visible;\n border-radius: var(--dimension-radius-025);\n background-color: var(--color-background-primary);\n box-shadow: var(--effect-elevation-elevated-sm);\n pointer-events: none;\n transition:\n background-color var(--effect-motion-short-2),\n box-shadow var(--effect-motion-short-2);\n}\n\n.slider__thumb-wash {\n position: absolute;\n inset: 0;\n border-radius: inherit;\n background-color: transparent;\n transition: background-color var(--effect-motion-short-2);\n}\n\n@media (hover: hover) and (pointer: fine) {\n :host(:not(.slider--readonly):not(.ds-control-inactive))\n .slider__thumb:hover\n .slider__thumb-wash {\n background-color: var(--color-interaction-hover);\n }\n}\n\n:host([data-dragging]:not(.slider--readonly):not(.ds-control-inactive))\n .slider__thumb--active\n .slider__thumb-wash {\n background-color: var(--color-interaction-pressed);\n}\n\n.slider__input:focus-visible + .slider__thumb-visual,\n:host([data-review-focus]) .slider__thumb[data-index='0'] .slider__thumb-visual {\n outline: var(--dimension-stroke-width-025) solid var(--color-interaction-focus);\n outline-offset: var(--dimension-space-025);\n}\n\n:host(.slider--vertical) .slider__rail {\n top: 0;\n right: auto;\n bottom: 0;\n left: 50%;\n width: var(--ds-slider-track-size);\n height: auto;\n transform: translateX(-50%);\n}\n\n:host(.slider--vertical.slider--thumb-center) .slider__rail {\n top: calc(var(--ds-slider-thumb-size) / 2);\n bottom: calc(var(--ds-slider-thumb-size) / 2);\n}\n\n:host(.slider--vertical) .slider__indicator {\n top: auto;\n right: 0;\n bottom: 0;\n left: 0;\n width: auto;\n height: calc(\n var(--ds-slider-thumb-size) / 2 + (var(--ds-slider-end-pct, 0) / 100) *\n (100% - var(--ds-slider-thumb-size))\n );\n}\n\n:host(.slider--vertical.slider--range) .slider__indicator {\n bottom: calc(\n var(--ds-slider-thumb-size) / 2 + (var(--ds-slider-start-pct, 0) / 100) *\n (100% - var(--ds-slider-thumb-size))\n );\n height: calc(\n ((var(--ds-slider-end-pct, 0) - var(--ds-slider-start-pct, 0)) / 100) *\n (100% - var(--ds-slider-thumb-size))\n );\n}\n\n:host(.slider--vertical.slider--thumb-center) .slider__indicator {\n bottom: calc(var(--ds-slider-start-pct, 0) * 1%);\n height: calc((var(--ds-slider-end-pct, 0) - var(--ds-slider-start-pct, 0)) * 1%);\n}\n\n:host(.slider--vertical) .slider__thumb {\n top: auto;\n bottom: calc((var(--ds-slider-thumb-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size)));\n left: 50%;\n transform: translateX(-50%);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .slider__thumb-visual,\n .slider__thumb-wash {\n transition: none;\n }\n}\n\n@media (forced-colors: active) {\n .slider__rail {\n border: var(--ds-slider-track-stroke) solid var(--ds-forced-color-content);\n box-shadow: none;\n }\n\n .slider__indicator {\n background-color: var(--ds-forced-color-selected);\n }\n\n .slider__thumb-visual {\n border: var(--dimension-stroke-width-012) solid var(--ds-forced-color-control-boundary);\n background-color: var(--ds-forced-color-surface);\n box-shadow: none;\n }\n\n .slider__input:focus-visible + .slider__thumb-visual,\n :host([data-review-focus]) .slider__thumb[data-index='0'] .slider__thumb-visual {\n outline-color: var(--ds-forced-color-selected);\n }\n}\n",
655
655
  "type": "registry:ui"
656
656
  },
657
657
  {
658
658
  "path": "src/wc/components/Slider/Slider.tsx",
659
- "content": "import {\n AttachInternals,\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport {\n restoreNumberArrayFormState,\n restoreStringFormState,\n setFormControlValue,\n setRepeatedFormControlValue,\n} from '../../utils';\n\nexport type SliderValue = number | number[];\nexport type SliderSize = 'md' | 'sm' | 'xs';\nexport type SliderOrientation = 'horizontal' | 'vertical';\nexport type SliderThumbAlignment = 'edge' | 'center';\n\nlet sliderId = 0;\nlet sliderExternalLabelId = 0;\n\n@Component({\n tag: 'ds-slider',\n styleUrl: 'Slider.css',\n scoped: true,\n formAssociated: true,\n})\nexport class Slider {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n /** Current value. Assign a two-number array through the JavaScript property for a range slider. */\n @Prop({ mutable: true }) value: SliderValue = 0;\n @Prop() min: number = 0;\n @Prop() max: number = 100;\n @Prop() step: number = 1;\n /** Minimum number of steps kept between two range thumbs. */\n @Prop() minStepsBetweenValues: number = 0;\n /** Visible field label. Supply aria-label when the label is intentionally hidden. */\n @Prop() label: string | undefined;\n /** Show the formatted current value beside the visible label. */\n @Prop() showValue: boolean = true;\n @Prop() size: SliderSize = 'md';\n @Prop() orientation: SliderOrientation = 'horizontal';\n /** Align the thumb edge with full-width rail endpoints, or its center with inset rail endpoints. */\n @Prop() thumbAlignment: SliderThumbAlignment = 'edge';\n @Prop({ reflect: true }) name: string | undefined;\n @Prop({ reflect: true }) form: string | undefined;\n @Prop({ reflect: true }) disabled: boolean = false;\n /** Keep the value focusable and submittable while preventing changes. */\n @Prop({ reflect: true }) readOnly: boolean = false;\n @Prop() isInactive: boolean = false;\n /** Direct accessible name for a single-thumb slider without a visible label. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n /** Id references for a visible single-thumb slider label. */\n @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;\n /** Id references for supporting guidance or error text. */\n @Prop({ attribute: 'aria-describedby' }) ariaDescribedby: string | undefined;\n /** Accessible label for the lower thumb in a range slider. */\n @Prop() startLabel: string = 'Minimum value';\n /** Accessible label for the upper thumb in a range slider. */\n @Prop() endLabel: string = 'Maximum value';\n /** Human-readable value text for a single slider or both range thumbs. */\n @Prop() valueText: string | undefined;\n /** Per-thumb human-readable value text for a range slider. Assign through the JavaScript property. */\n @Prop() valueTexts: string[] = [];\n /** Locale used by Intl.NumberFormat for the visible value. */\n @Prop() locale: string | undefined;\n /** Intl.NumberFormat options. Assign objects through the JavaScript property. */\n @Prop() formatOptions: Intl.NumberFormatOptions | undefined;\n @Prop() valuePrefix: string = '';\n @Prop() valueSuffix: string = '';\n @Prop() rangeSeparator: string = '–';\n /** Id applied to the first native range input. */\n @Prop() inputId: string | undefined;\n\n /** Emitted continuously while the value changes. */\n @Event() dsChange!: EventEmitter<SliderValue>;\n /** Emitted when a pointer or keyboard value change is committed. */\n @Event() dsCommit!: EventEmitter<SliderValue>;\n\n @State() private formDisabled = false;\n @State() private dragging = false;\n @State() private focused = false;\n @State() private touched = false;\n @State() private activeThumbIndex = 0;\n @State() private externalLabelledby: string | undefined;\n\n private readonly generatedId = `ds-slider-${++sliderId}`;\n private readonly labelId = `${this.generatedId}-label`;\n private initialValue: SliderValue = 0;\n private lastCommittedValue: SliderValue = 0;\n private pointerId: number | null = null;\n private pointerStartValue: SliderValue = 0;\n private controlEl: HTMLDivElement | null = null;\n\n componentWillLoad() {\n this.initialValue = this.cloneValue(this.publicValue);\n this.lastCommittedValue = this.cloneValue(this.publicValue);\n this.syncFormValue();\n }\n\n componentDidLoad() {\n this.syncNativeLabels();\n }\n\n @Watch('value')\n @Watch('min')\n @Watch('max')\n @Watch('step')\n @Watch('minStepsBetweenValues')\n @Watch('name')\n @Watch('disabled')\n @Watch('isInactive')\n syncFormValue() {\n const values = this.resolvedValues;\n if (this.isRange) {\n setRepeatedFormControlValue(this.internals, this.name, values, {\n inactive: this.isDisabled,\n });\n } else {\n const value = String(values[0]);\n setFormControlValue(this.internals, this.name ? value : null, {\n inactive: this.isDisabled,\n state: value,\n });\n }\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.value = this.cloneValue(this.initialValue);\n this.lastCommittedValue = this.cloneValue(this.initialValue);\n this.dragging = false;\n this.focused = false;\n this.touched = false;\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n if (this.isRange) {\n const restored = restoreNumberArrayFormState(state, 2);\n this.value = restored.length\n ? restored\n : this.cloneValue(this.initialValue);\n } else {\n const restored = Number(restoreStringFormState(state, 'NaN'));\n if (Number.isFinite(restored)) this.value = restored;\n }\n this.lastCommittedValue = this.cloneValue(this.publicValue);\n }\n\n @Method()\n async setFocus(index: number = 0) {\n const inputs = this.el.querySelectorAll<HTMLInputElement>('.slider__input');\n inputs[Math.max(0, Math.min(index, inputs.length - 1))]?.focus();\n }\n\n private get isDisabled(): boolean {\n return this.disabled || this.isInactive || this.formDisabled;\n }\n\n private get isRange(): boolean {\n return Array.isArray(this.value);\n }\n\n private get safeStep(): number {\n return Number.isFinite(this.step) && this.step > 0 ? this.step : 1;\n }\n\n private get safeMin(): number {\n return Number.isFinite(this.min) ? this.min : 0;\n }\n\n private get safeMax(): number {\n const minimum = this.safeMin;\n return Number.isFinite(this.max) && this.max > minimum ? this.max : minimum + this.safeStep;\n }\n\n private get minimumGap(): number {\n const steps = Number.isFinite(this.minStepsBetweenValues)\n ? Math.max(0, Math.floor(this.minStepsBetweenValues))\n : 0;\n return steps * this.safeStep;\n }\n\n private get resolvedValues(): number[] {\n const minimum = this.safeMin;\n const maximum = this.safeMax;\n if (!this.isRange) return [this.snap(Number(this.value))];\n\n const raw = this.value as number[];\n let lower = this.snap(Number(raw[0] ?? minimum));\n let upper = this.snap(Number(raw[1] ?? maximum));\n if (lower > upper) [lower, upper] = [upper, lower];\n\n const gap = Math.min(this.minimumGap, maximum - minimum);\n if (upper - lower < gap) {\n upper = Math.min(maximum, this.snap(lower + gap));\n lower = Math.max(minimum, this.snap(upper - gap));\n }\n return [lower, upper];\n }\n\n private get publicValue(): SliderValue {\n const values = this.resolvedValues;\n return this.isRange ? [...values] : values[0];\n }\n\n private cloneValue(value: SliderValue): SliderValue {\n return Array.isArray(value) ? [...value] : value;\n }\n\n private valuesEqual(a: SliderValue, b: SliderValue): boolean {\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((value, index) => value === b[index]);\n }\n return !Array.isArray(a) && !Array.isArray(b) && a === b;\n }\n\n private decimalPlaces(value: number): number {\n const text = String(value).toLowerCase();\n if (text.includes('e-')) return Number(text.split('e-')[1]) || 0;\n return text.includes('.') ? text.split('.')[1].length : 0;\n }\n\n private snap(value: number): number {\n const minimum = this.safeMin;\n const maximum = this.safeMax;\n const candidate = Number.isFinite(value) ? value : minimum;\n const stepped = minimum + Math.round((candidate - minimum) / this.safeStep) * this.safeStep;\n const precision = Math.min(10, Math.max(\n this.decimalPlaces(minimum),\n this.decimalPlaces(maximum),\n this.decimalPlaces(this.safeStep),\n ));\n return Number(Math.min(maximum, Math.max(minimum, stepped)).toFixed(precision));\n }\n\n private percent(value: number): number {\n return ((value - this.safeMin) / (this.safeMax - this.safeMin)) * 100;\n }\n\n private allowedMin(index: number, values = this.resolvedValues): number {\n return this.isRange && index === 1\n ? Math.min(this.safeMax, values[0] + this.minimumGap)\n : this.safeMin;\n }\n\n private allowedMax(index: number, values = this.resolvedValues): number {\n return this.isRange && index === 0\n ? Math.max(this.safeMin, values[1] - this.minimumGap)\n : this.safeMax;\n }\n\n private setValueAt(index: number, next: number): boolean {\n const previous = this.publicValue;\n const values = this.resolvedValues;\n const constrained = Math.min(this.allowedMax(index, values), Math.max(this.allowedMin(index, values), this.snap(next)));\n values[index] = constrained;\n const nextValue: SliderValue = this.isRange ? [...values] : values[0];\n if (this.valuesEqual(previous, nextValue)) return false;\n\n this.value = nextValue;\n this.dsChange.emit(this.cloneValue(nextValue));\n return true;\n }\n\n private commitIfChanged() {\n const current = this.publicValue;\n if (this.valuesEqual(current, this.lastCommittedValue)) return;\n this.lastCommittedValue = this.cloneValue(current);\n this.dsCommit.emit(this.cloneValue(current));\n }\n\n private syncNativeLabels() {\n if (this.label || this.ariaLabel || this.ariaLabelledby || this.isRange) return;\n const ids = (Array.from(this.internals.labels) as HTMLLabelElement[]).map(label => {\n if (!label.id) label.id = `ds-slider-label-${++sliderExternalLabelId}`;\n return label.id;\n });\n if (ids.length) this.externalLabelledby = ids.join(' ');\n }\n\n private formattedValue(value: number, index: number): string {\n const authored = this.valueTexts[index] ?? this.valueText;\n if (authored) return authored;\n let number: string;\n try {\n number = new Intl.NumberFormat(this.locale, this.formatOptions).format(value);\n } catch {\n number = String(value);\n }\n return `${this.valuePrefix}${number}${this.valueSuffix}`;\n }\n\n private ariaValueText(value: number, index: number): string | undefined {\n if (this.valueTexts[index] || this.valueText || this.valuePrefix || this.valueSuffix || this.formatOptions) {\n return this.formattedValue(value, index);\n }\n return undefined;\n }\n\n private handleNativeInput = (event: Event, index: number) => {\n const input = event.target as HTMLInputElement;\n this.activeThumbIndex = index;\n if (this.readOnly || this.isDisabled) {\n input.value = String(this.resolvedValues[index]);\n return;\n }\n this.setValueAt(index, Number(input.value));\n };\n\n private handleNativeChange = () => {\n if (!this.readOnly && !this.isDisabled) this.commitIfChanged();\n };\n\n private handleKeyDown = (event: KeyboardEvent) => {\n if (!this.readOnly) return;\n if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'PageUp', 'PageDown'].includes(event.key)) {\n event.preventDefault();\n }\n };\n\n private handleFocus = (index: number) => {\n this.activeThumbIndex = index;\n this.focused = true;\n };\n\n private handleBlur = (event: FocusEvent) => {\n const next = event.relatedTarget;\n if (next instanceof Node && this.el.contains(next)) return;\n this.focused = false;\n this.touched = true;\n };\n\n private handleHostClick = (event: MouseEvent) => {\n if (event.target !== this.el || this.isDisabled) return;\n const inputs = this.el.querySelectorAll<HTMLInputElement>('.slider__input');\n inputs[Math.min(this.activeThumbIndex, inputs.length - 1)]?.focus();\n };\n\n private pointerPercent(event: PointerEvent): number {\n const control = this.controlEl;\n if (!control) return 0;\n const rect = control.getBoundingClientRect();\n const thumb = this.el.querySelector<HTMLElement>('.slider__thumb');\n const thumbSize = thumb\n ? this.orientation === 'horizontal' ? thumb.offsetWidth : thumb.offsetHeight\n : 0;\n // Both alignment modes keep the complete thumb inside the control. Edge\n // alignment lets the rail span the control while center alignment insets\n // the rail to the same thumb-center travel.\n const inset = thumbSize / 2;\n\n if (this.orientation === 'vertical') {\n const travel = Math.max(1, rect.height - inset * 2);\n return Math.min(1, Math.max(0, (rect.bottom - inset - event.clientY) / travel));\n }\n\n const travel = Math.max(1, rect.width - inset * 2);\n const percent = (event.clientX - rect.left - inset) / travel;\n return Math.min(1, Math.max(0, percent));\n }\n\n private valueFromPointer(event: PointerEvent): number {\n return this.snap(this.safeMin + this.pointerPercent(event) * (this.safeMax - this.safeMin));\n }\n\n private closestThumbIndex(value: number, target: EventTarget | null): number {\n const targetElement = target instanceof HTMLElement\n ? target.closest<HTMLElement>('.slider__thumb')\n : null;\n if (targetElement?.dataset['index']) return Number(targetElement.dataset['index']);\n if (!this.isRange) return 0;\n\n const values = this.resolvedValues;\n const lowerDistance = Math.abs(value - values[0]);\n const upperDistance = Math.abs(value - values[1]);\n if (lowerDistance === upperDistance) return value >= values[0] ? 1 : 0;\n return lowerDistance < upperDistance ? 0 : 1;\n }\n\n private handlePointerDown = (event: PointerEvent) => {\n if (event.button !== 0 || this.isDisabled || this.readOnly) return;\n event.preventDefault();\n const value = this.valueFromPointer(event);\n const index = this.closestThumbIndex(value, event.target);\n this.activeThumbIndex = index;\n this.pointerId = event.pointerId;\n this.pointerStartValue = this.cloneValue(this.publicValue);\n this.dragging = true;\n this.setValueAt(index, value);\n this.controlEl?.setPointerCapture(event.pointerId);\n requestAnimationFrame(() => this.el.querySelectorAll<HTMLInputElement>('.slider__input')[index]?.focus({ preventScroll: true }));\n };\n\n private handlePointerMove = (event: PointerEvent) => {\n if (this.pointerId !== event.pointerId || !this.dragging) return;\n event.preventDefault();\n this.setValueAt(this.activeThumbIndex, this.valueFromPointer(event));\n };\n\n private endPointerInteraction(event: PointerEvent, canceled: boolean) {\n if (this.pointerId !== event.pointerId) return;\n if (this.controlEl?.hasPointerCapture(event.pointerId)) this.controlEl.releasePointerCapture(event.pointerId);\n this.pointerId = null;\n this.dragging = false;\n this.touched = true;\n if (!canceled && !this.valuesEqual(this.pointerStartValue, this.publicValue)) this.commitIfChanged();\n }\n\n private renderThumb(value: number, index: number) {\n const inputId = index === 0\n ? this.inputId ?? `${this.generatedId}-input`\n : `${this.inputId ?? `${this.generatedId}-input`}-end`;\n const rangeLabel = index === 0 ? this.startLabel : this.endLabel;\n const labelledby = this.isRange\n ? undefined\n : this.ariaLabelledby ?? (this.label ? this.labelId : this.externalLabelledby);\n const directLabel = this.isRange ? rangeLabel : labelledby ? undefined : this.ariaLabel;\n\n return (\n <div\n class={{\n slider__thumb: true,\n 'slider__thumb--active': index === this.activeThumbIndex,\n }}\n data-index={String(index)}\n style={{ '--ds-slider-thumb-pct': String(this.percent(value)) } as Record<string, string>}\n >\n <input\n class=\"slider__input\"\n type=\"range\"\n id={inputId}\n min={this.allowedMin(index)}\n max={this.allowedMax(index)}\n step={this.safeStep}\n value={value}\n disabled={this.isDisabled}\n aria-label={directLabel}\n aria-labelledby={labelledby}\n aria-describedby={this.ariaDescribedby}\n aria-valuetext={this.ariaValueText(value, index)}\n aria-orientation={this.orientation === 'vertical' ? 'vertical' : undefined}\n aria-readonly={this.readOnly ? 'true' : undefined}\n onInput={(event: Event) => this.handleNativeInput(event, index)}\n onChange={this.handleNativeChange}\n onKeyDown={this.handleKeyDown}\n onFocus={() => this.handleFocus(index)}\n onBlur={this.handleBlur}\n />\n <span class=\"slider__thumb-visual\" aria-hidden=\"true\"><span class=\"slider__thumb-wash\" /></span>\n </div>\n );\n }\n\n render() {\n const values = this.resolvedValues;\n const first = values[0];\n const last = values[values.length - 1];\n const firstInputId = this.inputId ?? `${this.generatedId}-input`;\n const dirty = !this.valuesEqual(this.publicValue, this.initialValue);\n const valueDisplay = values.map((value, index) => this.formattedValue(value, index)).join(` ${this.rangeSeparator} `);\n\n return (\n <Host\n aria-disabled={this.isDisabled ? 'true' : undefined}\n class={{\n slider: true,\n 'ds-field-stack': true,\n [`slider--${this.size}`]: true,\n [`slider--${this.orientation}`]: true,\n [`slider--thumb-${this.thumbAlignment}`]: true,\n 'slider--range': this.isRange,\n 'slider--readonly': this.readOnly,\n 'ds-control-inactive': this.isDisabled,\n }}\n data-orientation={this.orientation}\n data-disabled={this.isDisabled ? '' : undefined}\n data-readonly={this.readOnly ? '' : undefined}\n data-dragging={this.dragging ? '' : undefined}\n data-focused={this.focused ? '' : undefined}\n data-filled=\"\"\n data-dirty={dirty ? '' : undefined}\n data-touched={this.touched ? '' : undefined}\n data-valid={!this.isDisabled ? '' : undefined}\n onClick={this.handleHostClick}\n >\n {(this.label || this.showValue) && (\n <div class=\"slider__label-row\">\n {this.label && (\n <ds-text\n class=\"slider__label\"\n as=\"label\"\n variant=\"text-body-small\"\n color=\"primary\"\n emphasis\n for={firstInputId}\n textId={this.labelId}\n >\n {this.label}\n </ds-text>\n )}\n {this.showValue && (\n <ds-text\n class=\"slider__value\"\n as=\"span\"\n variant=\"text-body-small\"\n color=\"primary\"\n fontFeature=\"tabular-nums\"\n >\n {valueDisplay}\n </ds-text>\n )}\n </div>\n )}\n <div\n class=\"slider__control\"\n ref={element => { this.controlEl = element ?? null; }}\n style={{\n '--ds-slider-start-pct': String(this.isRange ? this.percent(first) : 0),\n '--ds-slider-end-pct': String(this.percent(last)),\n } as Record<string, string>}\n onPointerDown={this.handlePointerDown}\n onPointerMove={this.handlePointerMove}\n onPointerUp={(event: PointerEvent) => this.endPointerInteraction(event, false)}\n onPointerCancel={(event: PointerEvent) => this.endPointerInteraction(event, true)}\n >\n <div class=\"slider__rail\" aria-hidden=\"true\"><div class=\"slider__indicator\" /></div>\n {values.map((value, index) => this.renderThumb(value, index))}\n </div>\n </Host>\n );\n }\n}\n",
659
+ "content": "import {\n AttachInternals,\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport {\n restoreNumberArrayFormState,\n restoreStringFormState,\n setFormControlValue,\n setRepeatedFormControlValue,\n} from '../../utils';\n\nexport type SliderValue = number | number[];\nexport type SliderSize = 'md' | 'sm' | 'xs';\nexport type SliderOrientation = 'horizontal' | 'vertical';\nexport type SliderThumbAlignment = 'edge' | 'center';\n\nlet sliderId = 0;\nlet sliderExternalLabelId = 0;\n\n@Component({\n tag: 'ds-slider',\n styleUrl: 'Slider.css',\n scoped: true,\n formAssociated: true,\n})\nexport class Slider {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n /** Current value. Assign a two-number array through the JavaScript property for a range slider. */\n @Prop({ mutable: true }) value: SliderValue = 0;\n @Prop() min: number = 0;\n @Prop() max: number = 100;\n @Prop() step: number = 1;\n /** Minimum number of steps kept between two range thumbs. */\n @Prop() minStepsBetweenValues: number = 0;\n /** Visible field label. Supply aria-label when the label is intentionally hidden. */\n @Prop() label: string | undefined;\n /** Show the formatted current value beside the visible label. */\n @Prop() showValue: boolean = true;\n @Prop() size: SliderSize = 'md';\n @Prop() orientation: SliderOrientation = 'horizontal';\n /** Align the thumb edge with full-width rail endpoints, or its center with inset rail endpoints. */\n @Prop() thumbAlignment: SliderThumbAlignment = 'edge';\n @Prop({ reflect: true }) name: string | undefined;\n @Prop({ reflect: true }) form: string | undefined;\n @Prop({ reflect: true }) disabled: boolean = false;\n /** Keep the value focusable and submittable while preventing changes. */\n @Prop({ reflect: true }) readOnly: boolean = false;\n @Prop() isInactive: boolean = false;\n /** Direct accessible name for a single-thumb slider without a visible label. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n /** Id references for a visible single-thumb slider label. */\n @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;\n /** Id references for supporting guidance or error text. */\n @Prop({ attribute: 'aria-describedby' }) ariaDescribedby: string | undefined;\n /** Accessible label for the lower thumb in a range slider. */\n @Prop() startLabel: string = 'Minimum value';\n /** Accessible label for the upper thumb in a range slider. */\n @Prop() endLabel: string = 'Maximum value';\n /** Human-readable value text for a single slider or both range thumbs. */\n @Prop() valueText: string | undefined;\n /** Per-thumb human-readable value text for a range slider. Assign through the JavaScript property. */\n @Prop() valueTexts: string[] = [];\n /** Locale used by Intl.NumberFormat for the visible value. */\n @Prop() locale: string | undefined;\n /** Intl.NumberFormat options. Assign objects through the JavaScript property. */\n @Prop() formatOptions: Intl.NumberFormatOptions | undefined;\n @Prop() valuePrefix: string = '';\n @Prop() valueSuffix: string = '';\n @Prop() rangeSeparator: string = '–';\n /** Id applied to the first native range input. */\n @Prop() inputId: string | undefined;\n\n /** Emitted continuously while the value changes. */\n @Event() dsChange!: EventEmitter<SliderValue>;\n /** Emitted when a pointer or keyboard value change is committed. */\n @Event() dsCommit!: EventEmitter<SliderValue>;\n\n @State() private formDisabled = false;\n @State() private dragging = false;\n @State() private focused = false;\n @State() private touched = false;\n @State() private activeThumbIndex = 0;\n @State() private externalLabelledby: string | undefined;\n\n private readonly generatedId = `ds-slider-${++sliderId}`;\n private readonly labelId = `${this.generatedId}-label`;\n private initialValue: SliderValue = 0;\n private lastCommittedValue: SliderValue = 0;\n private pointerId: number | null = null;\n private pointerStartValue: SliderValue = 0;\n private controlEl: HTMLDivElement | null = null;\n\n componentWillLoad() {\n this.initialValue = this.cloneValue(this.publicValue);\n this.lastCommittedValue = this.cloneValue(this.publicValue);\n this.syncFormValue();\n }\n\n componentDidLoad() {\n this.syncNativeLabels();\n }\n\n @Watch('value')\n @Watch('min')\n @Watch('max')\n @Watch('step')\n @Watch('minStepsBetweenValues')\n @Watch('name')\n @Watch('disabled')\n @Watch('isInactive')\n syncFormValue() {\n const values = this.resolvedValues;\n if (this.isRange) {\n setRepeatedFormControlValue(this.internals, this.name, values, {\n inactive: this.isDisabled,\n });\n } else {\n const value = String(values[0]);\n setFormControlValue(this.internals, this.name ? value : null, {\n inactive: this.isDisabled,\n state: value,\n });\n }\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.value = this.cloneValue(this.initialValue);\n this.lastCommittedValue = this.cloneValue(this.initialValue);\n this.dragging = false;\n this.focused = false;\n this.touched = false;\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n if (this.isRange) {\n const restored = restoreNumberArrayFormState(state, 2);\n this.value = restored.length ? restored : this.cloneValue(this.initialValue);\n } else {\n const restored = Number(restoreStringFormState(state, 'NaN'));\n if (Number.isFinite(restored)) this.value = restored;\n }\n this.lastCommittedValue = this.cloneValue(this.publicValue);\n }\n\n @Method()\n async setFocus(index: number = 0) {\n const inputs = this.el.querySelectorAll<HTMLInputElement>('.slider__input');\n inputs[Math.max(0, Math.min(index, inputs.length - 1))]?.focus();\n }\n\n private get isDisabled(): boolean {\n return this.disabled || this.isInactive || this.formDisabled;\n }\n\n private get isRange(): boolean {\n return Array.isArray(this.value);\n }\n\n private get safeStep(): number {\n return Number.isFinite(this.step) && this.step > 0 ? this.step : 1;\n }\n\n private get safeMin(): number {\n return Number.isFinite(this.min) ? this.min : 0;\n }\n\n private get safeMax(): number {\n const minimum = this.safeMin;\n return Number.isFinite(this.max) && this.max > minimum ? this.max : minimum + this.safeStep;\n }\n\n private get minimumGap(): number {\n const steps = Number.isFinite(this.minStepsBetweenValues)\n ? Math.max(0, Math.floor(this.minStepsBetweenValues))\n : 0;\n return steps * this.safeStep;\n }\n\n private get resolvedValues(): number[] {\n const minimum = this.safeMin;\n const maximum = this.safeMax;\n if (!this.isRange) return [this.snap(Number(this.value))];\n\n const raw = this.value as number[];\n let lower = this.snap(Number(raw[0] ?? minimum));\n let upper = this.snap(Number(raw[1] ?? maximum));\n if (lower > upper) [lower, upper] = [upper, lower];\n\n const gap = Math.min(this.minimumGap, maximum - minimum);\n if (upper - lower < gap) {\n upper = Math.min(maximum, this.snap(lower + gap));\n lower = Math.max(minimum, this.snap(upper - gap));\n }\n return [lower, upper];\n }\n\n private get publicValue(): SliderValue {\n const values = this.resolvedValues;\n return this.isRange ? [...values] : values[0];\n }\n\n private cloneValue(value: SliderValue): SliderValue {\n return Array.isArray(value) ? [...value] : value;\n }\n\n private valuesEqual(a: SliderValue, b: SliderValue): boolean {\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((value, index) => value === b[index]);\n }\n return !Array.isArray(a) && !Array.isArray(b) && a === b;\n }\n\n private decimalPlaces(value: number): number {\n const text = String(value).toLowerCase();\n if (text.includes('e-')) return Number(text.split('e-')[1]) || 0;\n return text.includes('.') ? text.split('.')[1].length : 0;\n }\n\n private snap(value: number): number {\n const minimum = this.safeMin;\n const maximum = this.safeMax;\n const candidate = Number.isFinite(value) ? value : minimum;\n const stepped = minimum + Math.round((candidate - minimum) / this.safeStep) * this.safeStep;\n const precision = Math.min(\n 10,\n Math.max(\n this.decimalPlaces(minimum),\n this.decimalPlaces(maximum),\n this.decimalPlaces(this.safeStep)\n )\n );\n return Number(Math.min(maximum, Math.max(minimum, stepped)).toFixed(precision));\n }\n\n private percent(value: number): number {\n return ((value - this.safeMin) / (this.safeMax - this.safeMin)) * 100;\n }\n\n private allowedMin(index: number, values = this.resolvedValues): number {\n return this.isRange && index === 1\n ? Math.min(this.safeMax, values[0] + this.minimumGap)\n : this.safeMin;\n }\n\n private allowedMax(index: number, values = this.resolvedValues): number {\n return this.isRange && index === 0\n ? Math.max(this.safeMin, values[1] - this.minimumGap)\n : this.safeMax;\n }\n\n private setValueAt(index: number, next: number): boolean {\n const previous = this.publicValue;\n const values = this.resolvedValues;\n const constrained = Math.min(\n this.allowedMax(index, values),\n Math.max(this.allowedMin(index, values), this.snap(next))\n );\n values[index] = constrained;\n const nextValue: SliderValue = this.isRange ? [...values] : values[0];\n if (this.valuesEqual(previous, nextValue)) return false;\n\n this.value = nextValue;\n this.dsChange.emit(this.cloneValue(nextValue));\n return true;\n }\n\n private commitIfChanged() {\n const current = this.publicValue;\n if (this.valuesEqual(current, this.lastCommittedValue)) return;\n this.lastCommittedValue = this.cloneValue(current);\n this.dsCommit.emit(this.cloneValue(current));\n }\n\n private syncNativeLabels() {\n if (this.label || this.ariaLabel || this.ariaLabelledby || this.isRange) return;\n const ids = (Array.from(this.internals.labels) as HTMLLabelElement[]).map(label => {\n if (!label.id) label.id = `ds-slider-label-${++sliderExternalLabelId}`;\n return label.id;\n });\n if (ids.length) this.externalLabelledby = ids.join(' ');\n }\n\n private formattedValue(value: number, index: number): string {\n const authored = this.valueTexts[index] ?? this.valueText;\n if (authored) return authored;\n let number: string;\n try {\n number = new Intl.NumberFormat(this.locale, this.formatOptions).format(value);\n } catch {\n number = String(value);\n }\n return `${this.valuePrefix}${number}${this.valueSuffix}`;\n }\n\n private ariaValueText(value: number, index: number): string | undefined {\n if (\n this.valueTexts[index] ||\n this.valueText ||\n this.valuePrefix ||\n this.valueSuffix ||\n this.formatOptions\n ) {\n return this.formattedValue(value, index);\n }\n return undefined;\n }\n\n private handleNativeInput = (event: Event, index: number) => {\n const input = event.target as HTMLInputElement;\n this.activeThumbIndex = index;\n if (this.readOnly || this.isDisabled) {\n input.value = String(this.resolvedValues[index]);\n return;\n }\n this.setValueAt(index, Number(input.value));\n };\n\n private handleNativeChange = () => {\n if (!this.readOnly && !this.isDisabled) this.commitIfChanged();\n };\n\n private handleKeyDown = (event: KeyboardEvent) => {\n if (!this.readOnly) return;\n if (\n [\n 'ArrowLeft',\n 'ArrowRight',\n 'ArrowUp',\n 'ArrowDown',\n 'Home',\n 'End',\n 'PageUp',\n 'PageDown',\n ].includes(event.key)\n ) {\n event.preventDefault();\n }\n };\n\n private handleFocus = (index: number) => {\n this.activeThumbIndex = index;\n this.focused = true;\n };\n\n private handleBlur = (event: FocusEvent) => {\n const next = event.relatedTarget;\n if (next instanceof Node && this.el.contains(next)) return;\n this.focused = false;\n this.touched = true;\n };\n\n private handleHostClick = (event: MouseEvent) => {\n if (event.target !== this.el || this.isDisabled) return;\n const inputs = this.el.querySelectorAll<HTMLInputElement>('.slider__input');\n inputs[Math.min(this.activeThumbIndex, inputs.length - 1)]?.focus();\n };\n\n private pointerPercent(event: PointerEvent): number {\n const control = this.controlEl;\n if (!control) return 0;\n const rect = control.getBoundingClientRect();\n const thumb = this.el.querySelector<HTMLElement>('.slider__thumb');\n const thumbSize = thumb\n ? this.orientation === 'horizontal'\n ? thumb.offsetWidth\n : thumb.offsetHeight\n : 0;\n // Both alignment modes keep the complete thumb inside the control. Edge\n // alignment lets the rail span the control while center alignment insets\n // the rail to the same thumb-center travel.\n const inset = thumbSize / 2;\n\n if (this.orientation === 'vertical') {\n const travel = Math.max(1, rect.height - inset * 2);\n return Math.min(1, Math.max(0, (rect.bottom - inset - event.clientY) / travel));\n }\n\n const travel = Math.max(1, rect.width - inset * 2);\n const percent = (event.clientX - rect.left - inset) / travel;\n return Math.min(1, Math.max(0, percent));\n }\n\n private valueFromPointer(event: PointerEvent): number {\n return this.snap(this.safeMin + this.pointerPercent(event) * (this.safeMax - this.safeMin));\n }\n\n private closestThumbIndex(value: number, target: EventTarget | null): number {\n const targetElement =\n target instanceof HTMLElement ? target.closest<HTMLElement>('.slider__thumb') : null;\n if (targetElement?.dataset['index']) return Number(targetElement.dataset['index']);\n if (!this.isRange) return 0;\n\n const values = this.resolvedValues;\n const lowerDistance = Math.abs(value - values[0]);\n const upperDistance = Math.abs(value - values[1]);\n if (lowerDistance === upperDistance) return value >= values[0] ? 1 : 0;\n return lowerDistance < upperDistance ? 0 : 1;\n }\n\n private handlePointerDown = (event: PointerEvent) => {\n if (event.button !== 0 || this.isDisabled || this.readOnly) return;\n event.preventDefault();\n const value = this.valueFromPointer(event);\n const index = this.closestThumbIndex(value, event.target);\n this.activeThumbIndex = index;\n this.pointerId = event.pointerId;\n this.pointerStartValue = this.cloneValue(this.publicValue);\n this.dragging = true;\n this.setValueAt(index, value);\n this.controlEl?.setPointerCapture(event.pointerId);\n requestAnimationFrame(() => {\n const input = this.el.querySelectorAll<HTMLInputElement>('.slider__input')[index];\n input?.focus({ preventScroll: true });\n });\n };\n\n private handlePointerMove = (event: PointerEvent) => {\n if (this.pointerId !== event.pointerId || !this.dragging) return;\n event.preventDefault();\n this.setValueAt(this.activeThumbIndex, this.valueFromPointer(event));\n };\n\n private endPointerInteraction(event: PointerEvent, canceled: boolean) {\n if (this.pointerId !== event.pointerId) return;\n if (this.controlEl?.hasPointerCapture(event.pointerId))\n this.controlEl.releasePointerCapture(event.pointerId);\n this.pointerId = null;\n this.dragging = false;\n this.touched = true;\n if (!canceled && !this.valuesEqual(this.pointerStartValue, this.publicValue))\n this.commitIfChanged();\n }\n\n private renderThumb(value: number, index: number) {\n const inputId =\n index === 0\n ? (this.inputId ?? `${this.generatedId}-input`)\n : `${this.inputId ?? `${this.generatedId}-input`}-end`;\n const rangeLabel = index === 0 ? this.startLabel : this.endLabel;\n const labelledby = this.isRange\n ? undefined\n : (this.ariaLabelledby ?? (this.label ? this.labelId : this.externalLabelledby));\n const directLabel = this.isRange ? rangeLabel : labelledby ? undefined : this.ariaLabel;\n\n return (\n <div\n class={{\n slider__thumb: true,\n 'slider__thumb--active': index === this.activeThumbIndex,\n }}\n data-index={String(index)}\n style={{ '--ds-slider-thumb-pct': String(this.percent(value)) } as Record<string, string>}\n >\n <input\n class=\"slider__input\"\n type=\"range\"\n id={inputId}\n min={this.allowedMin(index)}\n max={this.allowedMax(index)}\n step={this.safeStep}\n value={value}\n disabled={this.isDisabled}\n aria-label={directLabel}\n aria-labelledby={labelledby}\n aria-describedby={this.ariaDescribedby}\n aria-valuetext={this.ariaValueText(value, index)}\n aria-orientation={this.orientation === 'vertical' ? 'vertical' : undefined}\n aria-readonly={this.readOnly ? 'true' : undefined}\n onInput={(event: Event) => this.handleNativeInput(event, index)}\n onChange={this.handleNativeChange}\n onKeyDown={this.handleKeyDown}\n onFocus={() => this.handleFocus(index)}\n onBlur={this.handleBlur}\n />\n <span class=\"slider__thumb-visual\" aria-hidden=\"true\">\n <span class=\"slider__thumb-wash\" />\n </span>\n </div>\n );\n }\n\n render() {\n const values = this.resolvedValues;\n const first = values[0];\n const last = values[values.length - 1];\n const firstInputId = this.inputId ?? `${this.generatedId}-input`;\n const dirty = !this.valuesEqual(this.publicValue, this.initialValue);\n const valueDisplay = values\n .map((value, index) => this.formattedValue(value, index))\n .join(` ${this.rangeSeparator} `);\n\n return (\n <Host\n aria-disabled={this.isDisabled ? 'true' : undefined}\n class={{\n slider: true,\n 'ds-field-stack': true,\n [`slider--${this.size}`]: true,\n [`slider--${this.orientation}`]: true,\n [`slider--thumb-${this.thumbAlignment}`]: true,\n 'slider--range': this.isRange,\n 'slider--readonly': this.readOnly,\n 'ds-control-inactive': this.isDisabled,\n }}\n data-orientation={this.orientation}\n data-disabled={this.isDisabled ? '' : undefined}\n data-readonly={this.readOnly ? '' : undefined}\n data-dragging={this.dragging ? '' : undefined}\n data-focused={this.focused ? '' : undefined}\n data-filled=\"\"\n data-dirty={dirty ? '' : undefined}\n data-touched={this.touched ? '' : undefined}\n data-valid={!this.isDisabled ? '' : undefined}\n onClick={this.handleHostClick}\n >\n {(this.label || this.showValue) && (\n <div class=\"slider__label-row\">\n {this.label && (\n <ds-text\n class=\"slider__label\"\n as=\"label\"\n variant=\"text-body-small\"\n color=\"primary\"\n emphasis\n for={firstInputId}\n textId={this.labelId}\n >\n {this.label}\n </ds-text>\n )}\n {this.showValue && (\n <ds-text\n class=\"slider__value\"\n as=\"span\"\n variant=\"text-body-small\"\n color=\"primary\"\n fontFeature=\"tabular-nums\"\n >\n {valueDisplay}\n </ds-text>\n )}\n </div>\n )}\n <div\n class=\"slider__control\"\n ref={element => {\n this.controlEl = element ?? null;\n }}\n style={\n {\n '--ds-slider-start-pct': String(this.isRange ? this.percent(first) : 0),\n '--ds-slider-end-pct': String(this.percent(last)),\n } as Record<string, string>\n }\n onPointerDown={this.handlePointerDown}\n onPointerMove={this.handlePointerMove}\n onPointerUp={(event: PointerEvent) => this.endPointerInteraction(event, false)}\n onPointerCancel={(event: PointerEvent) => this.endPointerInteraction(event, true)}\n >\n <div class=\"slider__rail\" aria-hidden=\"true\">\n <div class=\"slider__indicator\" />\n </div>\n {values.map((value, index) => this.renderThumb(value, index))}\n </div>\n </Host>\n );\n }\n}\n",
660
660
  "type": "registry:ui"
661
661
  }
662
662
  ]