@ds-mo/ui 15.3.0 → 15.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (430) hide show
  1. package/dist/.package-ready.json +1 -1
  2. package/dist/agent-patterns.json +1 -1
  3. package/dist/agent.json +506 -31
  4. package/dist/angular/ds-checkbox-group.d.ts +12 -0
  5. package/dist/angular/ds-checkbox-group.js +38 -0
  6. package/dist/angular/ds-checkbox.d.ts +1 -1
  7. package/dist/angular/ds-checkbox.js +3 -3
  8. package/dist/angular/ds-inline-banner-settings.d.ts +12 -0
  9. package/dist/angular/ds-inline-banner-settings.js +38 -0
  10. package/dist/angular/ds-radio.d.ts +1 -1
  11. package/dist/angular/ds-radio.js +3 -3
  12. package/dist/angular/ds-setting-row-checkbox.d.ts +12 -0
  13. package/dist/angular/ds-setting-row-checkbox.js +38 -0
  14. package/dist/angular/ds-setting-row-radio.d.ts +12 -0
  15. package/dist/angular/ds-setting-row-radio.js +38 -0
  16. package/dist/angular/ds-setting-row-toggle.d.ts +18 -0
  17. package/dist/angular/{ds-setting-row.js → ds-setting-row-toggle.js} +12 -12
  18. package/dist/angular/index.d.ts +1 -1
  19. package/dist/angular/index.js +5 -1
  20. package/dist/angular/proxies.d.ts +5 -1
  21. package/dist/angular/proxies.js +5 -1
  22. package/dist/components/ds-agent-activity.js +1 -1
  23. package/dist/components/ds-agent-questionnaire.js +1 -1
  24. package/dist/components/ds-agent-response.js +1 -1
  25. package/dist/components/ds-agent-response.js.map +1 -1
  26. package/dist/components/ds-agent-source-list.js +1 -1
  27. package/dist/components/ds-agent-tool-call.js +1 -1
  28. package/dist/components/ds-attachment-list.js +1 -1
  29. package/dist/components/ds-badge.js +1 -1
  30. package/dist/components/ds-banner.js +1 -1
  31. package/dist/components/ds-bar-action.js +1 -1
  32. package/dist/components/ds-bar-nav.js +1 -1
  33. package/dist/components/ds-bar-page-title.js +1 -1
  34. package/dist/components/ds-bar-title.js +1 -1
  35. package/dist/components/ds-bar-workflow.js +1 -1
  36. package/dist/components/ds-breadcrumb.js +1 -1
  37. package/dist/components/ds-button-filled.js +1 -1
  38. package/dist/components/ds-button-inverted.js +1 -1
  39. package/dist/components/ds-button-inverted.js.map +1 -1
  40. package/dist/components/ds-button-unfilled.js +1 -1
  41. package/dist/components/ds-calendar.js +1 -1
  42. package/dist/components/ds-card-action-center.js +1 -1
  43. package/dist/components/ds-card-action-center.js.map +1 -1
  44. package/dist/components/ds-card-chart.js +1 -1
  45. package/dist/components/ds-card-navigation.js +1 -1
  46. package/dist/components/ds-card-navigation.js.map +1 -1
  47. package/dist/components/ds-card-overview.js +1 -1
  48. package/dist/components/ds-card-overview.js.map +1 -1
  49. package/dist/components/ds-card-setting.js +1 -1
  50. package/dist/components/ds-card-setting.js.map +1 -1
  51. package/dist/components/ds-card-settings-scope.js +1 -1
  52. package/dist/components/ds-card-settings-scope.js.map +1 -1
  53. package/dist/components/ds-chart-legend.js +1 -1
  54. package/dist/components/ds-chart-legend.js.map +1 -1
  55. package/dist/components/ds-chart.js +1 -1
  56. package/dist/components/{ds-setting-row.d.ts → ds-checkbox-group.d.ts} +4 -4
  57. package/dist/components/ds-checkbox-group.js +2 -0
  58. package/dist/components/ds-checkbox-group.js.map +1 -0
  59. package/dist/components/ds-checkbox.js +1 -1
  60. package/dist/components/ds-chip.js +1 -1
  61. package/dist/components/ds-code-block.js +1 -1
  62. package/dist/components/ds-conversation-list-item.js +1 -1
  63. package/dist/components/ds-conversation-list-item.js.map +1 -1
  64. package/dist/components/ds-conversation-list-section.js +1 -1
  65. package/dist/components/ds-conversation-list.js +1 -1
  66. package/dist/components/ds-data-customize.js +1 -1
  67. package/dist/components/ds-data-filter.js +1 -1
  68. package/dist/components/ds-data-group.js +1 -1
  69. package/dist/components/ds-data-preferences.js +1 -1
  70. package/dist/components/ds-data-saved-views.js +1 -1
  71. package/dist/components/ds-data-search.js +1 -1
  72. package/dist/components/ds-data-search.js.map +1 -1
  73. package/dist/components/ds-data-sort.js +1 -1
  74. package/dist/components/ds-data-toolbar.js +1 -1
  75. package/dist/components/ds-divider.js +1 -1
  76. package/dist/components/ds-empty-state.js +1 -1
  77. package/dist/components/ds-field.js +1 -1
  78. package/dist/components/ds-filter-menu.js +1 -1
  79. package/dist/components/ds-inline-banner-settings.d.ts +11 -0
  80. package/dist/components/ds-inline-banner-settings.js +2 -0
  81. package/dist/components/ds-inline-banner-settings.js.map +1 -0
  82. package/dist/components/ds-input-date.js +1 -1
  83. package/dist/components/ds-input-date.js.map +1 -1
  84. package/dist/components/ds-input-time.js +1 -1
  85. package/dist/components/ds-input-time.js.map +1 -1
  86. package/dist/components/ds-input.js +1 -1
  87. package/dist/components/ds-map-cluster-marker.js +1 -1
  88. package/dist/components/ds-map-entity-marker.js +1 -1
  89. package/dist/components/ds-markdown.js +1 -1
  90. package/dist/components/ds-menu.js +1 -1
  91. package/dist/components/ds-message-actions.js +1 -1
  92. package/dist/components/ds-message-bubble.js +1 -1
  93. package/dist/components/ds-message-bubble.js.map +1 -1
  94. package/dist/components/ds-message-composer.js +1 -1
  95. package/dist/components/ds-message-composer.js.map +1 -1
  96. package/dist/components/ds-message-scroller.js +1 -1
  97. package/dist/components/ds-message.js +1 -1
  98. package/dist/components/ds-mobile-bar-nav.js +1 -1
  99. package/dist/components/ds-mobile-header.js +1 -1
  100. package/dist/components/ds-mobile-section-switcher.js +1 -1
  101. package/dist/components/ds-mobile-sheet-nav.js +1 -1
  102. package/dist/components/ds-modal.js +1 -1
  103. package/dist/components/ds-pagination.js +1 -1
  104. package/dist/components/ds-panel-nav.js +1 -1
  105. package/dist/components/ds-panel-sub-nav.js +1 -1
  106. package/dist/components/ds-panel-sub-nav.js.map +1 -1
  107. package/dist/components/ds-panel-tool-header.js +1 -1
  108. package/dist/components/ds-panel-tool-search.js +1 -1
  109. package/dist/components/ds-panel-tool-search.js.map +1 -1
  110. package/dist/components/ds-panel-tools.js +1 -1
  111. package/dist/components/ds-paper-texture.js +1 -1
  112. package/dist/components/ds-radio-tile.js +1 -1
  113. package/dist/components/ds-radio-tile.js.map +1 -1
  114. package/dist/components/ds-radio.js +1 -1
  115. package/dist/components/ds-score.js +1 -1
  116. package/dist/components/ds-scroll-overlay.js +1 -1
  117. package/dist/components/ds-scroll-overlay.js.map +1 -1
  118. package/dist/components/ds-select.js +1 -1
  119. package/dist/components/ds-setting-row-checkbox.d.ts +11 -0
  120. package/dist/components/ds-setting-row-checkbox.js +2 -0
  121. package/dist/components/ds-setting-row-checkbox.js.map +1 -0
  122. package/dist/components/ds-setting-row-radio.d.ts +11 -0
  123. package/dist/components/ds-setting-row-radio.js +2 -0
  124. package/dist/components/ds-setting-row-radio.js.map +1 -0
  125. package/dist/components/ds-setting-row-toggle.d.ts +11 -0
  126. package/dist/components/ds-setting-row-toggle.js +2 -0
  127. package/dist/components/ds-setting-row-toggle.js.map +1 -0
  128. package/dist/components/ds-shell-app.js +1 -1
  129. package/dist/components/ds-shell-app.js.map +1 -1
  130. package/dist/components/ds-shell-page.js +1 -1
  131. package/dist/components/ds-shell-tools.js +1 -1
  132. package/dist/components/ds-skeleton.js +1 -1
  133. package/dist/components/ds-slider.js +1 -1
  134. package/dist/components/ds-swatch-picker.js +1 -1
  135. package/dist/components/ds-switch.js +1 -1
  136. package/dist/components/ds-tab-group.js +1 -1
  137. package/dist/components/ds-table.js +1 -1
  138. package/dist/components/ds-table.js.map +1 -1
  139. package/dist/components/ds-tag.js +1 -1
  140. package/dist/components/ds-text.js +1 -1
  141. package/dist/components/ds-textarea.js +1 -1
  142. package/dist/components/ds-textarea.js.map +1 -1
  143. package/dist/components/ds-time-picker.js +1 -1
  144. package/dist/components/ds-toast.js +1 -1
  145. package/dist/components/ds-tooltip-chart.js +1 -1
  146. package/dist/components/ds-tooltip.js +1 -1
  147. package/dist/components/ds-typing-indicator.js +1 -1
  148. package/dist/components/p-7TVdPhGR.js +2 -0
  149. package/dist/components/{p-DHapI2sf.js.map → p-7TVdPhGR.js.map} +1 -1
  150. package/dist/components/{p-7V1AhetH.js → p-B0BQCVkq.js} +2 -2
  151. package/dist/components/{p-Co5MiPNa.js → p-B0EpvBDs.js} +2 -2
  152. package/dist/components/{p-DFkw6wrS.js → p-B39iJO8F.js} +2 -2
  153. package/dist/components/p-B9TM6FqC.js +2 -0
  154. package/dist/components/{p-p_MwknM3.js.map → p-B9TM6FqC.js.map} +1 -1
  155. package/dist/components/{p-AmT4GlhM.js → p-B9y6azEw.js} +2 -2
  156. package/dist/components/p-BDdlJZyL.js +2 -0
  157. package/dist/components/{p-cuZva5tl.js.map → p-BDdlJZyL.js.map} +1 -1
  158. package/dist/components/{p-CrL9wxZg.js → p-BERXvOCf.js} +2 -2
  159. package/dist/components/{p-AM_ht6Im.js → p-BFHpU-uU.js} +2 -2
  160. package/dist/components/{p-B-HOVYoH.js → p-BQspV5U7.js} +2 -2
  161. package/dist/components/p-BWSIw8AQ.js +2 -0
  162. package/dist/components/p-BWSIw8AQ.js.map +1 -0
  163. package/dist/components/p-BhjqYTiY.js +2 -0
  164. package/dist/components/p-BhjqYTiY.js.map +1 -0
  165. package/dist/components/{p-C_gJD8l3.js → p-Bk3poU91.js} +2 -2
  166. package/dist/components/{p-Gb-vTuXr.js → p-BnuNaXR2.js} +2 -2
  167. package/dist/components/p-BwdnVqAY.js +2 -0
  168. package/dist/components/{p-BHI4LcGK.js.map → p-BwdnVqAY.js.map} +1 -1
  169. package/dist/components/p-C4huuwed.js +2 -0
  170. package/dist/components/{p-tJaC01aE.js.map → p-C4huuwed.js.map} +1 -1
  171. package/dist/components/{p-BsM37Prt.js → p-C8V4lkVy.js} +2 -2
  172. package/dist/components/p-CJ1NnOEW.js +2 -0
  173. package/dist/components/p-CJ1NnOEW.js.map +1 -0
  174. package/dist/components/p-CPamUzaj.js +2 -0
  175. package/dist/components/{p-BSuMDPaV.js.map → p-CPamUzaj.js.map} +1 -1
  176. package/dist/components/{p-BU1B6NW8.js → p-CUDocY23.js} +2 -2
  177. package/dist/components/{p-BU1B6NW8.js.map → p-CUDocY23.js.map} +1 -1
  178. package/dist/components/p-CX6ZxUQT.js +2 -0
  179. package/dist/components/p-CX6ZxUQT.js.map +1 -0
  180. package/dist/components/{p-C-c8SJ00.js → p-CZNZTcYS.js} +2 -2
  181. package/dist/components/{p-DQPzqcYR.js → p-CbCN9ew9.js} +2 -2
  182. package/dist/components/p-CeXf9-X3.js.map +1 -1
  183. package/dist/components/p-Cgn-BORA.js +2 -0
  184. package/dist/components/p-Cgn-BORA.js.map +1 -0
  185. package/dist/components/{p-CzQ_JF6A.js → p-Co1rUdOR.js} +2 -2
  186. package/dist/components/p-Co1rUdOR.js.map +1 -0
  187. package/dist/components/{p-D0KD-Nr_.js → p-CwdRaJER.js} +2 -2
  188. package/dist/components/p-D6FRqNhN.js +2 -0
  189. package/dist/components/p-D6FRqNhN.js.map +1 -0
  190. package/dist/components/{p-Cihjy4WY.js → p-D8OWhnID.js} +2 -2
  191. package/dist/components/p-DCyJ3_PL.js +2 -0
  192. package/dist/components/{p-fYZK3zqM.js.map → p-DCyJ3_PL.js.map} +1 -1
  193. package/dist/components/{p-BXSHN1OK.js → p-DIQiHX0q.js} +2 -2
  194. package/dist/components/{p-z8L-YMi3.js → p-DJDbskZf.js} +2 -2
  195. package/dist/components/p-DRdoGSGX.js +2 -0
  196. package/dist/components/{p-DWmYCOKz.js.map → p-DRdoGSGX.js.map} +1 -1
  197. package/dist/components/{p-Bm3KWMe4.js → p-DSZPO06T.js} +2 -2
  198. package/dist/components/{p-C3-dcROG.js → p-DSmDyI2o.js} +2 -2
  199. package/dist/components/{p-DcrdyDRw.js → p-DVEx-Xg9.js} +2 -2
  200. package/dist/components/{p-Bu4OmhKK.js → p-DXk1eml1.js} +2 -2
  201. package/dist/components/{p-B7QEpaMe.js → p-DYQa1DVF.js} +2 -2
  202. package/dist/components/p-DYQa1DVF.js.map +1 -0
  203. package/dist/components/p-DYsE6HLk.js +2 -0
  204. package/dist/components/p-DYsE6HLk.js.map +1 -0
  205. package/dist/components/p-DebLYW7A.js +2 -0
  206. package/dist/components/p-DebLYW7A.js.map +1 -0
  207. package/dist/components/{p-Bh1Fx0aW.js → p-DmSnHVW6.js} +2 -2
  208. package/dist/components/p-Dnt5xUBD.js +2 -0
  209. package/dist/components/{p-C4VeD49U.js.map → p-Dnt5xUBD.js.map} +1 -1
  210. package/dist/components/p-Doa2blZT.js +2 -0
  211. package/dist/components/{p-CztB8Pbq.js.map → p-Doa2blZT.js.map} +1 -1
  212. package/dist/components/{p-CCpJT31M.js → p-Dyqd-cub.js} +2 -2
  213. package/dist/components/{p-CCpJT31M.js.map → p-Dyqd-cub.js.map} +1 -1
  214. package/dist/components/p-DzvTC27M.js +2 -0
  215. package/dist/components/p-DzvTC27M.js.map +1 -0
  216. package/dist/components/p-JBki_jyY.js +2 -0
  217. package/dist/components/{p-BugKaVWc.js.map → p-JBki_jyY.js.map} +1 -1
  218. package/dist/components/p-MOJmB6vX.js +2 -0
  219. package/dist/components/{p-iGNHXFZA.js.map → p-MOJmB6vX.js.map} +1 -1
  220. package/dist/components/{p-BYJtvRQm.js → p-UJMEz8_O.js} +2 -2
  221. package/dist/components/{p-Bo7GxFBh.js → p-_IqSPt5V.js} +2 -2
  222. package/dist/components/p-g0jsfEXz.js +2 -0
  223. package/dist/components/{p-Fkkd3gMB.js.map → p-g0jsfEXz.js.map} +1 -1
  224. package/dist/components/{p-lADW-TI8.js → p-pp1IQN4Z.js} +2 -2
  225. package/dist/docs/components.json +701 -13
  226. package/dist/lib/shell/index.js +1 -1
  227. package/dist/lib/shell/index.js.map +1 -1
  228. package/dist/lint/index.js +1 -1
  229. package/dist/mcp-data/registry/agent-activity.json +1 -1
  230. package/dist/mcp-data/registry/agent-questionnaire.json +1 -1
  231. package/dist/mcp-data/registry/agent-response.json +2 -2
  232. package/dist/mcp-data/registry/agent-source-list.json +1 -1
  233. package/dist/mcp-data/registry/agent-tool-call.json +1 -1
  234. package/dist/mcp-data/registry/attachment-list.json +1 -1
  235. package/dist/mcp-data/registry/avatar.json +1 -1
  236. package/dist/mcp-data/registry/badge.json +1 -1
  237. package/dist/mcp-data/registry/banner.json +1 -1
  238. package/dist/mcp-data/registry/bar-action.json +1 -1
  239. package/dist/mcp-data/registry/bar-nav.json +4 -4
  240. package/dist/mcp-data/registry/bar-page-title.json +3 -3
  241. package/dist/mcp-data/registry/bar-title.json +3 -3
  242. package/dist/mcp-data/registry/bar-workflow.json +1 -1
  243. package/dist/mcp-data/registry/breadcrumb.json +1 -1
  244. package/dist/mcp-data/registry/button-filled.json +1 -1
  245. package/dist/mcp-data/registry/button-inverted.json +1 -1
  246. package/dist/mcp-data/registry/button-unfilled.json +8 -8
  247. package/dist/mcp-data/registry/calendar.json +1 -1
  248. package/dist/mcp-data/registry/card-action-center.json +1 -1
  249. package/dist/mcp-data/registry/card-chart.json +1 -1
  250. package/dist/mcp-data/registry/card-navigation.json +1 -1
  251. package/dist/mcp-data/registry/card-overview.json +1 -1
  252. package/dist/mcp-data/registry/card-setting.json +22 -8
  253. package/dist/mcp-data/registry/card-settings-scope.json +1 -1
  254. package/dist/mcp-data/registry/chart-legend.json +1 -1
  255. package/dist/mcp-data/registry/chart.json +1 -1
  256. package/dist/mcp-data/registry/checkbox-group.json +246 -0
  257. package/dist/mcp-data/registry/checkbox.json +25 -5
  258. package/dist/mcp-data/registry/chip.json +1 -1
  259. package/dist/mcp-data/registry/code-block.json +1 -1
  260. package/dist/mcp-data/registry/conversation-list-item.json +1 -1
  261. package/dist/mcp-data/registry/conversation-list-section.json +1 -1
  262. package/dist/mcp-data/registry/conversation-list.json +1 -1
  263. package/dist/mcp-data/registry/data-customize.json +1 -1
  264. package/dist/mcp-data/registry/data-filter.json +1 -1
  265. package/dist/mcp-data/registry/data-group.json +1 -1
  266. package/dist/mcp-data/registry/data-preferences.json +1 -1
  267. package/dist/mcp-data/registry/data-saved-views.json +1 -1
  268. package/dist/mcp-data/registry/data-search.json +1 -1
  269. package/dist/mcp-data/registry/data-sort.json +1 -1
  270. package/dist/mcp-data/registry/data-toolbar.json +1 -1
  271. package/dist/mcp-data/registry/divider.json +1 -1
  272. package/dist/mcp-data/registry/empty-state.json +1 -1
  273. package/dist/mcp-data/registry/field.json +1 -1
  274. package/dist/mcp-data/registry/filter-menu.json +1 -1
  275. package/dist/mcp-data/registry/icon.json +1 -1
  276. package/dist/mcp-data/registry/inline-banner-settings.json +175 -0
  277. package/dist/mcp-data/registry/input-date.json +1 -1
  278. package/dist/mcp-data/registry/input-time.json +1 -1
  279. package/dist/mcp-data/registry/input.json +1 -1
  280. package/dist/mcp-data/registry/loader.json +1 -1
  281. package/dist/mcp-data/registry/map-cluster-marker.json +1 -1
  282. package/dist/mcp-data/registry/map-entity-marker.json +1 -1
  283. package/dist/mcp-data/registry/markdown.json +1 -1
  284. package/dist/mcp-data/registry/menu.json +1 -1
  285. package/dist/mcp-data/registry/message-actions.json +1 -1
  286. package/dist/mcp-data/registry/message-bubble.json +4 -4
  287. package/dist/mcp-data/registry/message-composer.json +6 -6
  288. package/dist/mcp-data/registry/message-scroller.json +1 -1
  289. package/dist/mcp-data/registry/message.json +2 -2
  290. package/dist/mcp-data/registry/mobile-bar-nav.json +1 -1
  291. package/dist/mcp-data/registry/mobile-header.json +1 -1
  292. package/dist/mcp-data/registry/mobile-section-switcher.json +1 -1
  293. package/dist/mcp-data/registry/mobile-sheet-nav.json +1 -1
  294. package/dist/mcp-data/registry/modal.json +1 -1
  295. package/dist/mcp-data/registry/pagination.json +1 -1
  296. package/dist/mcp-data/registry/panel-nav.json +3 -3
  297. package/dist/mcp-data/registry/panel-sub-nav.json +1 -1
  298. package/dist/mcp-data/registry/panel-tool-header.json +3 -3
  299. package/dist/mcp-data/registry/panel-tool-search.json +2 -2
  300. package/dist/mcp-data/registry/panel-tools.json +3 -3
  301. package/dist/mcp-data/registry/paper-texture.json +1 -1
  302. package/dist/mcp-data/registry/radio-tile.json +5 -4
  303. package/dist/mcp-data/registry/radio.json +50 -8
  304. package/dist/mcp-data/registry/registry.json +518 -31
  305. package/dist/mcp-data/registry/score.json +1 -1
  306. package/dist/mcp-data/registry/scroll-overlay.json +1 -1
  307. package/dist/mcp-data/registry/select.json +1 -1
  308. package/dist/mcp-data/registry/setting-row-checkbox.json +197 -0
  309. package/dist/mcp-data/registry/setting-row-radio.json +196 -0
  310. package/dist/mcp-data/registry/{setting-row.json → setting-row-toggle.json} +45 -25
  311. package/dist/mcp-data/registry/shell-app.json +3 -3
  312. package/dist/mcp-data/registry/shell-page.json +1 -1
  313. package/dist/mcp-data/registry/shell-tools.json +1 -1
  314. package/dist/mcp-data/registry/skeleton.json +1 -1
  315. package/dist/mcp-data/registry/slider.json +1 -1
  316. package/dist/mcp-data/registry/swatch-picker.json +1 -1
  317. package/dist/mcp-data/registry/switch.json +3 -3
  318. package/dist/mcp-data/registry/tab-group.json +1 -1
  319. package/dist/mcp-data/registry/table.json +1 -1
  320. package/dist/mcp-data/registry/tag.json +1 -1
  321. package/dist/mcp-data/registry/text.json +1 -1
  322. package/dist/mcp-data/registry/textarea.json +1 -1
  323. package/dist/mcp-data/registry/time-picker.json +1 -1
  324. package/dist/mcp-data/registry/toast.json +1 -1
  325. package/dist/mcp-data/registry/tooltip-chart.json +1 -1
  326. package/dist/mcp-data/registry/tooltip.json +1 -1
  327. package/dist/mcp-data/registry/typing-indicator.json +1 -1
  328. package/dist/react/components.d.ts +5 -1
  329. package/dist/react/components.js +5 -1
  330. package/dist/react/ds-checkbox-group.d.ts +9 -0
  331. package/dist/react/ds-checkbox-group.js +12 -0
  332. package/dist/react/ds-inline-banner-settings.d.ts +9 -0
  333. package/dist/react/ds-inline-banner-settings.js +12 -0
  334. package/dist/react/ds-setting-row-checkbox.d.ts +9 -0
  335. package/dist/react/ds-setting-row-checkbox.js +12 -0
  336. package/dist/react/ds-setting-row-radio.d.ts +9 -0
  337. package/dist/react/ds-setting-row-radio.js +12 -0
  338. package/dist/react/ds-setting-row-toggle.d.ts +12 -0
  339. package/dist/react/ds-setting-row-toggle.js +12 -0
  340. package/dist/types/components/BarPageTitle/BarPageTitle.d.ts +2 -0
  341. package/dist/types/components/BarTitle/bar-title-action-chrome.d.ts +4 -0
  342. package/dist/types/components/ButtonUnfilled/ButtonUnfilled.d.ts +5 -2
  343. package/dist/types/components/CardSetting/CardSetting.d.ts +12 -2
  344. package/dist/types/components/Checkbox/Checkbox.d.ts +2 -0
  345. package/dist/types/components/CheckboxGroup/CheckboxGroup.d.ts +20 -0
  346. package/dist/types/components/InlineBannerSettings/InlineBannerSettings.d.ts +6 -0
  347. package/dist/types/components/Radio/Radio.d.ts +8 -0
  348. package/dist/types/components/SettingRowCheckbox/SettingRowCheckbox.d.ts +19 -0
  349. package/dist/types/components/SettingRowRadio/SettingRowRadio.d.ts +19 -0
  350. package/dist/types/components/{SettingRow/SettingRow.d.ts → SettingRowToggle/SettingRowToggle.d.ts} +5 -2
  351. package/dist/types/components/Switch/Switch.d.ts +7 -0
  352. package/dist/types/components.d.ts +290 -20
  353. package/dist/vue/components.d.ts +5 -1
  354. package/dist/vue/components.js +5 -1
  355. package/dist/vue/{ds-setting-row.d.ts → ds-checkbox-group.d.ts} +1 -1
  356. package/dist/vue/ds-checkbox-group.js +12 -0
  357. package/dist/vue/ds-checkbox.js +1 -0
  358. package/dist/vue/ds-inline-banner-settings.d.ts +3 -0
  359. package/dist/vue/ds-inline-banner-settings.js +8 -0
  360. package/dist/vue/ds-radio.js +2 -0
  361. package/dist/vue/ds-setting-row-checkbox.d.ts +3 -0
  362. package/dist/vue/ds-setting-row-checkbox.js +11 -0
  363. package/dist/vue/ds-setting-row-radio.d.ts +3 -0
  364. package/dist/vue/ds-setting-row-radio.js +11 -0
  365. package/dist/vue/ds-setting-row-toggle.d.ts +3 -0
  366. package/dist/vue/ds-setting-row-toggle.js +15 -0
  367. package/package.json +3 -3
  368. package/dist/angular/ds-setting-row.d.ts +0 -18
  369. package/dist/components/ds-setting-row.js +0 -2
  370. package/dist/components/ds-setting-row.js.map +0 -1
  371. package/dist/components/p-3bsypMrY.js +0 -2
  372. package/dist/components/p-3bsypMrY.js.map +0 -1
  373. package/dist/components/p-B7QEpaMe.js.map +0 -1
  374. package/dist/components/p-B7bXN-vf.js +0 -2
  375. package/dist/components/p-B7bXN-vf.js.map +0 -1
  376. package/dist/components/p-BHI4LcGK.js +0 -2
  377. package/dist/components/p-BSuMDPaV.js +0 -2
  378. package/dist/components/p-BVwg5XVz.js +0 -2
  379. package/dist/components/p-BVwg5XVz.js.map +0 -1
  380. package/dist/components/p-BugKaVWc.js +0 -2
  381. package/dist/components/p-C4VeD49U.js +0 -2
  382. package/dist/components/p-CI7c9kp-.js +0 -2
  383. package/dist/components/p-CI7c9kp-.js.map +0 -1
  384. package/dist/components/p-CJD0kLeV.js +0 -2
  385. package/dist/components/p-CJD0kLeV.js.map +0 -1
  386. package/dist/components/p-Cc3ZUQlJ.js +0 -2
  387. package/dist/components/p-Cc3ZUQlJ.js.map +0 -1
  388. package/dist/components/p-CzQ_JF6A.js.map +0 -1
  389. package/dist/components/p-CztB8Pbq.js +0 -2
  390. package/dist/components/p-DCLRAH5i.js +0 -2
  391. package/dist/components/p-DCLRAH5i.js.map +0 -1
  392. package/dist/components/p-DHapI2sf.js +0 -2
  393. package/dist/components/p-DWmYCOKz.js +0 -2
  394. package/dist/components/p-DX2RyZba.js +0 -2
  395. package/dist/components/p-DX2RyZba.js.map +0 -1
  396. package/dist/components/p-Dcn1eDVt.js +0 -2
  397. package/dist/components/p-Dcn1eDVt.js.map +0 -1
  398. package/dist/components/p-Fkkd3gMB.js +0 -2
  399. package/dist/components/p-cuZva5tl.js +0 -2
  400. package/dist/components/p-fYZK3zqM.js +0 -2
  401. package/dist/components/p-iGNHXFZA.js +0 -2
  402. package/dist/components/p-p_MwknM3.js +0 -2
  403. package/dist/components/p-tJaC01aE.js +0 -2
  404. package/dist/react/ds-setting-row.d.ts +0 -12
  405. package/dist/react/ds-setting-row.js +0 -12
  406. package/dist/vue/ds-setting-row.js +0 -14
  407. /package/dist/components/{p-7V1AhetH.js.map → p-B0BQCVkq.js.map} +0 -0
  408. /package/dist/components/{p-Co5MiPNa.js.map → p-B0EpvBDs.js.map} +0 -0
  409. /package/dist/components/{p-DFkw6wrS.js.map → p-B39iJO8F.js.map} +0 -0
  410. /package/dist/components/{p-AmT4GlhM.js.map → p-B9y6azEw.js.map} +0 -0
  411. /package/dist/components/{p-CrL9wxZg.js.map → p-BERXvOCf.js.map} +0 -0
  412. /package/dist/components/{p-AM_ht6Im.js.map → p-BFHpU-uU.js.map} +0 -0
  413. /package/dist/components/{p-B-HOVYoH.js.map → p-BQspV5U7.js.map} +0 -0
  414. /package/dist/components/{p-C_gJD8l3.js.map → p-Bk3poU91.js.map} +0 -0
  415. /package/dist/components/{p-Gb-vTuXr.js.map → p-BnuNaXR2.js.map} +0 -0
  416. /package/dist/components/{p-BsM37Prt.js.map → p-C8V4lkVy.js.map} +0 -0
  417. /package/dist/components/{p-C-c8SJ00.js.map → p-CZNZTcYS.js.map} +0 -0
  418. /package/dist/components/{p-DQPzqcYR.js.map → p-CbCN9ew9.js.map} +0 -0
  419. /package/dist/components/{p-D0KD-Nr_.js.map → p-CwdRaJER.js.map} +0 -0
  420. /package/dist/components/{p-Cihjy4WY.js.map → p-D8OWhnID.js.map} +0 -0
  421. /package/dist/components/{p-BXSHN1OK.js.map → p-DIQiHX0q.js.map} +0 -0
  422. /package/dist/components/{p-z8L-YMi3.js.map → p-DJDbskZf.js.map} +0 -0
  423. /package/dist/components/{p-Bm3KWMe4.js.map → p-DSZPO06T.js.map} +0 -0
  424. /package/dist/components/{p-C3-dcROG.js.map → p-DSmDyI2o.js.map} +0 -0
  425. /package/dist/components/{p-DcrdyDRw.js.map → p-DVEx-Xg9.js.map} +0 -0
  426. /package/dist/components/{p-Bu4OmhKK.js.map → p-DXk1eml1.js.map} +0 -0
  427. /package/dist/components/{p-Bh1Fx0aW.js.map → p-DmSnHVW6.js.map} +0 -0
  428. /package/dist/components/{p-BYJtvRQm.js.map → p-UJMEz8_O.js.map} +0 -0
  429. /package/dist/components/{p-Bo7GxFBh.js.map → p-_IqSPt5V.js.map} +0 -0
  430. /package/dist/components/{p-lADW-TI8.js.map → p-pp1IQN4Z.js.map} +0 -0
@@ -518,7 +518,7 @@
518
518
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: accessories, headers, items.",
519
519
  "peerDependencies": {
520
520
  "required": [
521
- "@ds-mo/tokens ^6.5.2",
521
+ "@ds-mo/tokens ^6.6.1",
522
522
  "@ds-mo/icons ^8.1.1"
523
523
  ],
524
524
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -542,12 +542,12 @@
542
542
  "files": [
543
543
  {
544
544
  "path": "src/wc/components/PanelTools/PanelTools.css",
545
- "content": "@import '../../utils/radius.css';\n@import '../../utils/control-density.css';\n@import '../../utils/chrome-layout.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n@import '../../utils/scrollbar-hidden.css';\n\n:host {\n display: block;\n flex: 0 0 auto;\n box-sizing: border-box;\n height: 100%;\n min-height: 0;\n overflow: hidden;\n --_panel-tools-rail-width: var(--dimension-size-600);\n --_panel-tools-drawer-width: var(--ds-shell-panel-tools-width, var(--dimension-panel-width-xs));\n --_panel-tools-border: var(--dimension-stroke-width-012, 1px) solid var(--color-border-tertiary);\n /* Match panel-nav expand timing (medium-1); easing differs by direction. */\n --_panel-tools-expand-dur: var(--effect-animation-duration-medium-1, 300ms);\n}\n\n.panel-tools__layout {\n display: flex;\n flex-direction: row-reverse;\n align-items: stretch;\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n}\n\n/* Clip frame — only max-width animates (slide reveal, same as pre-rail :host). */\n.panel-tools__drawer {\n flex: 0 0 auto;\n max-width: 0;\n min-width: 0;\n height: 100%;\n min-height: 0;\n overflow: hidden;\n box-sizing: border-box;\n transition: none;\n}\n\n:host(.panel-tools--ready) .panel-tools__drawer {\n transition: max-width var(--_panel-tools-expand-dur)\n var(--effect-animation-easing-ease-in-out, ease);\n}\n\n:host(.panel-tools--ready.panel-tools--motion-opening) .panel-tools__drawer {\n transition: max-width var(--_panel-tools-expand-dur)\n var(--effect-animation-easing-ease-out, ease-out);\n}\n\n:host(.panel-tools--ready.panel-tools--motion-closing) .panel-tools__drawer {\n transition: max-width var(--_panel-tools-expand-dur)\n var(--effect-animation-easing-ease-in, ease-in);\n}\n\n:host(.panel-tools--open) .panel-tools__drawer {\n max-width: var(--_panel-tools-drawer-width);\n}\n\n.panel-tools__drawer--visible {\n border-left: var(--_panel-tools-border);\n}\n\n/* Fixed-width surface — ShellApp supplies the standard 300px tools width. */\n.panel-tools__drawer-surface {\n display: flex;\n flex-direction: column;\n width: var(--_panel-tools-drawer-width);\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n background-color: transparent;\n}\n\n.panel-tools__full-views,\n.panel-tools__full-view {\n flex: 1 1 auto;\n min-height: 0;\n overflow: hidden;\n}\n\n.panel-tools__full-view {\n height: 100%;\n}\n\n.panel-tools__full-view {\n display: none;\n}\n.panel-tools__full-view--active {\n display: block;\n}\n\n:host([presentation='fullscreen']) {\n height: 100%;\n inset: 0;\n position: fixed;\n width: 100%;\n z-index: var(--dimension-z-index-modal);\n}\n\n:host([presentation='fullscreen']) .panel-tools__rail {\n display: none;\n}\n:host([presentation='fullscreen']) .panel-tools__drawer {\n max-width: 100%;\n transition: none;\n width: 100%;\n}\n:host([presentation='fullscreen']) .panel-tools__drawer--visible {\n border-left: none;\n}\n:host([presentation='fullscreen']) .panel-tools__drawer-surface {\n background-color: transparent;\n width: 100%;\n}\n\n/*\n * A framework can update the reflected presentation attribute before Stencil\n * commits the matching render class. Conceal that single inconsistent frame so\n * slotted master/detail content never paints against the previous geometry.\n */\n:host([presentation='fullscreen']:not(.panel-tools--fullscreen)) .panel-tools__drawer-surface,\n:host(.panel-tools--fullscreen:not([presentation='fullscreen'])) .panel-tools__drawer-surface {\n visibility: hidden;\n}\n\n:host(.panel-tools--presentation-snap) .panel-tools__drawer {\n transition: none;\n}\n\n.panel-tools__body {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n position: relative;\n}\n\n/* Skip paint while fully closed; cleared during open/close motion for WYSIWYG slide. */\n:host(.panel-tools--drawer-resting) .panel-tools__body {\n content-visibility: auto;\n contain-intrinsic-size: auto 200px;\n}\n\n.panel-tools__view {\n display: none;\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n padding: var(--dimension-space-200);\n color: var(--color-foreground-secondary);\n font-family: var(--typography-font-family-ui);\n font-size: var(--typography-fontsize-md);\n line-height: var(--typography-lineheight-md);\n font-weight: var(--typography-weight-regular);\n letter-spacing: var(--typography-letterspacing-negative-half);\n}\n\n.panel-tools__view ::slotted(*) {\n margin: 0;\n color: var(--color-foreground-secondary);\n font-family: var(--typography-font-family-ui);\n font-size: var(--typography-fontsize-md);\n line-height: var(--typography-lineheight-md);\n font-weight: var(--typography-weight-regular);\n letter-spacing: var(--typography-letterspacing-negative-half);\n}\n\n.panel-tools__view--active {\n display: block;\n}\n\n/* Rail — mirrors collapsed panel-nav width, padding, and item rhythm. */\n.panel-tools__rail {\n flex: 0 0 var(--_panel-tools-rail-width);\n width: var(--_panel-tools-rail-width);\n min-width: var(--_panel-tools-rail-width);\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n height: 100%;\n min-height: 0;\n border-left: var(--_panel-tools-border);\n background-color: transparent;\n}\n\n.panel-tools__rail-header {\n justify-content: center;\n height: var(--dimension-size-600);\n flex-shrink: 0;\n border-bottom: var(--_panel-tools-border);\n}\n\n.panel-tools__rail-header.panel-tools__rail-section--stacked {\n height: auto;\n min-height: var(--dimension-size-600);\n}\n\n.panel-tools__rail-body {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n overflow-x: hidden;\n}\n\n.panel-tools__rail-actions {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050);\n width: 100%;\n}\n\n/* Flush to the bottom of the rail — mirrors Search header pin. */\n.panel-tools__rail-footer {\n justify-content: center;\n flex-shrink: 0;\n margin-top: auto;\n}\n\n.panel-tools__rail-section--stacked {\n align-items: stretch;\n}\n\n.panel-tools__rail-action {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: var(--ds-control-height);\n flex-shrink: 0;\n}\n\n.panel-tools__accessory {\n display: flex;\n flex-direction: column;\n flex: 0 0 calc(var(--dimension-size-400) * 2);\n box-sizing: border-box;\n width: 100%;\n min-height: calc(var(--dimension-size-400) * 2);\n}\n\n.panel-tools__accessory--shortcut {\n flex-basis: var(--dimension-size-400);\n min-width: var(--dimension-size-400);\n min-height: var(--dimension-size-400);\n}\n\n.panel-tools__accessory--shortcut > ds-tooltip {\n display: flex;\n flex: 1 1 auto;\n width: 100%;\n min-height: 0;\n}\n\n.panel-tools__accessory--transient {\n align-items: center;\n gap: var(--dimension-space-100);\n width: var(--dimension-size-400);\n height: calc(var(--dimension-size-400) * 2);\n min-height: calc(var(--dimension-size-400) * 2);\n padding: var(--dimension-space-050);\n border-radius: var(--dimension-radius-half);\n}\n\n.panel-tools__accessory--transient-active {\n background-color: var(--color-background-bold-brand);\n}\n\n.panel-tools__accessory--transient-positive {\n background-color: var(--color-background-bold-positive);\n}\n\n.panel-tools__accessory--transient > ds-tooltip {\n display: flex;\n flex: 0 0 var(--dimension-size-300);\n width: var(--dimension-size-300);\n height: var(--dimension-size-300);\n min-height: var(--dimension-size-300);\n}\n\n.panel-tools__shortcut {\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: 100%;\n height: 100%;\n margin: 0;\n padding: 0;\n border: 0;\n border-radius: var(--dimension-radius-half);\n color: var(--color-foreground-secondary);\n background: transparent;\n font: inherit;\n}\n\n.panel-tools__shortcut:disabled {\n color: var(--color-foreground-disabled);\n cursor: not-allowed;\n}\n\n.panel-tools__shortcut-content {\n display: flex;\n align-items: center;\n justify-content: center;\n width: var(--dimension-size-400);\n height: var(--dimension-size-400);\n}\n\n.panel-tools__shortcut-orb {\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: var(--dimension-size-300);\n height: var(--dimension-size-300);\n border: var(--dimension-stroke-width-012) solid var(--color-border-secondary);\n border-radius: var(--dimension-radius-half);\n background: transparent;\n}\n\n.panel-tools__shortcut-dot {\n position: absolute;\n inset-block-start: calc((var(--dimension-size-400) - var(--dimension-iconography-md)) / 2);\n inset-inline-end: calc((var(--dimension-size-400) - var(--dimension-iconography-md)) / 2);\n z-index: 2;\n pointer-events: none;\n}\n\n.panel-tools__accessory-primary {\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: var(--dimension-size-300);\n height: var(--dimension-size-300);\n margin: 0;\n padding: 0;\n border: 0;\n border-radius: var(--dimension-radius-half);\n color: var(--color-foreground-on-bold-background-secondary);\n background: transparent;\n font: inherit;\n --ds-interaction-border-width: var(--dimension-stroke-width-012);\n --ds-interaction-border-color: var(--color-border-on-bold-background-secondary);\n}\n\n.panel-tools__accessory-primary:disabled {\n color: var(--color-foreground-on-bold-background-quaternary);\n cursor: not-allowed;\n}\n\n.panel-tools__accessory-primary-content {\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n width: 100%;\n height: 100%;\n}\n\n.panel-tools__accessory-image,\n.panel-tools__accessory-initial {\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: 100%;\n height: 100%;\n border-radius: var(--dimension-radius-half);\n}\n\n.panel-tools__accessory-image {\n object-fit: cover;\n}\n\n.panel-tools__accessory-initial {\n color: inherit;\n background: transparent;\n}\n\n.panel-tools__accessory-secondary {\n display: inline-flex;\n flex: 0 0 var(--dimension-size-300);\n width: var(--dimension-size-300);\n height: var(--dimension-size-300);\n}\n\n.panel-tools__accessory-divider {\n flex: 0 0 auto;\n box-sizing: border-box;\n width: 100%;\n padding-block: var(--dimension-space-050);\n}\n\n.panel-tools__accessory-divider ds-divider {\n display: block;\n}\n\n@media (prefers-reduced-motion: reduce) {\n :host(.panel-tools--ready.panel-tools--motion-opening) .panel-tools__drawer,\n :host(.panel-tools--ready.panel-tools--motion-closing) .panel-tools__drawer {\n transition: none;\n }\n}\n",
545
+ "content": "@import '../../utils/radius.css';\n@import '../../utils/control-density.css';\n@import '../../utils/chrome-layout.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n@import '../../utils/scrollbar-hidden.css';\n\n:host {\n display: block;\n flex: 0 0 auto;\n box-sizing: border-box;\n height: 100%;\n min-height: 0;\n overflow: hidden;\n --_panel-tools-rail-width: var(--dimension-size-600);\n --_panel-tools-drawer-width: var(--ds-shell-panel-tools-width, var(--dimension-panel-width-xs));\n --_panel-tools-border: var(--dimension-stroke-width-012, 1px) solid\n var(--_shell-tools-border, var(--color-border-tertiary));\n --_panel-tools-divider-color: var(--_shell-tools-divider, var(--color-divider-divider));\n --_panel-tools-divider: var(--dimension-stroke-width-012, 1px) solid\n var(--_panel-tools-divider-color);\n --_panel-tools-fg-secondary: var(--_shell-tools-fg-secondary, var(--color-foreground-secondary));\n --_panel-tools-shortcut-fg-secondary: var(\n --_shell-tools-fg-secondary,\n var(--color-chrome-foreground-secondary)\n );\n --_panel-tools-shortcut-border: var(\n --_shell-tools-border-secondary,\n var(--color-chrome-border-secondary)\n );\n --_panel-tools-shortcut-dot-ring: var(\n --_shell-tools-background-theme,\n var(--color-chrome-background-theme)\n );\n /* Match panel-nav expand timing (medium-1); easing differs by direction. */\n --_panel-tools-expand-dur: var(--effect-animation-duration-medium-1, 300ms);\n}\n\n.panel-tools__layout {\n display: flex;\n flex-direction: row-reverse;\n align-items: stretch;\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n}\n\n/* Clip frame — only max-width animates (slide reveal, same as pre-rail :host). */\n.panel-tools__drawer {\n flex: 0 0 auto;\n max-width: 0;\n min-width: 0;\n height: 100%;\n min-height: 0;\n overflow: hidden;\n box-sizing: border-box;\n transition: none;\n}\n\n:host(.panel-tools--ready) .panel-tools__drawer {\n transition: max-width var(--_panel-tools-expand-dur)\n var(--effect-animation-easing-ease-in-out, ease);\n}\n\n:host(.panel-tools--ready.panel-tools--motion-opening) .panel-tools__drawer {\n transition: max-width var(--_panel-tools-expand-dur)\n var(--effect-animation-easing-ease-out, ease-out);\n}\n\n:host(.panel-tools--ready.panel-tools--motion-closing) .panel-tools__drawer {\n transition: max-width var(--_panel-tools-expand-dur)\n var(--effect-animation-easing-ease-in, ease-in);\n}\n\n:host(.panel-tools--open) .panel-tools__drawer {\n max-width: var(--_panel-tools-drawer-width);\n}\n\n.panel-tools__drawer--visible {\n border-left: var(--_panel-tools-border);\n}\n\n/* Fixed-width surface — ShellApp supplies the standard 300px tools width. */\n.panel-tools__drawer-surface {\n display: flex;\n flex-direction: column;\n width: var(--_panel-tools-drawer-width);\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n background-color: transparent;\n}\n\n.panel-tools__full-views,\n.panel-tools__full-view {\n flex: 1 1 auto;\n min-height: 0;\n overflow: hidden;\n}\n\n.panel-tools__full-view {\n height: 100%;\n}\n\n.panel-tools__full-view {\n display: none;\n}\n.panel-tools__full-view--active {\n display: block;\n}\n\n:host([presentation='fullscreen']) {\n height: 100%;\n inset: 0;\n position: fixed;\n width: 100%;\n z-index: var(--dimension-z-index-modal);\n}\n\n:host([presentation='fullscreen']) .panel-tools__rail {\n display: none;\n}\n:host([presentation='fullscreen']) .panel-tools__drawer {\n max-width: 100%;\n transition: none;\n width: 100%;\n}\n:host([presentation='fullscreen']) .panel-tools__drawer--visible {\n border-left: none;\n}\n:host([presentation='fullscreen']) .panel-tools__drawer-surface {\n background-color: transparent;\n width: 100%;\n}\n\n/*\n * A framework can update the reflected presentation attribute before Stencil\n * commits the matching render class. Conceal that single inconsistent frame so\n * slotted master/detail content never paints against the previous geometry.\n */\n:host([presentation='fullscreen']:not(.panel-tools--fullscreen)) .panel-tools__drawer-surface,\n:host(.panel-tools--fullscreen:not([presentation='fullscreen'])) .panel-tools__drawer-surface {\n visibility: hidden;\n}\n\n:host(.panel-tools--presentation-snap) .panel-tools__drawer {\n transition: none;\n}\n\n.panel-tools__body {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n position: relative;\n}\n\n/* Skip paint while fully closed; cleared during open/close motion for WYSIWYG slide. */\n:host(.panel-tools--drawer-resting) .panel-tools__body {\n content-visibility: auto;\n contain-intrinsic-size: auto 200px;\n}\n\n.panel-tools__view {\n display: none;\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n padding: var(--dimension-space-200);\n color: var(--_panel-tools-fg-secondary);\n font-family: var(--typography-font-family-ui);\n font-size: var(--typography-fontsize-md);\n line-height: var(--typography-lineheight-md);\n font-weight: var(--typography-weight-regular);\n letter-spacing: var(--typography-letterspacing-negative-half);\n}\n\n.panel-tools__view ::slotted(*) {\n margin: 0;\n color: var(--_panel-tools-fg-secondary);\n font-family: var(--typography-font-family-ui);\n font-size: var(--typography-fontsize-md);\n line-height: var(--typography-lineheight-md);\n font-weight: var(--typography-weight-regular);\n letter-spacing: var(--typography-letterspacing-negative-half);\n}\n\n.panel-tools__view--active {\n display: block;\n}\n\n/* Rail — mirrors collapsed panel-nav width, padding, and item rhythm. */\n.panel-tools__rail {\n flex: 0 0 var(--_panel-tools-rail-width);\n width: var(--_panel-tools-rail-width);\n min-width: var(--_panel-tools-rail-width);\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n height: 100%;\n min-height: 0;\n border-left: var(--_panel-tools-border);\n background-color: transparent;\n}\n\n.panel-tools__rail-header {\n justify-content: center;\n height: var(--dimension-size-600);\n flex-shrink: 0;\n border-bottom: var(--_panel-tools-divider);\n}\n\n.panel-tools__rail-header.panel-tools__rail-section--stacked {\n height: auto;\n min-height: var(--dimension-size-600);\n}\n\n.panel-tools__rail-body {\n flex: 1 1 auto;\n min-height: 0;\n overflow-y: auto;\n overflow-x: hidden;\n}\n\n.panel-tools__rail-actions {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050);\n width: 100%;\n}\n\n/* Flush to the bottom of the rail — mirrors Search header pin. */\n.panel-tools__rail-footer {\n justify-content: center;\n flex-shrink: 0;\n margin-top: auto;\n}\n\n.panel-tools__rail-section--stacked {\n align-items: stretch;\n}\n\n.panel-tools__rail-action {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: var(--ds-control-height);\n flex-shrink: 0;\n}\n\n.panel-tools__accessory {\n display: flex;\n flex-direction: column;\n flex: 0 0 calc(var(--dimension-size-400) * 2);\n box-sizing: border-box;\n width: 100%;\n min-height: calc(var(--dimension-size-400) * 2);\n}\n\n.panel-tools__accessory--shortcut {\n flex-basis: var(--dimension-size-400);\n min-width: var(--dimension-size-400);\n min-height: var(--dimension-size-400);\n}\n\n.panel-tools__accessory--shortcut > ds-tooltip {\n display: flex;\n flex: 1 1 auto;\n width: 100%;\n min-height: 0;\n}\n\n.panel-tools__accessory--transient {\n align-items: center;\n gap: var(--dimension-space-100);\n width: var(--dimension-size-400);\n height: calc(var(--dimension-size-400) * 2);\n min-height: calc(var(--dimension-size-400) * 2);\n padding: var(--dimension-space-050);\n border-radius: var(--dimension-radius-half);\n}\n\n.panel-tools__accessory--transient-active {\n background-color: var(--color-background-bold-brand);\n}\n\n.panel-tools__accessory--transient-positive {\n background-color: var(--color-background-bold-positive);\n}\n\n.panel-tools__accessory--transient > ds-tooltip {\n display: flex;\n flex: 0 0 var(--dimension-size-300);\n width: var(--dimension-size-300);\n height: var(--dimension-size-300);\n min-height: var(--dimension-size-300);\n}\n\n.panel-tools__shortcut {\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: 100%;\n height: 100%;\n margin: 0;\n padding: 0;\n border: 0;\n border-radius: var(--dimension-radius-half);\n color: var(--_panel-tools-shortcut-fg-secondary);\n background: transparent;\n font: inherit;\n}\n\n.panel-tools__shortcut:disabled {\n color: var(--color-foreground-disabled);\n cursor: not-allowed;\n}\n\n.panel-tools__shortcut-content {\n display: flex;\n align-items: center;\n justify-content: center;\n width: var(--dimension-size-400);\n height: var(--dimension-size-400);\n}\n\n.panel-tools__shortcut-orb {\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: var(--dimension-size-300);\n height: var(--dimension-size-300);\n border: var(--dimension-stroke-width-012) solid var(--_panel-tools-shortcut-border);\n border-radius: var(--dimension-radius-half);\n background: transparent;\n}\n\n.panel-tools__shortcut-dot {\n position: absolute;\n inset-block-start: calc((var(--dimension-size-400) - var(--dimension-iconography-md)) / 2);\n inset-inline-end: calc((var(--dimension-size-400) - var(--dimension-iconography-md)) / 2);\n z-index: 2;\n pointer-events: none;\n}\n\n.panel-tools__accessory-primary {\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: var(--dimension-size-300);\n height: var(--dimension-size-300);\n margin: 0;\n padding: 0;\n border: 0;\n border-radius: var(--dimension-radius-half);\n color: var(--color-foreground-on-bold-background-secondary);\n background: transparent;\n font: inherit;\n --ds-interaction-border-width: var(--dimension-stroke-width-012);\n --ds-interaction-border-color: var(--color-border-on-bold-background-secondary);\n}\n\n.panel-tools__accessory-primary:disabled {\n color: var(--color-foreground-on-bold-background-quaternary);\n cursor: not-allowed;\n}\n\n.panel-tools__accessory-primary-content {\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n width: 100%;\n height: 100%;\n}\n\n.panel-tools__accessory-image,\n.panel-tools__accessory-initial {\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: 100%;\n height: 100%;\n border-radius: var(--dimension-radius-half);\n}\n\n.panel-tools__accessory-image {\n object-fit: cover;\n}\n\n.panel-tools__accessory-initial {\n color: inherit;\n background: transparent;\n}\n\n.panel-tools__accessory-secondary {\n display: inline-flex;\n flex: 0 0 var(--dimension-size-300);\n width: var(--dimension-size-300);\n height: var(--dimension-size-300);\n}\n\n.panel-tools__accessory-divider {\n flex: 0 0 auto;\n box-sizing: border-box;\n width: 100%;\n padding-block: var(--dimension-space-050);\n}\n\n.panel-tools__accessory-divider ds-divider {\n display: block;\n --_divider-color: var(--_panel-tools-divider-color);\n}\n\n@media (prefers-reduced-motion: reduce) {\n :host(.panel-tools--ready.panel-tools--motion-opening) .panel-tools__drawer,\n :host(.panel-tools--ready.panel-tools--motion-closing) .panel-tools__drawer {\n transition: none;\n }\n}\n",
546
546
  "type": "registry:ui"
547
547
  },
548
548
  {
549
549
  "path": "src/wc/components/PanelTools/PanelTools.tsx",
550
- "content": "import {\n Component,\n Prop,\n Event,\n EventEmitter,\n Element,\n State,\n Watch,\n Method,\n Listen,\n h,\n Host,\n} from '@stencil/core';\nimport type { ChromeTransitionDetail } from '../../shell/chrome-transition';\nimport { ANCHORED_OVERLAY_BOUNDARY_ATTRIBUTE } from '../../utils/anchored-overlay-boundary';\nimport {\n PANEL_TOOLS_LABELS,\n PANEL_TOOLS_SHORTCUTS,\n type PanelToolsHeaderAction,\n type PanelToolsHeaderActionDetail,\n type PanelToolsHeaderConfig,\n type PanelToolsHeaders,\n type PanelToolsItem,\n type PanelToolsRailAccessory,\n type PanelToolsRailAccessoryAction,\n type PanelToolsRailAccessoryActionDetail,\n type PanelToolsRailShortcutAccessory,\n type PanelToolsRailTransientAccessory,\n type PanelToolsToolId,\n} from './panel-tools-types';\nimport {\n isPanelToolsToolId,\n orderPanelToolsRailEntries,\n orderPanelToolsItems,\n panelToolsRailAccessoryActionDetail,\n panelToolsRailFocusKeys,\n panelToolsDrawerAtTerminal,\n panelToolsDrawerResting,\n panelToolsDrawerTransitionMs,\n panelToolsRailPlacement,\n reconcilePanelToolsAvailability,\n reconcilePanelToolsRovingIndex,\n resolvePanelToolActivation,\n type PanelToolsRailEntry,\n} from './panel-tools-utils';\n\n@Component({\n tag: 'ds-panel-tools',\n styleUrl: 'PanelTools.css',\n shadow: true,\n})\nexport class PanelTools {\n @Element() el!: HTMLElement;\n\n /** When false, only the icon rail is shown. */\n @Prop({ mutable: true, reflect: true }) open: boolean = false;\n\n /** Drawer presentation or presentation covering the shell stage for the active tool. */\n @Prop({ mutable: true, reflect: true }) presentation: 'drawer' | 'fullscreen' = 'drawer';\n\n /** Let a fullscreen product supply independent master/detail headers inside its view. */\n @Prop({ attribute: 'fullscreen-header-mode', reflect: true })\n fullscreenHeaderMode: 'shared' | 'split' = 'shared';\n\n /** Active tool view — `search`, `agents`, `messages`, `stacks`, `activity`, or `help`. */\n @Prop({ mutable: true, attribute: 'active-tool', reflect: true })\n activeTool: PanelToolsToolId | '' = '';\n\n /**\n * Rail items rendered in the right column.\n * Set via JS property: `el.items = [...]`. Replace the array reference to update.\n */\n @Prop() items: PanelToolsItem[] = [];\n\n /**\n * Desktop/tablet-only application-owned rail boundaries and direct intents.\n * Set via JS property and replace the array reference to update.\n */\n @Prop() accessories: PanelToolsRailAccessory[] = [];\n\n @Prop() toolsLabel: string = 'Tools';\n @Prop() toolShortcutsLabel: string = 'Tool shortcuts';\n\n /** Active header state per tool. Replace the object when title, depth, or actions change. */\n @Prop() headers: PanelToolsHeaders = {};\n\n /**\n * Optional localStorage key for the last active tool. The drawer always starts\n * closed; only the tool identity is restored for continuity within this browser.\n */\n @Prop({ attribute: 'storage-key' }) storageKey: string = '';\n\n /** Emitted when a rail button is toggled. Detail = { id, selected }. */\n @Event({ bubbles: true, composed: true }) dsToolChange!: EventEmitter<{\n id: PanelToolsToolId;\n selected: boolean;\n }>;\n\n /** Emitted when fullscreen changes, including Escape-initiated exits. */\n @Event({ bubbles: true, composed: true }) dsPresentationChange!: EventEmitter<{\n presentation: 'drawer' | 'fullscreen';\n }>;\n\n /** Requests navigation to the active tool's parent view. */\n @Event({ bubbles: true, composed: true }) dsHeaderBack!: EventEmitter<{\n tool: PanelToolsToolId;\n }>;\n\n /** Requests one application-owned action from the active tool header. */\n @Event({ bubbles: true, composed: true })\n dsHeaderAction!: EventEmitter<PanelToolsHeaderActionDetail>;\n\n /** Direct accessory intent. Emitting this event never changes the active tool or drawer. */\n @Event({ bubbles: true, composed: true })\n dsRailAccessoryAction!: EventEmitter<PanelToolsRailAccessoryActionDetail>;\n\n /** Bubbling lifecycle — `ds-bar-nav` defers overflow checks during drawer motion. */\n @Event({ bubbles: true, composed: true })\n dsChromeTransitionStart!: EventEmitter<ChromeTransitionDetail>;\n\n @Event({ bubbles: true, composed: true })\n dsChromeTransitionEnd!: EventEmitter<ChromeTransitionDetail>;\n\n /** Arms open vs close easing for the in-flight width transition. */\n @State() private motion: 'opening' | 'closing' | 'idle' = 'idle';\n\n /** Keeps header and slotted content painted until the closing clip transition completes. */\n @State() private drawerSurfaceRetained = false;\n\n /** Suppresses width transition until the host has painted its initial open state. */\n @State() private readyForMotion = false;\n\n /** Fullscreen presentation changes snap without borrowing drawer width motion. */\n @State() private presentationMotionSuppressed = false;\n\n @State() private rovingIndex = 0;\n @State() private fullViewToolIds: PanelToolsToolId[] = [];\n @State() private railBodyScrollable = false;\n\n private motionEnableGeneration = 0;\n private presentationMotionGeneration = 0;\n private transitionGeneration = 0;\n private transitionFallbackTimer: number | null = null;\n /** Ignores the old transition's terminal event when reversing direction. */\n private ignoreReplacementTerminal = false;\n private focusBeforeFullscreen: HTMLElement | null = null;\n private fullViewObserver: MutationObserver | null = null;\n private railBodyEl?: HTMLElement;\n private railBodyObserver?: ResizeObserver;\n\n private get railItems(): PanelToolsItem[] {\n return this.items ?? [];\n }\n\n private get orderedRailItems(): PanelToolsItem[] {\n return orderPanelToolsItems(this.railItems);\n }\n\n private get railAccessories(): PanelToolsRailAccessory[] {\n return this.accessories ?? [];\n }\n\n private get orderedRailEntries(): PanelToolsRailEntry[] {\n return orderPanelToolsRailEntries(this.railItems, this.railAccessories);\n }\n\n disconnectedCallback() {\n this.el.removeEventListener('transitionend', this.handleTransitionEnd);\n this.el.removeEventListener('transitioncancel', this.handleTransitionEnd);\n this.el.removeEventListener('dsAction', this.handleComposedHeaderAction);\n this.clearTransitionCompletion();\n this.motionEnableGeneration += 1;\n this.fullViewObserver?.disconnect();\n this.fullViewObserver = null;\n this.disconnectRailBodyObserver();\n }\n\n @Watch('presentation')\n presentationChanged(next: 'drawer' | 'fullscreen', previous?: 'drawer' | 'fullscreen') {\n if (next === previous) return;\n // Framework property bindings can update slotted master/detail content in\n // the same render that requests fullscreen. Reflect the host selector\n // immediately so that content cannot paint once in the drawer geometry\n // while Stencil waits to commit its next render.\n if (this.el.getAttribute('presentation') !== next) {\n this.el.setAttribute('presentation', next);\n }\n const presentationGeneration = ++this.presentationMotionGeneration;\n this.presentationMotionSuppressed = true;\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n if (presentationGeneration !== this.presentationMotionGeneration) return;\n this.presentationMotionSuppressed = false;\n });\n });\n if (next === 'fullscreen') {\n this.focusBeforeFullscreen =\n document.activeElement instanceof HTMLElement ? document.activeElement : null;\n this.open = true;\n } else if (previous === 'fullscreen') {\n const focusBeforeFullscreen = this.focusBeforeFullscreen;\n const fallbackActionId = this.activeHeader().actions?.[0]?.id;\n requestAnimationFrame(async () => {\n if (focusBeforeFullscreen?.isConnected) {\n focusBeforeFullscreen.focus({ preventScroll: true });\n }\n const focusRestored = focusBeforeFullscreen\n ? document.activeElement === focusBeforeFullscreen ||\n focusBeforeFullscreen.contains(document.activeElement)\n : false;\n if (!focusRestored && fallbackActionId) await this.focusHeaderAction(fallbackActionId);\n this.focusBeforeFullscreen = null;\n });\n }\n this.dsPresentationChange.emit({ presentation: next });\n }\n\n @Listen('keydown', { target: 'window', capture: true })\n handleFullscreenEscape(event: KeyboardEvent) {\n if (this.presentation !== 'fullscreen' || event.key !== 'Escape') return;\n event.preventDefault();\n event.stopPropagation();\n this.presentation = 'drawer';\n }\n\n private deferMotionEnable() {\n if (this.readyForMotion) return;\n const generation = ++this.motionEnableGeneration;\n requestAnimationFrame(() => {\n if (generation !== this.motionEnableGeneration) return;\n requestAnimationFrame(() => {\n if (generation !== this.motionEnableGeneration) return;\n this.readyForMotion = true;\n });\n });\n }\n\n connectedCallback() {\n this.el.setAttribute(ANCHORED_OVERLAY_BOUNDARY_ATTRIBUTE, '');\n this.el.addEventListener('transitionend', this.handleTransitionEnd);\n this.el.addEventListener('transitioncancel', this.handleTransitionEnd);\n this.el.addEventListener('dsAction', this.handleComposedHeaderAction);\n }\n\n private handleComposedHeaderAction = (event: Event) => {\n const detail = (event as CustomEvent<{ id?: string; anchor?: HTMLElement }>).detail;\n if (!this.activeTool || !detail?.id) return;\n event.stopPropagation();\n this.dsHeaderAction.emit({\n tool: this.activeTool,\n id: detail.id,\n anchor: detail.anchor,\n });\n };\n\n componentWillLoad() {\n this.restoreLastActiveTool();\n this.drawerSurfaceRetained = this.open;\n }\n\n @Watch('open')\n openChanged(isOpen: boolean, wasOpen?: boolean) {\n if (isOpen) this.drawerSurfaceRetained = true;\n if (this.readyForMotion && wasOpen !== undefined && wasOpen !== isOpen) {\n this.startDrawerTransition(isOpen ? 'opening' : 'closing');\n } else if (!isOpen) {\n this.drawerSurfaceRetained = false;\n }\n this.deferMotionEnable();\n }\n\n @Watch('activeTool')\n activeToolChanged() {\n this.persistLastActiveTool();\n this.deferMotionEnable();\n }\n\n @Watch('storageKey')\n storageKeyChanged() {\n this.restoreLastActiveTool();\n }\n\n componentDidLoad() {\n this.deferMotionEnable();\n this.connectRailBodyObserver();\n this.updateRailBodyScrollable();\n this.syncFullViewTools();\n this.fullViewObserver = new MutationObserver(() => this.syncFullViewTools());\n this.fullViewObserver.observe(this.el, {\n childList: true,\n attributes: true,\n attributeFilter: ['slot'],\n });\n }\n\n componentDidRender() {\n this.updateRailBodyScrollable();\n }\n\n private connectRailBodyObserver() {\n this.disconnectRailBodyObserver();\n if (!this.railBodyEl || typeof ResizeObserver === 'undefined') return;\n this.railBodyObserver = new ResizeObserver(() => this.updateRailBodyScrollable());\n this.railBodyObserver.observe(this.railBodyEl);\n }\n\n private disconnectRailBodyObserver() {\n this.railBodyObserver?.disconnect();\n this.railBodyObserver = undefined;\n }\n\n private updateRailBodyScrollable() {\n const next = Boolean(\n this.railBodyEl && this.railBodyEl.scrollHeight > this.railBodyEl.clientHeight + 1\n );\n if (next !== this.railBodyScrollable) this.railBodyScrollable = next;\n }\n\n @Watch('items')\n itemsChanged() {\n this.rovingIndex = 0;\n this.reconcileActiveTool();\n this.deferMotionEnable();\n }\n\n @Watch('accessories')\n accessoriesChanged(\n next: PanelToolsRailAccessory[] = [],\n previous: PanelToolsRailAccessory[] = []\n ) {\n const previousKeys = panelToolsRailFocusKeys(\n orderPanelToolsRailEntries(this.railItems, previous)\n );\n const nextKeys = panelToolsRailFocusKeys(orderPanelToolsRailEntries(this.railItems, next));\n const activeElement = (this.renderRoot as ShadowRoot).activeElement as HTMLElement | null;\n const focusedKey =\n activeElement?.closest<HTMLElement>('[data-rail-focus-key]')?.dataset.railFocusKey;\n const focusedIndex = focusedKey ? previousKeys.indexOf(focusedKey) : -1;\n const currentIndex = focusedIndex >= 0 ? focusedIndex : this.rovingIndex;\n const nextIndex = reconcilePanelToolsRovingIndex(previousKeys, nextKeys, currentIndex);\n const restoreFocus = focusedIndex >= 0 && !nextKeys.includes(focusedKey ?? '');\n this.rovingIndex = nextIndex;\n if (restoreFocus && nextKeys.length) {\n requestAnimationFrame(() => this.focusRailAt(nextIndex));\n }\n }\n\n private restoreLastActiveTool() {\n if (!this.storageKey || this.activeTool) return;\n try {\n const stored = localStorage.getItem(this.storageKey);\n if (isPanelToolsToolId(stored)) this.activeTool = stored;\n } catch {\n /* localStorage unavailable */\n }\n }\n\n private persistLastActiveTool() {\n if (!this.storageKey || !this.activeTool) return;\n try {\n localStorage.setItem(this.storageKey, this.activeTool);\n } catch {\n /* unavailable */\n }\n }\n\n private clearPersistedTool() {\n if (!this.storageKey) return;\n try {\n localStorage.removeItem(this.storageKey);\n } catch {\n /* unavailable */\n }\n }\n\n private reconcileActiveTool() {\n const next = reconcilePanelToolsAvailability(this.railItems, this.open, this.activeTool);\n if (!next.removedTool) return;\n this.clearPersistedTool();\n this.open = next.open;\n this.activeTool = next.activeTool;\n this.dsToolChange.emit({ id: next.removedTool, selected: false });\n }\n\n private startDrawerTransition(phase: 'opening' | 'closing') {\n const drawer = this.renderRoot.querySelector('.panel-tools__drawer') as HTMLElement | null;\n if (this.motion !== 'idle') {\n this.finishDrawerTransition();\n this.ignoreReplacementTerminal = true;\n }\n this.motion = phase;\n this.dsChromeTransitionStart.emit({ source: 'panel-tools', phase });\n const generation = ++this.transitionGeneration;\n\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n if (generation !== this.transitionGeneration || this.motion !== phase) return;\n if (!drawer) {\n this.finishDrawerTransition();\n return;\n }\n\n if (this.drawerTransitionAtTerminal(drawer, phase)) {\n this.finishDrawerTransition();\n return;\n }\n\n const fallbackMs = panelToolsDrawerTransitionMs(getComputedStyle(drawer));\n if (fallbackMs <= 0) {\n this.finishDrawerTransition();\n return;\n }\n this.transitionFallbackTimer = window.setTimeout(() => {\n this.finishDrawerTransition();\n }, fallbackMs);\n });\n });\n }\n\n private handleTransitionEnd = (event: TransitionEvent) => {\n const drawer = this.renderRoot.querySelector('.panel-tools__drawer') as HTMLElement | null;\n if (event.target !== drawer) return;\n if (event.propertyName !== 'max-width') return;\n if (this.ignoreReplacementTerminal) {\n this.ignoreReplacementTerminal = false;\n return;\n }\n // Changing from the neutral transition recipe to the direction-specific\n // easing can cancel the transition that the reflected `open` class began.\n // The replacement transition and its watchdog still own completion; ending\n // motion here would hide the fixed-width tool surface while it is visible.\n if (event.type === 'transitioncancel') return;\n if (drawer && !this.drawerTransitionAtTerminal(drawer, this.motion)) return;\n this.finishDrawerTransition();\n };\n\n private drawerTransitionAtTerminal(\n drawer: HTMLElement,\n phase: 'opening' | 'closing' | 'idle'\n ): boolean {\n const width = drawer.getBoundingClientRect().width;\n const surface = drawer.querySelector('.panel-tools__drawer-surface') as HTMLElement | null;\n const targetWidth = surface?.getBoundingClientRect().width ?? 0;\n return panelToolsDrawerAtTerminal(width, targetWidth, phase);\n }\n\n private finishDrawerTransition() {\n const completedPhase = this.motion;\n const wasTransitioning = completedPhase !== 'idle';\n this.clearTransitionCompletion();\n if (!wasTransitioning) return;\n this.ignoreReplacementTerminal = false;\n this.motion = 'idle';\n if (completedPhase === 'closing' && !this.open) {\n this.drawerSurfaceRetained = false;\n }\n this.dsChromeTransitionEnd.emit({ source: 'panel-tools' });\n }\n\n private clearTransitionCompletion() {\n this.transitionGeneration += 1;\n if (this.transitionFallbackTimer !== null) {\n window.clearTimeout(this.transitionFallbackTimer);\n this.transitionFallbackTimer = null;\n }\n }\n\n /** Rail selection follows `open` immediately — independent of the slide animation. */\n private isRailSelected(id: PanelToolsToolId): boolean {\n return this.open && this.activeTool === id;\n }\n\n /** Drawer body stays mounted while the clip frame animates closed. */\n private isDrawerPresent(): boolean {\n return this.open || this.drawerSurfaceRetained;\n }\n\n private isViewActive(id: PanelToolsToolId): boolean {\n return this.isDrawerPresent() && this.activeTool === id;\n }\n\n private hasFullView(id: PanelToolsToolId): boolean {\n return this.fullViewToolIds.includes(id);\n }\n\n private syncFullViewTools() {\n const ids = this.orderedRailItems.map(item => item.id);\n const next = ids.filter(id => Boolean(this.el.querySelector(`[slot=\"${id}-view\"]`)));\n if (next.join('|') !== this.fullViewToolIds.join('|')) this.fullViewToolIds = next;\n }\n\n private get renderRoot(): ParentNode {\n return this.el.shadowRoot ?? this.el;\n }\n\n private accessibleLabel(): string {\n if (!this.isDrawerPresent() || !this.activeTool) return '';\n const item = this.railItems.find(candidate => candidate.id === this.activeTool);\n return (\n item?.label ??\n PANEL_TOOLS_LABELS[this.activeTool as keyof typeof PANEL_TOOLS_LABELS] ??\n this.activeTool\n );\n }\n\n private resolvedHeaders(): PanelToolsHeaders {\n return this.headers ?? {};\n }\n\n private activeHeader(): PanelToolsHeaderConfig {\n if (!this.activeTool) return {};\n return this.resolvedHeaders()[this.activeTool] ?? {};\n }\n\n private handleToolChange = (id: PanelToolsToolId) => {\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 /** Toggle any rail tool open/closed — shell shortcuts K/A/S/M/N call this. */\n @Method()\n async activateTool(id: PanelToolsToolId) {\n const item = this.railItems.find(entry => entry.id === id);\n if (!item || item.isInactive) return;\n this.handleToolChange(id);\n }\n\n /** Close the tools drawer when open — used by shell keyboard shortcuts. */\n @Method()\n async closeDrawer() {\n if (!this.open) return;\n const id = this.activeTool;\n this.open = false;\n if (id) {\n this.dsToolChange.emit({ id, selected: false });\n }\n }\n\n /** Focus an active tool-header action by its application-owned id. */\n @Method()\n async focusHeaderAction(id: string) {\n const action = this.renderRoot.querySelector(`[data-header-action-id=\"${CSS.escape(id)}\"]`) as\n | (HTMLElement & { setFocus?: () => Promise<void> })\n | null;\n await action?.setFocus?.();\n }\n\n private focusRailAt(index: number) {\n const keys = panelToolsRailFocusKeys(this.orderedRailEntries);\n if (!keys.length) return;\n const bounded = Math.max(0, Math.min(index, keys.length - 1));\n this.rovingIndex = bounded;\n const action = this.renderRoot.querySelector(\n `[data-rail-focus-key=\"${CSS.escape(keys[bounded])}\"]`\n ) as (HTMLElement & { setFocus?: () => Promise<void> }) | null;\n if (action?.setFocus) {\n void action.setFocus();\n } else {\n action?.focus({ preventScroll: true });\n }\n }\n\n private handleRailKeyDown = (e: KeyboardEvent, index: number, activate?: () => void) => {\n const keys = panelToolsRailFocusKeys(this.orderedRailEntries);\n if (!keys.length) return;\n\n if ((e.key === 'Enter' || e.key === ' ') && activate) {\n e.preventDefault();\n activate();\n return;\n }\n\n if (e.key === 'ArrowDown') {\n if (index >= keys.length - 1) return;\n e.preventDefault();\n this.focusRailAt(index + 1);\n return;\n }\n\n if (e.key === 'ArrowUp') {\n if (index <= 0) return;\n e.preventDefault();\n this.focusRailAt(index - 1);\n }\n };\n\n private renderRailAction(item: PanelToolsItem, index: number) {\n const focusKey = `tool:${item.id}`;\n const label =\n item.ariaLabel ??\n item.label ??\n PANEL_TOOLS_LABELS[item.id as keyof typeof PANEL_TOOLS_LABELS] ??\n item.id;\n return (\n <ds-tooltip\n key={item.id}\n label={label}\n shortcutKey={\n item.shortcutKey ?? PANEL_TOOLS_SHORTCUTS[item.id as keyof typeof PANEL_TOOLS_SHORTCUTS]\n }\n side=\"left\"\n size=\"sm\"\n boundary=\"viewport\"\n >\n <ds-button-unfilled\n variant=\"icon\"\n class=\"panel-tools__rail-action\"\n icon={item.icon}\n isActive={this.isRailSelected(item.id)}\n activeFill={false}\n hasBorder={false}\n pressScale={false}\n dot={item.dot ?? false}\n isInactive={item.isInactive}\n focusTabIndex={index === this.rovingIndex ? 0 : -1}\n data-rail-focus-key={focusKey}\n aria-label={label}\n pressed={this.isRailSelected(item.id)}\n onFocusin={() => {\n this.rovingIndex = index;\n }}\n onKeyDown={(e: KeyboardEvent) =>\n this.handleRailKeyDown(e, index, () => this.handleToolChange(item.id))\n }\n onDsClick={() => this.handleToolChange(item.id)}\n />\n </ds-tooltip>\n );\n }\n\n private accessoryPrimaryLabel(accessory: PanelToolsRailTransientAccessory): string {\n const actionLabel = accessory.primaryAction.ariaLabel?.trim() || accessory.ariaLabel;\n const status = accessory.statusText.trim();\n return status ? `${actionLabel}. ${status}` : actionLabel;\n }\n\n private emitAccessoryAction(\n accessoryId: string,\n action: PanelToolsRailAccessoryAction,\n anchor: HTMLElement\n ) {\n if (action.isInactive) return;\n this.dsRailAccessoryAction.emit(\n panelToolsRailAccessoryActionDetail(accessoryId, action.id, anchor)\n );\n }\n\n private renderAccessoryVisual(accessory: PanelToolsRailTransientAccessory) {\n if (accessory.visual.type === 'image') {\n return (\n <img\n class=\"panel-tools__accessory-image\"\n src={accessory.visual.src}\n alt=\"\"\n aria-hidden=\"true\"\n />\n );\n }\n if (accessory.visual.type === 'initial') {\n const initial = [...accessory.visual.initial.trim()].slice(0, 2).join('');\n return (\n <ds-text\n class=\"panel-tools__accessory-initial\"\n as=\"span\"\n variant=\"text-caption\"\n emphasis\n color=\"inherit\"\n aria-hidden=\"true\"\n >\n {initial}\n </ds-text>\n );\n }\n return <ds-icon name={accessory.visual.icon} size=\"sm\" aria-hidden=\"true\" />;\n }\n\n private renderRailShortcut(\n accessory: PanelToolsRailShortcutAccessory,\n focusIndexes: Map<string, number>\n ) {\n const focusKey = `accessory:${accessory.id}:${accessory.action.id}`;\n const focusIndex = focusIndexes.get(focusKey);\n const label = accessory.action.ariaLabel?.trim() || accessory.ariaLabel;\n const initials = [...accessory.initials.trim()].slice(0, 2).join('');\n\n return (\n <div\n key={`accessory:${accessory.id}`}\n class=\"panel-tools__accessory panel-tools__accessory--shortcut\"\n >\n <ds-tooltip label={label} side=\"left\" size=\"sm\" boundary=\"viewport\">\n <button\n type=\"button\"\n class=\"panel-tools__shortcut ds-focus-ring-inset ds-interaction-fill\"\n disabled={accessory.action.isInactive}\n tabIndex={focusIndex === this.rovingIndex ? 0 : -1}\n data-rail-focus-key={focusKey}\n aria-label={label}\n onFocus={() => {\n if (focusIndex !== undefined) this.rovingIndex = focusIndex;\n }}\n onKeyDown={(event: KeyboardEvent) =>\n focusIndex === undefined ? undefined : this.handleRailKeyDown(event, focusIndex)\n }\n onClick={(event: MouseEvent) =>\n this.emitAccessoryAction(\n accessory.id,\n accessory.action,\n event.currentTarget as HTMLElement\n )\n }\n >\n <span class=\"panel-tools__shortcut-content ds-interaction-fill__content\">\n <ds-text\n class=\"panel-tools__shortcut-orb\"\n as=\"span\"\n variant=\"text-caption\"\n emphasis\n color=\"inherit\"\n >\n {initials}\n </ds-text>\n {accessory.dot ? (\n <ds-badge\n class=\"panel-tools__shortcut-dot\"\n variant=\"dot\"\n background=\"var(--color-background-secondary)\"\n aria-hidden=\"true\"\n />\n ) : null}\n </span>\n </button>\n </ds-tooltip>\n </div>\n );\n }\n\n private renderRailAccessory(\n accessory: PanelToolsRailAccessory,\n focusIndexes: Map<string, number>\n ) {\n if (accessory.type === 'divider') {\n return (\n <div\n key={`accessory:${accessory.id}`}\n class=\"panel-tools__accessory-divider\"\n aria-hidden=\"true\"\n >\n <ds-divider />\n </div>\n );\n }\n\n if (accessory.type === 'shortcut') {\n return this.renderRailShortcut(accessory, focusIndexes);\n }\n\n const primaryKey = `accessory:${accessory.id}:${accessory.primaryAction.id}`;\n const primaryIndex = focusIndexes.get(primaryKey);\n const primaryLabel = this.accessoryPrimaryLabel(accessory);\n const secondary = accessory.secondaryAction;\n const secondaryKey = secondary ? `accessory:${accessory.id}:${secondary.id}` : '';\n const secondaryIndex = secondary ? focusIndexes.get(secondaryKey) : undefined;\n\n return (\n <div\n key={`accessory:${accessory.id}`}\n class={{\n 'panel-tools__accessory': true,\n 'panel-tools__accessory--transient': true,\n 'panel-tools__accessory--transient-active': accessory.statusTone === 'active',\n 'panel-tools__accessory--transient-positive': accessory.statusTone === 'positive',\n }}\n >\n <ds-tooltip label={primaryLabel} side=\"left\" size=\"sm\" boundary=\"viewport\">\n <button\n type=\"button\"\n class=\"panel-tools__accessory-primary ds-focus-ring-inset ds-interaction-fill ds-interaction-fill--on-bold\"\n disabled={accessory.primaryAction.isInactive}\n tabIndex={primaryIndex === this.rovingIndex ? 0 : -1}\n data-rail-focus-key={primaryKey}\n aria-label={primaryLabel}\n onFocus={() => {\n if (primaryIndex !== undefined) this.rovingIndex = primaryIndex;\n }}\n onKeyDown={(event: KeyboardEvent) =>\n primaryIndex === undefined ? undefined : this.handleRailKeyDown(event, primaryIndex)\n }\n onClick={(event: MouseEvent) =>\n this.emitAccessoryAction(\n accessory.id,\n accessory.primaryAction,\n event.currentTarget as HTMLElement\n )\n }\n >\n <span class=\"panel-tools__accessory-primary-content ds-interaction-fill__content\">\n {this.renderAccessoryVisual(accessory)}\n </span>\n </button>\n </ds-tooltip>\n {secondary ? (\n <ds-tooltip label={secondary.ariaLabel} side=\"left\" size=\"sm\" boundary=\"viewport\">\n <ds-button-unfilled\n class=\"panel-tools__accessory-secondary\"\n variant=\"icon\"\n size=\"sm\"\n rounded\n background=\"bold\"\n hasBorder={false}\n icon={secondary.icon}\n isInactive={secondary.isInactive}\n focusTabIndex={secondaryIndex === this.rovingIndex ? 0 : -1}\n data-rail-focus-key={secondaryKey}\n aria-label={secondary.ariaLabel}\n onFocusin={() => {\n if (secondaryIndex !== undefined) this.rovingIndex = secondaryIndex;\n }}\n onKeyDown={(event: KeyboardEvent) =>\n secondaryIndex === undefined\n ? undefined\n : this.handleRailKeyDown(event, secondaryIndex)\n }\n onDsClick={(event: CustomEvent<MouseEvent>) =>\n this.emitAccessoryAction(\n accessory.id,\n secondary,\n event.currentTarget as HTMLElement\n )\n }\n />\n </ds-tooltip>\n ) : null}\n </div>\n );\n }\n\n private renderRailEntry(entry: PanelToolsRailEntry, focusIndexes: Map<string, number>) {\n if (entry.type === 'tool') {\n return this.renderRailAction(entry.item, focusIndexes.get(`tool:${entry.id}`) ?? 0);\n }\n return this.renderRailAccessory(entry.accessory, focusIndexes);\n }\n\n render() {\n const accessibleLabel = this.accessibleLabel();\n const header = this.activeHeader();\n const headerTitle = header.title?.trim() || accessibleLabel;\n const headerActions = header.actions ?? [];\n const orderedRailItems = this.orderedRailItems;\n const orderedRailEntries = this.orderedRailEntries;\n const entryPlacement = (entry: PanelToolsRailEntry) =>\n entry.type === 'tool' ? panelToolsRailPlacement(entry.item) : entry.accessory.railPlacement;\n const headerEntries = orderedRailEntries.filter(entry => entryPlacement(entry) === 'header');\n const bodyEntries = orderedRailEntries.filter(entry => entryPlacement(entry) === 'body');\n const footerEntries = orderedRailEntries.filter(entry => entryPlacement(entry) === 'footer');\n const focusIndexes = new Map(\n panelToolsRailFocusKeys(orderedRailEntries).map((key, index) => [key, index])\n );\n const showDrawerChrome = this.isDrawerPresent();\n const drawerResting = panelToolsDrawerResting(this.open, this.motion);\n const activeFullView = Boolean(\n showDrawerChrome && this.activeTool && this.hasFullView(this.activeTool)\n );\n const toolIds = orderedRailItems.map(item => item.id);\n\n return (\n <Host\n class={{\n 'panel-tools': true,\n 'panel-tools--open': this.open,\n 'panel-tools--ready': this.readyForMotion,\n 'panel-tools--motion-opening': this.motion === 'opening',\n 'panel-tools--motion-closing': this.motion === 'closing',\n 'panel-tools--drawer-resting': drawerResting,\n 'panel-tools--fullscreen': this.presentation === 'fullscreen',\n 'panel-tools--presentation-snap': this.presentationMotionSuppressed,\n }}\n data-ds-overlay-boundary\n role={this.presentation === 'fullscreen' ? 'dialog' : 'complementary'}\n aria-modal={this.presentation === 'fullscreen' ? 'true' : undefined}\n aria-label={this.toolsLabel}\n >\n <div class=\"panel-tools__layout\">\n <nav class=\"panel-tools__rail\" aria-label={this.toolShortcutsLabel}>\n {headerEntries.length ? (\n <div\n class={{\n 'panel-tools__rail-header': true,\n 'panel-tools__rail-section--stacked': headerEntries.length > 1,\n 'ds-chrome-row': headerEntries.length === 1,\n 'ds-chrome-column': headerEntries.length > 1,\n 'ds-chrome-space--md': true,\n }}\n >\n {headerEntries.map(entry => this.renderRailEntry(entry, focusIndexes))}\n </div>\n ) : null}\n <div\n class={{\n 'panel-tools__rail-body': true,\n 'ds-chrome-column': true,\n 'ds-chrome-space--md': true,\n 'ds-scrollbar-hidden': true,\n 'ds-focus-ring': this.railBodyScrollable,\n }}\n ref={element => {\n const next = (element as HTMLElement) ?? undefined;\n if (next === this.railBodyEl) return;\n this.railBodyEl = next;\n this.connectRailBodyObserver();\n }}\n role={this.railBodyScrollable ? 'region' : undefined}\n aria-label={this.railBodyScrollable ? this.toolShortcutsLabel : undefined}\n tabIndex={this.railBodyScrollable ? 0 : undefined}\n >\n <div class=\"panel-tools__rail-actions\">\n {bodyEntries.map(entry => this.renderRailEntry(entry, focusIndexes))}\n </div>\n </div>\n {footerEntries.length ? (\n <div\n class={{\n 'panel-tools__rail-footer': true,\n 'panel-tools__rail-section--stacked': footerEntries.length > 1,\n 'ds-chrome-row': footerEntries.length === 1,\n 'ds-chrome-column': footerEntries.length > 1,\n 'ds-chrome-space--md': true,\n }}\n >\n {footerEntries.map(entry => this.renderRailEntry(entry, focusIndexes))}\n </div>\n ) : null}\n </nav>\n\n <div\n class={{\n 'panel-tools__drawer': true,\n 'panel-tools__drawer--visible': showDrawerChrome,\n }}\n aria-hidden={showDrawerChrome ? null : 'true'}\n inert={showDrawerChrome ? undefined : true}\n >\n <div class=\"panel-tools__drawer-surface\">\n {this.presentation !== 'fullscreen' || this.fullscreenHeaderMode === 'shared' ? (\n <ds-panel-tool-header\n class=\"panel-tools__header\"\n heading={headerTitle}\n showBack={header.showBack ?? false}\n backIcon={header.backIcon || 'ChevronLeft'}\n backAriaLabel={header.backAriaLabel || 'Back'}\n showMenu={false}\n ref={element => {\n const toolHeader = element as\n | (HTMLDsPanelToolHeaderElement & {\n actions: PanelToolsHeaderAction[];\n })\n | undefined;\n if (toolHeader) toolHeader.actions = headerActions;\n }}\n onDsBack={() => {\n if (this.activeTool) this.dsHeaderBack.emit({ tool: this.activeTool });\n }}\n />\n ) : null}\n <div class=\"panel-tools__body\" hidden={activeFullView}>\n {toolIds.map(id => {\n const active = this.isViewActive(id);\n return (\n <div\n class={{\n 'panel-tools__view': true,\n 'panel-tools__view--active': active,\n }}\n hidden={!active}\n >\n <slot name={id} />\n </div>\n );\n })}\n </div>\n <div class=\"panel-tools__full-views\" hidden={!activeFullView}>\n {toolIds.map(id => (\n <div\n class={{\n 'panel-tools__full-view': true,\n 'panel-tools__full-view--active':\n this.isViewActive(id) && this.hasFullView(id),\n }}\n hidden={!(this.isViewActive(id) && this.hasFullView(id))}\n >\n <slot name={`${id}-view`} />\n </div>\n ))}\n </div>\n </div>\n </div>\n </div>\n </Host>\n );\n }\n}\n",
550
+ "content": "import {\n Component,\n Prop,\n Event,\n EventEmitter,\n Element,\n State,\n Watch,\n Method,\n Listen,\n h,\n Host,\n} from '@stencil/core';\nimport type { ChromeTransitionDetail } from '../../shell/chrome-transition';\nimport { ANCHORED_OVERLAY_BOUNDARY_ATTRIBUTE } from '../../utils/anchored-overlay-boundary';\nimport {\n PANEL_TOOLS_LABELS,\n PANEL_TOOLS_SHORTCUTS,\n type PanelToolsHeaderAction,\n type PanelToolsHeaderActionDetail,\n type PanelToolsHeaderConfig,\n type PanelToolsHeaders,\n type PanelToolsItem,\n type PanelToolsRailAccessory,\n type PanelToolsRailAccessoryAction,\n type PanelToolsRailAccessoryActionDetail,\n type PanelToolsRailShortcutAccessory,\n type PanelToolsRailTransientAccessory,\n type PanelToolsToolId,\n} from './panel-tools-types';\nimport {\n isPanelToolsToolId,\n orderPanelToolsRailEntries,\n orderPanelToolsItems,\n panelToolsRailAccessoryActionDetail,\n panelToolsRailFocusKeys,\n panelToolsDrawerAtTerminal,\n panelToolsDrawerResting,\n panelToolsDrawerTransitionMs,\n panelToolsRailPlacement,\n reconcilePanelToolsAvailability,\n reconcilePanelToolsRovingIndex,\n resolvePanelToolActivation,\n type PanelToolsRailEntry,\n} from './panel-tools-utils';\n\n@Component({\n tag: 'ds-panel-tools',\n styleUrl: 'PanelTools.css',\n shadow: true,\n})\nexport class PanelTools {\n @Element() el!: HTMLElement;\n\n /** When false, only the icon rail is shown. */\n @Prop({ mutable: true, reflect: true }) open: boolean = false;\n\n /** Drawer presentation or presentation covering the shell stage for the active tool. */\n @Prop({ mutable: true, reflect: true }) presentation: 'drawer' | 'fullscreen' = 'drawer';\n\n /** Let a fullscreen product supply independent master/detail headers inside its view. */\n @Prop({ attribute: 'fullscreen-header-mode', reflect: true })\n fullscreenHeaderMode: 'shared' | 'split' = 'shared';\n\n /** Active tool view — `search`, `agents`, `messages`, `stacks`, `activity`, or `help`. */\n @Prop({ mutable: true, attribute: 'active-tool', reflect: true })\n activeTool: PanelToolsToolId | '' = '';\n\n /**\n * Rail items rendered in the right column.\n * Set via JS property: `el.items = [...]`. Replace the array reference to update.\n */\n @Prop() items: PanelToolsItem[] = [];\n\n /**\n * Desktop/tablet-only application-owned rail boundaries and direct intents.\n * Set via JS property and replace the array reference to update.\n */\n @Prop() accessories: PanelToolsRailAccessory[] = [];\n\n @Prop() toolsLabel: string = 'Tools';\n @Prop() toolShortcutsLabel: string = 'Tool shortcuts';\n\n /** Active header state per tool. Replace the object when title, depth, or actions change. */\n @Prop() headers: PanelToolsHeaders = {};\n\n /**\n * Optional localStorage key for the last active tool. The drawer always starts\n * closed; only the tool identity is restored for continuity within this browser.\n */\n @Prop({ attribute: 'storage-key' }) storageKey: string = '';\n\n /** Emitted when a rail button is toggled. Detail = { id, selected }. */\n @Event({ bubbles: true, composed: true }) dsToolChange!: EventEmitter<{\n id: PanelToolsToolId;\n selected: boolean;\n }>;\n\n /** Emitted when fullscreen changes, including Escape-initiated exits. */\n @Event({ bubbles: true, composed: true }) dsPresentationChange!: EventEmitter<{\n presentation: 'drawer' | 'fullscreen';\n }>;\n\n /** Requests navigation to the active tool's parent view. */\n @Event({ bubbles: true, composed: true }) dsHeaderBack!: EventEmitter<{\n tool: PanelToolsToolId;\n }>;\n\n /** Requests one application-owned action from the active tool header. */\n @Event({ bubbles: true, composed: true })\n dsHeaderAction!: EventEmitter<PanelToolsHeaderActionDetail>;\n\n /** Direct accessory intent. Emitting this event never changes the active tool or drawer. */\n @Event({ bubbles: true, composed: true })\n dsRailAccessoryAction!: EventEmitter<PanelToolsRailAccessoryActionDetail>;\n\n /** Bubbling lifecycle — `ds-bar-nav` defers overflow checks during drawer motion. */\n @Event({ bubbles: true, composed: true })\n dsChromeTransitionStart!: EventEmitter<ChromeTransitionDetail>;\n\n @Event({ bubbles: true, composed: true })\n dsChromeTransitionEnd!: EventEmitter<ChromeTransitionDetail>;\n\n /** Arms open vs close easing for the in-flight width transition. */\n @State() private motion: 'opening' | 'closing' | 'idle' = 'idle';\n\n /** Keeps header and slotted content painted until the closing clip transition completes. */\n @State() private drawerSurfaceRetained = false;\n\n /** Suppresses width transition until the host has painted its initial open state. */\n @State() private readyForMotion = false;\n\n /** Fullscreen presentation changes snap without borrowing drawer width motion. */\n @State() private presentationMotionSuppressed = false;\n\n @State() private rovingIndex = 0;\n @State() private fullViewToolIds: PanelToolsToolId[] = [];\n @State() private railBodyScrollable = false;\n\n private motionEnableGeneration = 0;\n private presentationMotionGeneration = 0;\n private transitionGeneration = 0;\n private transitionFallbackTimer: number | null = null;\n /** Ignores the old transition's terminal event when reversing direction. */\n private ignoreReplacementTerminal = false;\n private focusBeforeFullscreen: HTMLElement | null = null;\n private fullViewObserver: MutationObserver | null = null;\n private railBodyEl?: HTMLElement;\n private railBodyObserver?: ResizeObserver;\n\n private get railItems(): PanelToolsItem[] {\n return this.items ?? [];\n }\n\n private get orderedRailItems(): PanelToolsItem[] {\n return orderPanelToolsItems(this.railItems);\n }\n\n private get railAccessories(): PanelToolsRailAccessory[] {\n return this.accessories ?? [];\n }\n\n private get orderedRailEntries(): PanelToolsRailEntry[] {\n return orderPanelToolsRailEntries(this.railItems, this.railAccessories);\n }\n\n disconnectedCallback() {\n this.el.removeEventListener('transitionend', this.handleTransitionEnd);\n this.el.removeEventListener('transitioncancel', this.handleTransitionEnd);\n this.el.removeEventListener('dsAction', this.handleComposedHeaderAction);\n this.clearTransitionCompletion();\n this.motionEnableGeneration += 1;\n this.fullViewObserver?.disconnect();\n this.fullViewObserver = null;\n this.disconnectRailBodyObserver();\n }\n\n @Watch('presentation')\n presentationChanged(next: 'drawer' | 'fullscreen', previous?: 'drawer' | 'fullscreen') {\n if (next === previous) return;\n // Framework property bindings can update slotted master/detail content in\n // the same render that requests fullscreen. Reflect the host selector\n // immediately so that content cannot paint once in the drawer geometry\n // while Stencil waits to commit its next render.\n if (this.el.getAttribute('presentation') !== next) {\n this.el.setAttribute('presentation', next);\n }\n const presentationGeneration = ++this.presentationMotionGeneration;\n this.presentationMotionSuppressed = true;\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n if (presentationGeneration !== this.presentationMotionGeneration) return;\n this.presentationMotionSuppressed = false;\n });\n });\n if (next === 'fullscreen') {\n this.focusBeforeFullscreen =\n document.activeElement instanceof HTMLElement ? document.activeElement : null;\n this.open = true;\n } else if (previous === 'fullscreen') {\n const focusBeforeFullscreen = this.focusBeforeFullscreen;\n const fallbackActionId = this.activeHeader().actions?.[0]?.id;\n requestAnimationFrame(async () => {\n if (focusBeforeFullscreen?.isConnected) {\n focusBeforeFullscreen.focus({ preventScroll: true });\n }\n const focusRestored = focusBeforeFullscreen\n ? document.activeElement === focusBeforeFullscreen ||\n focusBeforeFullscreen.contains(document.activeElement)\n : false;\n if (!focusRestored && fallbackActionId) await this.focusHeaderAction(fallbackActionId);\n this.focusBeforeFullscreen = null;\n });\n }\n this.dsPresentationChange.emit({ presentation: next });\n }\n\n @Listen('keydown', { target: 'window', capture: true })\n handleFullscreenEscape(event: KeyboardEvent) {\n if (this.presentation !== 'fullscreen' || event.key !== 'Escape') return;\n event.preventDefault();\n event.stopPropagation();\n this.presentation = 'drawer';\n }\n\n private deferMotionEnable() {\n if (this.readyForMotion) return;\n const generation = ++this.motionEnableGeneration;\n requestAnimationFrame(() => {\n if (generation !== this.motionEnableGeneration) return;\n requestAnimationFrame(() => {\n if (generation !== this.motionEnableGeneration) return;\n this.readyForMotion = true;\n });\n });\n }\n\n connectedCallback() {\n this.el.setAttribute(ANCHORED_OVERLAY_BOUNDARY_ATTRIBUTE, '');\n this.el.addEventListener('transitionend', this.handleTransitionEnd);\n this.el.addEventListener('transitioncancel', this.handleTransitionEnd);\n this.el.addEventListener('dsAction', this.handleComposedHeaderAction);\n }\n\n private handleComposedHeaderAction = (event: Event) => {\n const detail = (event as CustomEvent<{ id?: string; anchor?: HTMLElement }>).detail;\n if (!this.activeTool || !detail?.id) return;\n event.stopPropagation();\n this.dsHeaderAction.emit({\n tool: this.activeTool,\n id: detail.id,\n anchor: detail.anchor,\n });\n };\n\n componentWillLoad() {\n this.restoreLastActiveTool();\n this.drawerSurfaceRetained = this.open;\n }\n\n @Watch('open')\n openChanged(isOpen: boolean, wasOpen?: boolean) {\n if (isOpen) this.drawerSurfaceRetained = true;\n if (this.readyForMotion && wasOpen !== undefined && wasOpen !== isOpen) {\n this.startDrawerTransition(isOpen ? 'opening' : 'closing');\n } else if (!isOpen) {\n this.drawerSurfaceRetained = false;\n }\n this.deferMotionEnable();\n }\n\n @Watch('activeTool')\n activeToolChanged() {\n this.persistLastActiveTool();\n this.deferMotionEnable();\n }\n\n @Watch('storageKey')\n storageKeyChanged() {\n this.restoreLastActiveTool();\n }\n\n componentDidLoad() {\n this.deferMotionEnable();\n this.connectRailBodyObserver();\n this.updateRailBodyScrollable();\n this.syncFullViewTools();\n this.fullViewObserver = new MutationObserver(() => this.syncFullViewTools());\n this.fullViewObserver.observe(this.el, {\n childList: true,\n attributes: true,\n attributeFilter: ['slot'],\n });\n }\n\n componentDidRender() {\n this.updateRailBodyScrollable();\n }\n\n private connectRailBodyObserver() {\n this.disconnectRailBodyObserver();\n if (!this.railBodyEl || typeof ResizeObserver === 'undefined') return;\n this.railBodyObserver = new ResizeObserver(() => this.updateRailBodyScrollable());\n this.railBodyObserver.observe(this.railBodyEl);\n }\n\n private disconnectRailBodyObserver() {\n this.railBodyObserver?.disconnect();\n this.railBodyObserver = undefined;\n }\n\n private updateRailBodyScrollable() {\n const next = Boolean(\n this.railBodyEl && this.railBodyEl.scrollHeight > this.railBodyEl.clientHeight + 1\n );\n if (next !== this.railBodyScrollable) this.railBodyScrollable = next;\n }\n\n @Watch('items')\n itemsChanged() {\n this.rovingIndex = 0;\n this.reconcileActiveTool();\n this.deferMotionEnable();\n }\n\n @Watch('accessories')\n accessoriesChanged(\n next: PanelToolsRailAccessory[] = [],\n previous: PanelToolsRailAccessory[] = []\n ) {\n const previousKeys = panelToolsRailFocusKeys(\n orderPanelToolsRailEntries(this.railItems, previous)\n );\n const nextKeys = panelToolsRailFocusKeys(orderPanelToolsRailEntries(this.railItems, next));\n const activeElement = (this.renderRoot as ShadowRoot).activeElement as HTMLElement | null;\n const focusedKey =\n activeElement?.closest<HTMLElement>('[data-rail-focus-key]')?.dataset.railFocusKey;\n const focusedIndex = focusedKey ? previousKeys.indexOf(focusedKey) : -1;\n const currentIndex = focusedIndex >= 0 ? focusedIndex : this.rovingIndex;\n const nextIndex = reconcilePanelToolsRovingIndex(previousKeys, nextKeys, currentIndex);\n const restoreFocus = focusedIndex >= 0 && !nextKeys.includes(focusedKey ?? '');\n this.rovingIndex = nextIndex;\n if (restoreFocus && nextKeys.length) {\n requestAnimationFrame(() => this.focusRailAt(nextIndex));\n }\n }\n\n private restoreLastActiveTool() {\n if (!this.storageKey || this.activeTool) return;\n try {\n const stored = localStorage.getItem(this.storageKey);\n if (isPanelToolsToolId(stored)) this.activeTool = stored;\n } catch {\n /* localStorage unavailable */\n }\n }\n\n private persistLastActiveTool() {\n if (!this.storageKey || !this.activeTool) return;\n try {\n localStorage.setItem(this.storageKey, this.activeTool);\n } catch {\n /* unavailable */\n }\n }\n\n private clearPersistedTool() {\n if (!this.storageKey) return;\n try {\n localStorage.removeItem(this.storageKey);\n } catch {\n /* unavailable */\n }\n }\n\n private reconcileActiveTool() {\n const next = reconcilePanelToolsAvailability(this.railItems, this.open, this.activeTool);\n if (!next.removedTool) return;\n this.clearPersistedTool();\n this.open = next.open;\n this.activeTool = next.activeTool;\n this.dsToolChange.emit({ id: next.removedTool, selected: false });\n }\n\n private startDrawerTransition(phase: 'opening' | 'closing') {\n const drawer = this.renderRoot.querySelector('.panel-tools__drawer') as HTMLElement | null;\n if (this.motion !== 'idle') {\n this.finishDrawerTransition();\n this.ignoreReplacementTerminal = true;\n }\n this.motion = phase;\n this.dsChromeTransitionStart.emit({ source: 'panel-tools', phase });\n const generation = ++this.transitionGeneration;\n\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n if (generation !== this.transitionGeneration || this.motion !== phase) return;\n if (!drawer) {\n this.finishDrawerTransition();\n return;\n }\n\n if (this.drawerTransitionAtTerminal(drawer, phase)) {\n this.finishDrawerTransition();\n return;\n }\n\n const fallbackMs = panelToolsDrawerTransitionMs(getComputedStyle(drawer));\n if (fallbackMs <= 0) {\n this.finishDrawerTransition();\n return;\n }\n this.transitionFallbackTimer = window.setTimeout(() => {\n this.finishDrawerTransition();\n }, fallbackMs);\n });\n });\n }\n\n private handleTransitionEnd = (event: TransitionEvent) => {\n const drawer = this.renderRoot.querySelector('.panel-tools__drawer') as HTMLElement | null;\n if (event.target !== drawer) return;\n if (event.propertyName !== 'max-width') return;\n if (this.ignoreReplacementTerminal) {\n this.ignoreReplacementTerminal = false;\n return;\n }\n // Changing from the neutral transition recipe to the direction-specific\n // easing can cancel the transition that the reflected `open` class began.\n // The replacement transition and its watchdog still own completion; ending\n // motion here would hide the fixed-width tool surface while it is visible.\n if (event.type === 'transitioncancel') return;\n if (drawer && !this.drawerTransitionAtTerminal(drawer, this.motion)) return;\n this.finishDrawerTransition();\n };\n\n private drawerTransitionAtTerminal(\n drawer: HTMLElement,\n phase: 'opening' | 'closing' | 'idle'\n ): boolean {\n const width = drawer.getBoundingClientRect().width;\n const surface = drawer.querySelector('.panel-tools__drawer-surface') as HTMLElement | null;\n const targetWidth = surface?.getBoundingClientRect().width ?? 0;\n return panelToolsDrawerAtTerminal(width, targetWidth, phase);\n }\n\n private finishDrawerTransition() {\n const completedPhase = this.motion;\n const wasTransitioning = completedPhase !== 'idle';\n this.clearTransitionCompletion();\n if (!wasTransitioning) return;\n this.ignoreReplacementTerminal = false;\n this.motion = 'idle';\n if (completedPhase === 'closing' && !this.open) {\n this.drawerSurfaceRetained = false;\n }\n this.dsChromeTransitionEnd.emit({ source: 'panel-tools' });\n }\n\n private clearTransitionCompletion() {\n this.transitionGeneration += 1;\n if (this.transitionFallbackTimer !== null) {\n window.clearTimeout(this.transitionFallbackTimer);\n this.transitionFallbackTimer = null;\n }\n }\n\n /** Rail selection follows `open` immediately — independent of the slide animation. */\n private isRailSelected(id: PanelToolsToolId): boolean {\n return this.open && this.activeTool === id;\n }\n\n /** Drawer body stays mounted while the clip frame animates closed. */\n private isDrawerPresent(): boolean {\n return this.open || this.drawerSurfaceRetained;\n }\n\n private isViewActive(id: PanelToolsToolId): boolean {\n return this.isDrawerPresent() && this.activeTool === id;\n }\n\n private hasFullView(id: PanelToolsToolId): boolean {\n return this.fullViewToolIds.includes(id);\n }\n\n private syncFullViewTools() {\n const ids = this.orderedRailItems.map(item => item.id);\n const next = ids.filter(id => Boolean(this.el.querySelector(`[slot=\"${id}-view\"]`)));\n if (next.join('|') !== this.fullViewToolIds.join('|')) this.fullViewToolIds = next;\n }\n\n private get renderRoot(): ParentNode {\n return this.el.shadowRoot ?? this.el;\n }\n\n private accessibleLabel(): string {\n if (!this.isDrawerPresent() || !this.activeTool) return '';\n const item = this.railItems.find(candidate => candidate.id === this.activeTool);\n return (\n item?.label ??\n PANEL_TOOLS_LABELS[this.activeTool as keyof typeof PANEL_TOOLS_LABELS] ??\n this.activeTool\n );\n }\n\n private resolvedHeaders(): PanelToolsHeaders {\n return this.headers ?? {};\n }\n\n private activeHeader(): PanelToolsHeaderConfig {\n if (!this.activeTool) return {};\n return this.resolvedHeaders()[this.activeTool] ?? {};\n }\n\n private handleToolChange = (id: PanelToolsToolId) => {\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 /** Toggle any rail tool open/closed — shell shortcuts K/A/S/M/N call this. */\n @Method()\n async activateTool(id: PanelToolsToolId) {\n const item = this.railItems.find(entry => entry.id === id);\n if (!item || item.isInactive) return;\n this.handleToolChange(id);\n }\n\n /** Close the tools drawer when open — used by shell keyboard shortcuts. */\n @Method()\n async closeDrawer() {\n if (!this.open) return;\n const id = this.activeTool;\n this.open = false;\n if (id) {\n this.dsToolChange.emit({ id, selected: false });\n }\n }\n\n /** Focus an active tool-header action by its application-owned id. */\n @Method()\n async focusHeaderAction(id: string) {\n const action = this.renderRoot.querySelector(`[data-header-action-id=\"${CSS.escape(id)}\"]`) as\n | (HTMLElement & { setFocus?: () => Promise<void> })\n | null;\n await action?.setFocus?.();\n }\n\n private focusRailAt(index: number) {\n const keys = panelToolsRailFocusKeys(this.orderedRailEntries);\n if (!keys.length) return;\n const bounded = Math.max(0, Math.min(index, keys.length - 1));\n this.rovingIndex = bounded;\n const action = this.renderRoot.querySelector(\n `[data-rail-focus-key=\"${CSS.escape(keys[bounded])}\"]`\n ) as (HTMLElement & { setFocus?: () => Promise<void> }) | null;\n if (action?.setFocus) {\n void action.setFocus();\n } else {\n action?.focus({ preventScroll: true });\n }\n }\n\n private handleRailKeyDown = (e: KeyboardEvent, index: number, activate?: () => void) => {\n const keys = panelToolsRailFocusKeys(this.orderedRailEntries);\n if (!keys.length) return;\n\n if ((e.key === 'Enter' || e.key === ' ') && activate) {\n e.preventDefault();\n activate();\n return;\n }\n\n if (e.key === 'ArrowDown') {\n if (index >= keys.length - 1) return;\n e.preventDefault();\n this.focusRailAt(index + 1);\n return;\n }\n\n if (e.key === 'ArrowUp') {\n if (index <= 0) return;\n e.preventDefault();\n this.focusRailAt(index - 1);\n }\n };\n\n private renderRailAction(item: PanelToolsItem, index: number) {\n const focusKey = `tool:${item.id}`;\n const label =\n item.ariaLabel ??\n item.label ??\n PANEL_TOOLS_LABELS[item.id as keyof typeof PANEL_TOOLS_LABELS] ??\n item.id;\n return (\n <ds-tooltip\n key={item.id}\n label={label}\n shortcutKey={\n item.shortcutKey ?? PANEL_TOOLS_SHORTCUTS[item.id as keyof typeof PANEL_TOOLS_SHORTCUTS]\n }\n side=\"left\"\n size=\"sm\"\n boundary=\"viewport\"\n >\n <ds-button-unfilled\n variant=\"icon\"\n class=\"panel-tools__rail-action\"\n icon={item.icon}\n isActive={this.isRailSelected(item.id)}\n activeFill={false}\n hasBorder={false}\n background=\"chrome\"\n pressScale={false}\n dot={item.dot ?? false}\n isInactive={item.isInactive}\n focusTabIndex={index === this.rovingIndex ? 0 : -1}\n data-rail-focus-key={focusKey}\n aria-label={label}\n pressed={this.isRailSelected(item.id)}\n onFocusin={() => {\n this.rovingIndex = index;\n }}\n onKeyDown={(e: KeyboardEvent) =>\n this.handleRailKeyDown(e, index, () => this.handleToolChange(item.id))\n }\n onDsClick={() => this.handleToolChange(item.id)}\n />\n </ds-tooltip>\n );\n }\n\n private accessoryPrimaryLabel(accessory: PanelToolsRailTransientAccessory): string {\n const actionLabel = accessory.primaryAction.ariaLabel?.trim() || accessory.ariaLabel;\n const status = accessory.statusText.trim();\n return status ? `${actionLabel}. ${status}` : actionLabel;\n }\n\n private emitAccessoryAction(\n accessoryId: string,\n action: PanelToolsRailAccessoryAction,\n anchor: HTMLElement\n ) {\n if (action.isInactive) return;\n this.dsRailAccessoryAction.emit(\n panelToolsRailAccessoryActionDetail(accessoryId, action.id, anchor)\n );\n }\n\n private renderAccessoryVisual(accessory: PanelToolsRailTransientAccessory) {\n if (accessory.visual.type === 'image') {\n return (\n <img\n class=\"panel-tools__accessory-image\"\n src={accessory.visual.src}\n alt=\"\"\n aria-hidden=\"true\"\n />\n );\n }\n if (accessory.visual.type === 'initial') {\n const initial = [...accessory.visual.initial.trim()].slice(0, 2).join('');\n return (\n <ds-text\n class=\"panel-tools__accessory-initial\"\n as=\"span\"\n variant=\"text-caption\"\n emphasis\n color=\"inherit\"\n aria-hidden=\"true\"\n >\n {initial}\n </ds-text>\n );\n }\n return <ds-icon name={accessory.visual.icon} size=\"sm\" aria-hidden=\"true\" />;\n }\n\n private renderRailShortcut(\n accessory: PanelToolsRailShortcutAccessory,\n focusIndexes: Map<string, number>\n ) {\n const focusKey = `accessory:${accessory.id}:${accessory.action.id}`;\n const focusIndex = focusIndexes.get(focusKey);\n const label = accessory.action.ariaLabel?.trim() || accessory.ariaLabel;\n const initials = [...accessory.initials.trim()].slice(0, 2).join('');\n\n return (\n <div\n key={`accessory:${accessory.id}`}\n class=\"panel-tools__accessory panel-tools__accessory--shortcut\"\n >\n <ds-tooltip label={label} side=\"left\" size=\"sm\" boundary=\"viewport\">\n <button\n type=\"button\"\n class=\"panel-tools__shortcut ds-focus-ring-inset ds-interaction-fill ds-interaction-fill--on-chrome\"\n disabled={accessory.action.isInactive}\n tabIndex={focusIndex === this.rovingIndex ? 0 : -1}\n data-rail-focus-key={focusKey}\n aria-label={label}\n onFocus={() => {\n if (focusIndex !== undefined) this.rovingIndex = focusIndex;\n }}\n onKeyDown={(event: KeyboardEvent) =>\n focusIndex === undefined ? undefined : this.handleRailKeyDown(event, focusIndex)\n }\n onClick={(event: MouseEvent) =>\n this.emitAccessoryAction(\n accessory.id,\n accessory.action,\n event.currentTarget as HTMLElement\n )\n }\n >\n <span class=\"panel-tools__shortcut-content ds-interaction-fill__content\">\n <ds-text\n class=\"panel-tools__shortcut-orb\"\n as=\"span\"\n variant=\"text-caption\"\n emphasis\n color=\"inherit\"\n >\n {initials}\n </ds-text>\n {accessory.dot ? (\n <ds-badge\n class=\"panel-tools__shortcut-dot\"\n variant=\"dot\"\n background=\"var(--_panel-tools-shortcut-dot-ring)\"\n aria-hidden=\"true\"\n />\n ) : null}\n </span>\n </button>\n </ds-tooltip>\n </div>\n );\n }\n\n private renderRailAccessory(\n accessory: PanelToolsRailAccessory,\n focusIndexes: Map<string, number>\n ) {\n if (accessory.type === 'divider') {\n return (\n <div\n key={`accessory:${accessory.id}`}\n class=\"panel-tools__accessory-divider\"\n aria-hidden=\"true\"\n >\n <ds-divider />\n </div>\n );\n }\n\n if (accessory.type === 'shortcut') {\n return this.renderRailShortcut(accessory, focusIndexes);\n }\n\n const primaryKey = `accessory:${accessory.id}:${accessory.primaryAction.id}`;\n const primaryIndex = focusIndexes.get(primaryKey);\n const primaryLabel = this.accessoryPrimaryLabel(accessory);\n const secondary = accessory.secondaryAction;\n const secondaryKey = secondary ? `accessory:${accessory.id}:${secondary.id}` : '';\n const secondaryIndex = secondary ? focusIndexes.get(secondaryKey) : undefined;\n\n return (\n <div\n key={`accessory:${accessory.id}`}\n class={{\n 'panel-tools__accessory': true,\n 'panel-tools__accessory--transient': true,\n 'panel-tools__accessory--transient-active': accessory.statusTone === 'active',\n 'panel-tools__accessory--transient-positive': accessory.statusTone === 'positive',\n }}\n >\n <ds-tooltip label={primaryLabel} side=\"left\" size=\"sm\" boundary=\"viewport\">\n <button\n type=\"button\"\n class=\"panel-tools__accessory-primary ds-focus-ring-inset ds-interaction-fill ds-interaction-fill--on-bold\"\n disabled={accessory.primaryAction.isInactive}\n tabIndex={primaryIndex === this.rovingIndex ? 0 : -1}\n data-rail-focus-key={primaryKey}\n aria-label={primaryLabel}\n onFocus={() => {\n if (primaryIndex !== undefined) this.rovingIndex = primaryIndex;\n }}\n onKeyDown={(event: KeyboardEvent) =>\n primaryIndex === undefined ? undefined : this.handleRailKeyDown(event, primaryIndex)\n }\n onClick={(event: MouseEvent) =>\n this.emitAccessoryAction(\n accessory.id,\n accessory.primaryAction,\n event.currentTarget as HTMLElement\n )\n }\n >\n <span class=\"panel-tools__accessory-primary-content ds-interaction-fill__content\">\n {this.renderAccessoryVisual(accessory)}\n </span>\n </button>\n </ds-tooltip>\n {secondary ? (\n <ds-tooltip label={secondary.ariaLabel} side=\"left\" size=\"sm\" boundary=\"viewport\">\n <ds-button-unfilled\n class=\"panel-tools__accessory-secondary\"\n variant=\"icon\"\n size=\"sm\"\n rounded\n background=\"bold\"\n hasBorder={false}\n icon={secondary.icon}\n isInactive={secondary.isInactive}\n focusTabIndex={secondaryIndex === this.rovingIndex ? 0 : -1}\n data-rail-focus-key={secondaryKey}\n aria-label={secondary.ariaLabel}\n onFocusin={() => {\n if (secondaryIndex !== undefined) this.rovingIndex = secondaryIndex;\n }}\n onKeyDown={(event: KeyboardEvent) =>\n secondaryIndex === undefined\n ? undefined\n : this.handleRailKeyDown(event, secondaryIndex)\n }\n onDsClick={(event: CustomEvent<MouseEvent>) =>\n this.emitAccessoryAction(\n accessory.id,\n secondary,\n event.currentTarget as HTMLElement\n )\n }\n />\n </ds-tooltip>\n ) : null}\n </div>\n );\n }\n\n private renderRailEntry(entry: PanelToolsRailEntry, focusIndexes: Map<string, number>) {\n if (entry.type === 'tool') {\n return this.renderRailAction(entry.item, focusIndexes.get(`tool:${entry.id}`) ?? 0);\n }\n return this.renderRailAccessory(entry.accessory, focusIndexes);\n }\n\n render() {\n const accessibleLabel = this.accessibleLabel();\n const header = this.activeHeader();\n const headerTitle = header.title?.trim() || accessibleLabel;\n const headerActions = header.actions ?? [];\n const orderedRailItems = this.orderedRailItems;\n const orderedRailEntries = this.orderedRailEntries;\n const entryPlacement = (entry: PanelToolsRailEntry) =>\n entry.type === 'tool' ? panelToolsRailPlacement(entry.item) : entry.accessory.railPlacement;\n const headerEntries = orderedRailEntries.filter(entry => entryPlacement(entry) === 'header');\n const bodyEntries = orderedRailEntries.filter(entry => entryPlacement(entry) === 'body');\n const footerEntries = orderedRailEntries.filter(entry => entryPlacement(entry) === 'footer');\n const focusIndexes = new Map(\n panelToolsRailFocusKeys(orderedRailEntries).map((key, index) => [key, index])\n );\n const showDrawerChrome = this.isDrawerPresent();\n const drawerResting = panelToolsDrawerResting(this.open, this.motion);\n const activeFullView = Boolean(\n showDrawerChrome && this.activeTool && this.hasFullView(this.activeTool)\n );\n const toolIds = orderedRailItems.map(item => item.id);\n\n return (\n <Host\n class={{\n 'panel-tools': true,\n 'panel-tools--open': this.open,\n 'panel-tools--ready': this.readyForMotion,\n 'panel-tools--motion-opening': this.motion === 'opening',\n 'panel-tools--motion-closing': this.motion === 'closing',\n 'panel-tools--drawer-resting': drawerResting,\n 'panel-tools--fullscreen': this.presentation === 'fullscreen',\n 'panel-tools--presentation-snap': this.presentationMotionSuppressed,\n }}\n data-ds-overlay-boundary\n role={this.presentation === 'fullscreen' ? 'dialog' : 'complementary'}\n aria-modal={this.presentation === 'fullscreen' ? 'true' : undefined}\n aria-label={this.toolsLabel}\n >\n <div class=\"panel-tools__layout\">\n <nav class=\"panel-tools__rail\" aria-label={this.toolShortcutsLabel}>\n {headerEntries.length ? (\n <div\n class={{\n 'panel-tools__rail-header': true,\n 'panel-tools__rail-section--stacked': headerEntries.length > 1,\n 'ds-chrome-row': headerEntries.length === 1,\n 'ds-chrome-column': headerEntries.length > 1,\n 'ds-chrome-space--md': true,\n }}\n >\n {headerEntries.map(entry => this.renderRailEntry(entry, focusIndexes))}\n </div>\n ) : null}\n <div\n class={{\n 'panel-tools__rail-body': true,\n 'ds-chrome-column': true,\n 'ds-chrome-space--md': true,\n 'ds-scrollbar-hidden': true,\n 'ds-focus-ring': this.railBodyScrollable,\n }}\n ref={element => {\n const next = (element as HTMLElement) ?? undefined;\n if (next === this.railBodyEl) return;\n this.railBodyEl = next;\n this.connectRailBodyObserver();\n }}\n role={this.railBodyScrollable ? 'region' : undefined}\n aria-label={this.railBodyScrollable ? this.toolShortcutsLabel : undefined}\n tabIndex={this.railBodyScrollable ? 0 : undefined}\n >\n <div class=\"panel-tools__rail-actions\">\n {bodyEntries.map(entry => this.renderRailEntry(entry, focusIndexes))}\n </div>\n </div>\n {footerEntries.length ? (\n <div\n class={{\n 'panel-tools__rail-footer': true,\n 'panel-tools__rail-section--stacked': footerEntries.length > 1,\n 'ds-chrome-row': footerEntries.length === 1,\n 'ds-chrome-column': footerEntries.length > 1,\n 'ds-chrome-space--md': true,\n }}\n >\n {footerEntries.map(entry => this.renderRailEntry(entry, focusIndexes))}\n </div>\n ) : null}\n </nav>\n\n <div\n class={{\n 'panel-tools__drawer': true,\n 'panel-tools__drawer--visible': showDrawerChrome,\n }}\n aria-hidden={showDrawerChrome ? null : 'true'}\n inert={showDrawerChrome ? undefined : true}\n >\n <div class=\"panel-tools__drawer-surface\">\n {this.presentation !== 'fullscreen' || this.fullscreenHeaderMode === 'shared' ? (\n <ds-panel-tool-header\n class=\"panel-tools__header\"\n heading={headerTitle}\n showBack={header.showBack ?? false}\n backIcon={header.backIcon || 'ChevronLeft'}\n backAriaLabel={header.backAriaLabel || 'Back'}\n showMenu={false}\n ref={element => {\n const toolHeader = element as\n | (HTMLDsPanelToolHeaderElement & {\n actions: PanelToolsHeaderAction[];\n })\n | undefined;\n if (toolHeader) toolHeader.actions = headerActions;\n }}\n onDsBack={() => {\n if (this.activeTool) this.dsHeaderBack.emit({ tool: this.activeTool });\n }}\n />\n ) : null}\n <div class=\"panel-tools__body\" hidden={activeFullView}>\n {toolIds.map(id => {\n const active = this.isViewActive(id);\n return (\n <div\n class={{\n 'panel-tools__view': true,\n 'panel-tools__view--active': active,\n }}\n hidden={!active}\n >\n <slot name={id} />\n </div>\n );\n })}\n </div>\n <div class=\"panel-tools__full-views\" hidden={!activeFullView}>\n {toolIds.map(id => (\n <div\n class={{\n 'panel-tools__full-view': true,\n 'panel-tools__full-view--active':\n this.isViewActive(id) && this.hasFullView(id),\n }}\n hidden={!(this.isViewActive(id) && this.hasFullView(id))}\n >\n <slot name={`${id}-view`} />\n </div>\n ))}\n </div>\n </div>\n </div>\n </div>\n </Host>\n );\n }\n}\n",
551
551
  "type": "registry:ui"
552
552
  },
553
553
  {
@@ -119,7 +119,7 @@
119
119
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: config.",
120
120
  "peerDependencies": {
121
121
  "required": [
122
- "@ds-mo/tokens ^6.5.2",
122
+ "@ds-mo/tokens ^6.6.1",
123
123
  "@ds-mo/icons ^8.1.1"
124
124
  ],
125
125
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -47,7 +47,8 @@
47
47
  "Label the group with Field or an accessible name. Descriptions name each option separately."
48
48
  ],
49
49
  "states": [
50
- "Selection is shown by the radio indicator while the tile keeps its neutral border. Error retains the negative border, and keyboard focus keeps the focus-token outline. Inactive options cannot be selected.",
50
+ "Selection is shown by the 16px radio indicator centered in a 20px placement box while the tile keeps its neutral border. The placement box and copy have a 12px gap inside the tile's 16px padding, and the tile uses the 2px compact radius. Error retains the negative border, and keyboard focus keeps the focus-token outline. Inactive options cannot be selected.",
51
+ "Options with descriptions use primary body-medium emphasis for the title and secondary body-medium supporting text with a 4px gap. Text-only options use secondary body-medium text without emphasis.",
51
52
  "The component owns form values, required validation, reset, and disabled-fieldset behavior."
52
53
  ],
53
54
  "responsiveBehavior": [
@@ -369,7 +370,7 @@
369
370
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: options.",
370
371
  "peerDependencies": {
371
372
  "required": [
372
- "@ds-mo/tokens ^6.5.2",
373
+ "@ds-mo/tokens ^6.6.1",
373
374
  "@ds-mo/icons ^8.1.1"
374
375
  ],
375
376
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -386,12 +387,12 @@
386
387
  "files": [
387
388
  {
388
389
  "path": "src/wc/components/RadioTile/RadioTile.css",
389
- "content": "@import '../../utils/interaction-fill.css';\n@import '../../utils/control-inactive.css';\n@import '../../utils/forced-colors.css';\n:host {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-100);\n}\n:host(.radio-tiles--horizontal) {\n flex-direction: row;\n flex-wrap: wrap;\n}\n:host(.radio-tiles--horizontal) .radio-tile {\n flex-basis: var(--dimension-menu-width-xs);\n}\n.radio-tile {\n display: flex;\n position: relative;\n align-items: flex-start;\n flex: 1;\n box-sizing: border-box;\n gap: var(--dimension-space-100);\n padding: var(--dimension-space-200);\n min-width: 0;\n border-radius: var(--dimension-radius-050);\n background: var(--color-background-primary);\n cursor: pointer;\n user-select: none;\n}\n.radio-tile__outline {\n position: absolute;\n inset: 0;\n border-radius: inherit;\n box-shadow: inset 0 0 0 var(--dimension-stroke-width-012) var(--color-border-secondary);\n pointer-events: none;\n}\n:host([aria-invalid='true']) .radio-tile__outline {\n box-shadow: inset 0 0 0 var(--dimension-stroke-width-025) var(--color-border-bold-negative);\n}\n.radio-tile__input {\n position: absolute;\n inset: 0;\n opacity: 0;\n width: 100%;\n height: 100%;\n margin: 0;\n cursor: inherit;\n z-index: 3;\n}\n.radio-tile__input:disabled {\n cursor: not-allowed;\n}\n.radio-tile__input:focus-visible + .radio-tile__outline {\n outline: var(--dimension-stroke-width-025) solid var(--color-interaction-focus);\n outline-offset: var(--dimension-space-025);\n}\n.radio-tile__indicator {\n display: flex;\n align-items: center;\n justify-content: center;\n flex: none;\n box-sizing: border-box;\n width: var(--dimension-size-200);\n height: var(--dimension-size-200);\n margin-top: calc((var(--typography-lineheight-md) - var(--dimension-size-200)) / 2);\n border-radius: var(--dimension-radius-half);\n box-shadow: inset 0 0 0 var(--dimension-stroke-width-015) var(--color-foreground-tertiary);\n}\n.radio-tile--selected .radio-tile__indicator {\n background: var(--color-background-bold-brand);\n box-shadow: none;\n}\n.radio-tile__indicator > span {\n width: var(--dimension-size-100);\n height: var(--dimension-size-100);\n border-radius: var(--dimension-radius-half);\n background: var(--color-foreground-on-bold-background-primary);\n}\n.radio-tile__copy {\n display: grid;\n gap: var(--dimension-space-050);\n min-width: 0;\n padding-inline: var(--dimension-space-025);\n}\n\n@media (forced-colors: active) {\n .radio-tile__outline,\n .radio-tile__indicator {\n border: var(--dimension-stroke-width-012) solid var(--ds-forced-color-content);\n box-shadow: none;\n }\n .radio-tile--selected .radio-tile__indicator {\n background: var(--ds-forced-color-selected);\n }\n .radio-tile__indicator > span {\n background: var(--ds-forced-color-on-selected);\n }\n}\n",
390
+ "content": "@import '../../utils/interaction-fill.css';\n@import '../../utils/control-inactive.css';\n@import '../../utils/forced-colors.css';\n:host {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-100);\n}\n:host(.radio-tiles--horizontal) {\n flex-direction: row;\n flex-wrap: wrap;\n}\n:host(.radio-tiles--horizontal) .radio-tile {\n flex-basis: var(--dimension-menu-width-xs);\n}\n.radio-tile {\n display: flex;\n position: relative;\n align-items: flex-start;\n flex: 1;\n box-sizing: border-box;\n gap: var(--dimension-space-150);\n padding: var(--dimension-space-200);\n min-width: 0;\n border-radius: var(--dimension-radius-025);\n background: var(--color-background-primary);\n cursor: pointer;\n user-select: none;\n}\n.radio-tile__outline {\n position: absolute;\n inset: 0;\n border-radius: inherit;\n box-shadow: inset 0 0 0 var(--dimension-stroke-width-012) var(--color-border-secondary);\n pointer-events: none;\n}\n:host([aria-invalid='true']) .radio-tile__outline {\n box-shadow: inset 0 0 0 var(--dimension-stroke-width-025) var(--color-border-bold-negative);\n}\n.radio-tile__input {\n position: absolute;\n inset: 0;\n opacity: 0;\n width: 100%;\n height: 100%;\n margin: 0;\n cursor: inherit;\n z-index: 3;\n}\n.radio-tile__input:disabled {\n cursor: not-allowed;\n}\n.radio-tile__input:focus-visible + .radio-tile__outline {\n outline: var(--dimension-stroke-width-025) solid var(--color-interaction-focus);\n outline-offset: var(--dimension-space-025);\n}\n.radio-tile__placement {\n display: flex;\n align-items: center;\n justify-content: center;\n flex: none;\n width: var(--dimension-iconography-md);\n height: var(--dimension-iconography-md);\n}\n\n.radio-tile__indicator {\n display: flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: var(--dimension-iconography-sm);\n height: var(--dimension-iconography-sm);\n border-radius: var(--dimension-radius-half);\n box-shadow: inset 0 0 0 var(--dimension-stroke-width-015) var(--color-foreground-tertiary);\n}\n.radio-tile--selected .radio-tile__indicator {\n background: var(--color-background-bold-brand);\n box-shadow: none;\n}\n.radio-tile__indicator > span {\n width: var(--dimension-size-100);\n height: var(--dimension-size-100);\n border-radius: var(--dimension-radius-half);\n background: var(--color-foreground-on-bold-background-primary);\n}\n.radio-tile__copy {\n display: grid;\n gap: var(--dimension-space-050);\n min-width: 0;\n}\n\n@media (forced-colors: active) {\n .radio-tile__outline,\n .radio-tile__indicator {\n border: var(--dimension-stroke-width-012) solid var(--ds-forced-color-content);\n box-shadow: none;\n }\n .radio-tile--selected .radio-tile__indicator {\n background: var(--ds-forced-color-selected);\n }\n .radio-tile__indicator > span {\n background: var(--ds-forced-color-on-selected);\n }\n}\n",
390
391
  "type": "registry:ui"
391
392
  },
392
393
  {
393
394
  "path": "src/wc/components/RadioTile/RadioTile.tsx",
394
- "content": "import {\n AttachInternals,\n Component,\n Element,\n Event,\n EventEmitter,\n Prop,\n State,\n Watch,\n Method,\n h,\n Host,\n} from '@stencil/core';\nimport {\n DEFAULT_REQUIRED_MESSAGE,\n restoreStringFormState,\n setFormControlValue,\n setRequiredValidity,\n} from '../../utils';\n\nexport interface RadioTileOption {\n value: string;\n label: string;\n description?: string;\n isInactive?: boolean;\n}\nlet tileId = 0;\n\n@Component({ tag: 'ds-radio-tile', styleUrl: 'RadioTile.css', scoped: true, formAssociated: true })\nexport class RadioTile {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n /** One-of-many choices displayed as fully clickable tiles. */\n @Prop() options: RadioTileOption[] = [];\n @Prop({ mutable: true }) value: string = '';\n @Prop({ reflect: true }) name: string | undefined;\n @Prop({ reflect: true }) form: string | undefined;\n @Prop({ reflect: true }) disabled: boolean = false;\n @Prop() isInactive: boolean = false;\n @Prop({ reflect: true }) required: boolean = false;\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n @Prop() direction: 'horizontal' | 'vertical' = 'vertical';\n @Prop() inputId: string | undefined;\n @Prop() error: boolean = false;\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;\n @Prop({ attribute: 'aria-describedby' }) ariaDescribedby: string | undefined;\n @Event() dsChange!: EventEmitter<string>;\n @State() private formDisabled = false;\n private readonly generatedId = `ds-radio-tile-${++tileId}`;\n private initialValue = '';\n private get inactive() {\n return this.disabled || this.isInactive || this.formDisabled;\n }\n componentWillLoad() {\n this.initialValue = this.value;\n this.syncFormValue();\n }\n @Watch('value')\n @Watch('options')\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('required')\n @Watch('requiredMessage')\n syncFormValue() {\n const selected = this.options.some(option => option.value === this.value && !option.isInactive);\n setFormControlValue(this.internals, selected ? this.value : '', { inactive: this.inactive });\n setRequiredValidity(\n this.internals,\n this.required && !this.inactive && !selected,\n this.requiredMessage\n );\n }\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n formResetCallback() {\n this.value = this.initialValue;\n }\n formStateRestoreCallback(state: string | File | FormData | null) {\n this.value = restoreStringFormState(state);\n }\n @Method()\n async setFocus() {\n (\n this.el.querySelector<HTMLInputElement>('input:checked:not(:disabled)') ??\n this.el.querySelector<HTMLInputElement>('input:not(:disabled)')\n )?.focus();\n }\n private select(value: string) {\n if (this.inactive || value === this.value) return;\n this.value = value;\n this.dsChange.emit(value);\n }\n render() {\n return (\n <Host\n role=\"radiogroup\"\n aria-label={this.ariaLabel}\n aria-labelledby={this.ariaLabelledby}\n aria-describedby={this.ariaDescribedby}\n aria-required={this.required ? 'true' : undefined}\n aria-invalid={this.error ? 'true' : undefined}\n class={{ 'radio-tiles': true, 'radio-tiles--horizontal': this.direction === 'horizontal' }}\n >\n {this.options.map((option, index) => {\n const selected = option.value === this.value;\n const inactive = this.inactive || Boolean(option.isInactive);\n return (\n <label\n class={{\n 'radio-tile': true,\n 'radio-tile--selected': selected,\n 'ds-interaction-fill': !inactive,\n 'ds-control-inactive': inactive,\n }}\n key={option.value}\n >\n <input\n type=\"radio\"\n class=\"radio-tile__input\"\n id={index === 0 ? this.inputId : undefined}\n name={this.generatedId}\n form=\"\"\n value={option.value}\n checked={selected}\n disabled={inactive}\n aria-labelledby={`${this.generatedId}-label-${index}`}\n aria-describedby={\n option.description ? `${this.generatedId}-description-${index}` : undefined\n }\n onChange={() => this.select(option.value)}\n />\n <span class=\"radio-tile__outline\" aria-hidden=\"true\" />\n <span class=\"radio-tile__indicator ds-interaction-fill__content\" aria-hidden=\"true\">\n {selected && <span />}\n </span>\n <span class=\"radio-tile__copy ds-interaction-fill__content\">\n <ds-text\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis\n textId={`${this.generatedId}-label-${index}`}\n >\n {option.label}\n </ds-text>\n {option.description && (\n <ds-text\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n textId={`${this.generatedId}-description-${index}`}\n >\n {option.description}\n </ds-text>\n )}\n </span>\n </label>\n );\n })}\n </Host>\n );\n }\n}\n",
395
+ "content": "import {\n AttachInternals,\n Component,\n Element,\n Event,\n EventEmitter,\n Prop,\n State,\n Watch,\n Method,\n h,\n Host,\n} from '@stencil/core';\nimport {\n DEFAULT_REQUIRED_MESSAGE,\n restoreStringFormState,\n setFormControlValue,\n setRequiredValidity,\n} from '../../utils';\n\nexport interface RadioTileOption {\n value: string;\n label: string;\n description?: string;\n isInactive?: boolean;\n}\nlet tileId = 0;\n\n@Component({ tag: 'ds-radio-tile', styleUrl: 'RadioTile.css', scoped: true, formAssociated: true })\nexport class RadioTile {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n /** One-of-many choices displayed as fully clickable tiles. */\n @Prop() options: RadioTileOption[] = [];\n @Prop({ mutable: true }) value: string = '';\n @Prop({ reflect: true }) name: string | undefined;\n @Prop({ reflect: true }) form: string | undefined;\n @Prop({ reflect: true }) disabled: boolean = false;\n @Prop() isInactive: boolean = false;\n @Prop({ reflect: true }) required: boolean = false;\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n @Prop() direction: 'horizontal' | 'vertical' = 'vertical';\n @Prop() inputId: string | undefined;\n @Prop() error: boolean = false;\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;\n @Prop({ attribute: 'aria-describedby' }) ariaDescribedby: string | undefined;\n @Event() dsChange!: EventEmitter<string>;\n @State() private formDisabled = false;\n private readonly generatedId = `ds-radio-tile-${++tileId}`;\n private initialValue = '';\n private get inactive() {\n return this.disabled || this.isInactive || this.formDisabled;\n }\n componentWillLoad() {\n this.initialValue = this.value;\n this.syncFormValue();\n }\n @Watch('value')\n @Watch('options')\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('required')\n @Watch('requiredMessage')\n syncFormValue() {\n const selected = this.options.some(option => option.value === this.value && !option.isInactive);\n setFormControlValue(this.internals, selected ? this.value : '', { inactive: this.inactive });\n setRequiredValidity(\n this.internals,\n this.required && !this.inactive && !selected,\n this.requiredMessage\n );\n }\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n formResetCallback() {\n this.value = this.initialValue;\n }\n formStateRestoreCallback(state: string | File | FormData | null) {\n this.value = restoreStringFormState(state);\n }\n @Method()\n async setFocus() {\n (\n this.el.querySelector<HTMLInputElement>('input:checked:not(:disabled)') ??\n this.el.querySelector<HTMLInputElement>('input:not(:disabled)')\n )?.focus();\n }\n private select(value: string) {\n if (this.inactive || value === this.value) return;\n this.value = value;\n this.dsChange.emit(value);\n }\n render() {\n return (\n <Host\n role=\"radiogroup\"\n aria-label={this.ariaLabel}\n aria-labelledby={this.ariaLabelledby}\n aria-describedby={this.ariaDescribedby}\n aria-required={this.required ? 'true' : undefined}\n aria-invalid={this.error ? 'true' : undefined}\n class={{ 'radio-tiles': true, 'radio-tiles--horizontal': this.direction === 'horizontal' }}\n >\n {this.options.map((option, index) => {\n const selected = option.value === this.value;\n const inactive = this.inactive || Boolean(option.isInactive);\n return (\n <label\n class={{\n 'radio-tile': true,\n 'radio-tile--selected': selected,\n 'ds-interaction-fill': !inactive,\n 'ds-control-inactive': inactive,\n }}\n key={option.value}\n >\n <input\n type=\"radio\"\n class=\"radio-tile__input\"\n id={index === 0 ? this.inputId : undefined}\n name={this.generatedId}\n form=\"\"\n value={option.value}\n checked={selected}\n disabled={inactive}\n aria-labelledby={`${this.generatedId}-label-${index}`}\n aria-describedby={\n option.description ? `${this.generatedId}-description-${index}` : undefined\n }\n onChange={() => this.select(option.value)}\n />\n <span class=\"radio-tile__outline\" aria-hidden=\"true\" />\n <span class=\"radio-tile__placement ds-interaction-fill__content\" aria-hidden=\"true\">\n <span class=\"radio-tile__indicator\">{selected && <span />}</span>\n </span>\n <span class=\"radio-tile__copy ds-interaction-fill__content\">\n <ds-text\n as=\"span\"\n variant=\"text-body-medium\"\n color={option.description ? 'primary' : 'secondary'}\n emphasis={Boolean(option.description)}\n textId={`${this.generatedId}-label-${index}`}\n >\n {option.label}\n </ds-text>\n {option.description && (\n <ds-text\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n textId={`${this.generatedId}-description-${index}`}\n >\n {option.description}\n </ds-text>\n )}\n </span>\n </label>\n );\n })}\n </Host>\n );\n }\n}\n",
395
396
  "type": "registry:ui"
396
397
  }
397
398
  ]
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
3
  "name": "radio",
4
4
  "title": "Radio",
5
- "description": "Form-associated one-of-many radio selection set with vertical or horizontal layout, optional per-option descriptions, four densities, per-option inactive state, and roving keyboard focus.",
5
+ "description": "Form-associated one-of-many radio selection set with an optional visible group label, vertical or horizontal layout, per-option descriptions, four densities, per-option inactive state, and roving keyboard focus.",
6
6
  "type": "registry:ui",
7
7
  "meta": {
8
8
  "library": "@ds-mo/ui",
@@ -13,7 +13,7 @@
13
13
  "intent": {
14
14
  "audience": "general",
15
15
  "status": "stable",
16
- "summary": "Form-associated one-of-many radio selection set with vertical or horizontal layout, optional per-option descriptions, four densities, per-option inactive state, and roving keyboard focus.",
16
+ "summary": "Form-associated one-of-many radio selection set with an optional visible group label, vertical or horizontal layout, per-option descriptions, four densities, per-option inactive state, and roving keyboard focus.",
17
17
  "useWhen": [
18
18
  "A user must select exactly one visible option from a small set.",
19
19
  "All available choices should remain visible for comparison.",
@@ -46,7 +46,9 @@
46
46
  "component:ds-text"
47
47
  ],
48
48
  "accessibility": [
49
- "The host exposes radiogroup semantics and requires an accessible group name from aria-label or aria-labelledby.",
49
+ "The host exposes radiogroup semantics and requires an accessible group name from the visible label, aria-label, or aria-labelledby.",
50
+ "When groupLabel is provided, Radio renders it above the options and uses the same text as the radiogroup's accessible name.",
51
+ "Inside ds-setting-row-radio the visible form groupLabel is omitted on first paint; the row heading is the accessible name via aria-labelledby. Independent multi-select settings use ds-setting-row-checkbox with ds-checkbox-group instead.",
50
52
  "Each visible option exposes radio semantics, its checked state, its label text, optional supporting description, and any inactive state.",
51
53
  "Arrow keys move and select through active options; Home and End select the first and last active options; Space and Enter select the focused option.",
52
54
  "Roving tabindex keeps one active option in the tab order, preferring the selected option; setFocus uses that same public focus target."
@@ -56,7 +58,10 @@
56
58
  "Selected circles remove the border, use a brand fill, and draw a centered on-bold foreground dot sized 10px at lg, 8px at md, 6px at sm, and 4px at xs.",
57
59
  "Sizes are lg with a 20px circle in a 24px placement, md with a 16px circle in a 20px placement, sm with a 12px circle in a 16px placement, and xs with an 8px circle in a 12px placement.",
58
60
  "The initial value is restored by native form reset; the selected value submits under name and a required set is invalid until one active option is selected.",
59
- "Description-rich options grow beyond the fixed density height while retaining its minimum target size and primitive-owned selection anatomy."
61
+ "Description-rich options grow beyond the fixed density height while retaining its minimum target size and primitive-owned selection anatomy.",
62
+ "A visible group label uses the same density height and horizontal text origin as a menu section heading above the option list without adding a second interaction target. Copy uses text-body-small emphasis.",
63
+ "Vertical form options use a 4px gap; a medium single-line option and its group label each use the shared 32px control height.",
64
+ "Form and settings usage omits a row hover or pressed wash by default; set hasInteractionFill only when each radio option is presented as a choice-list row."
60
65
  ],
61
66
  "responsiveBehavior": [
62
67
  "Choose size from the density of the containing form or settings row.",
@@ -115,7 +120,8 @@
115
120
  ],
116
121
  "reason": "The component owns its surface and interaction treatment; use its semantic presentation props or compose an application-owned surrounding surface.",
117
122
  "props": [
118
- "size"
123
+ "size",
124
+ "hasInteractionFill"
119
125
  ]
120
126
  }
121
127
  ],
@@ -167,6 +173,24 @@
167
173
  "mutable": false,
168
174
  "description": "Associates the radio set with a form by id when rendered outside that form."
169
175
  },
176
+ "groupLabel": {
177
+ "type": "string",
178
+ "resolvedType": "string",
179
+ "attribute": "group-label",
180
+ "default": "''",
181
+ "required": false,
182
+ "mutable": false,
183
+ "description": "Optional visible group label rendered above the options."
184
+ },
185
+ "hasInteractionFill": {
186
+ "type": "boolean",
187
+ "resolvedType": "boolean",
188
+ "attribute": "has-interaction-fill",
189
+ "default": "false",
190
+ "required": false,
191
+ "mutable": false,
192
+ "description": "Show full-row hover and pressed feedback for choice-list presentations."
193
+ },
170
194
  "isInactive": {
171
195
  "type": "boolean",
172
196
  "resolvedType": "boolean",
@@ -292,6 +316,24 @@
292
316
  "mutable": false,
293
317
  "description": "Associates the radio set with a form by id when rendered outside that form."
294
318
  },
319
+ "groupLabel": {
320
+ "type": "string",
321
+ "resolvedType": "string",
322
+ "attribute": "group-label",
323
+ "default": "''",
324
+ "required": false,
325
+ "mutable": false,
326
+ "description": "Optional visible group label rendered above the options."
327
+ },
328
+ "hasInteractionFill": {
329
+ "type": "boolean",
330
+ "resolvedType": "boolean",
331
+ "attribute": "has-interaction-fill",
332
+ "default": "false",
333
+ "required": false,
334
+ "mutable": false,
335
+ "description": "Show full-row hover and pressed feedback for choice-list presentations."
336
+ },
295
337
  "isInactive": {
296
338
  "type": "boolean",
297
339
  "resolvedType": "boolean",
@@ -400,7 +442,7 @@
400
442
  "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: options.",
401
443
  "peerDependencies": {
402
444
  "required": [
403
- "@ds-mo/tokens ^6.5.2",
445
+ "@ds-mo/tokens ^6.6.1",
404
446
  "@ds-mo/icons ^8.1.1"
405
447
  ],
406
448
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
@@ -417,12 +459,12 @@
417
459
  "files": [
418
460
  {
419
461
  "path": "src/wc/components/Radio/Radio.css",
420
- "content": "@import '../../utils/control-inactive.css';\n@import '../../utils/radius.css';\n@import '../../utils/control-density.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n@import '../../utils/forced-colors.css';\n\n:host {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-025);\n}\n\n:host(.radio--horizontal) {\n flex-direction: row;\n gap: var(--dimension-space-100);\n}\n\n:host(.radio--lg) {\n --_radio-circle: var(--dimension-iconography-md);\n --_radio-dot: calc(var(--dimension-size-base) * 5 / 4);\n --_radio-stroke: var(--dimension-stroke-width-018);\n}\n\n:host(.radio--md) {\n --_radio-circle: var(--dimension-iconography-sm);\n --_radio-dot: var(--dimension-size-100);\n --_radio-stroke: var(--dimension-stroke-width-015);\n}\n\n:host(.radio--sm) {\n --_radio-circle: var(--dimension-iconography-xs);\n --_radio-dot: var(--dimension-size-075);\n --_radio-stroke: var(--dimension-stroke-width-012);\n}\n\n:host(.radio--xs) {\n --_radio-circle: var(--dimension-size-100);\n --_radio-dot: var(--dimension-size-050);\n --_radio-stroke: calc(var(--dimension-stroke-width-base) * 3 / 32);\n}\n\n.radio__item {\n display: inline-flex;\n align-items: center;\n box-sizing: border-box;\n gap: var(--ds-control-gap);\n height: var(--ds-control-height);\n padding: 0 var(--ds-control-padding-inline);\n border-radius: var(--ds-control-radius);\n cursor: default;\n user-select: none;\n}\n\n.radio__item--described {\n height: auto;\n min-height: var(--ds-control-height);\n padding-block: var(--dimension-space-075);\n}\n\n.radio__placement {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--ds-control-icon);\n height: var(--ds-control-icon);\n}\n\n.radio__circle {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: var(--_radio-circle);\n height: var(--_radio-circle);\n border: 0;\n border-radius: var(--dimension-radius-half);\n box-shadow: inset 0 0 0 var(--_radio-stroke) var(--color-foreground-tertiary);\n transition:\n background-color var(--effect-motion-short-2),\n box-shadow var(--effect-motion-short-2);\n}\n\n.radio__circle--checked {\n background-color: var(--color-background-bold-brand);\n box-shadow: none;\n}\n\n.radio__dot {\n width: var(--_radio-dot);\n height: var(--_radio-dot);\n border-radius: var(--dimension-radius-half);\n background-color: var(--color-foreground-on-bold-background-primary);\n}\n\n.radio__copy {\n display: grid;\n flex: 1;\n gap: var(--dimension-space-025);\n min-width: 0;\n padding: 0 var(--ds-control-label-inset);\n}\n\n@media (forced-colors: active) {\n .radio__circle {\n border: var(--_radio-stroke) solid var(--ds-forced-color-content);\n box-shadow: none;\n }\n\n .radio__circle--checked {\n background-color: var(--ds-forced-color-selected);\n border-color: var(--ds-forced-color-selected);\n }\n\n .radio__dot {\n background-color: var(--ds-forced-color-on-selected);\n }\n}\n",
462
+ "content": "@import '../../utils/control-inactive.css';\n@import '../../utils/radius.css';\n@import '../../utils/control-density.css';\n@import '../../utils/control-parts.css';\n@import '../../utils/focus-ring.css';\n@import '../../utils/interaction-fill.css';\n@import '../../utils/forced-colors.css';\n\n:host {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050);\n}\n\n:host(.radio--labeled) {\n gap: var(--dimension-space-050);\n}\n\n.radio__options {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-050);\n min-width: 0;\n}\n\n:host(.radio--horizontal) {\n flex-direction: row;\n gap: var(--dimension-space-100);\n}\n\n:host(.radio--horizontal) .radio__options {\n flex-direction: row;\n gap: var(--dimension-space-100);\n}\n\n:host(.radio--labeled.radio--horizontal) {\n flex-direction: column;\n gap: var(--dimension-space-050);\n}\n\n:host(.radio--labeled.radio--horizontal) .radio__options {\n flex-direction: row;\n gap: var(--dimension-space-100);\n}\n\n:host(.radio--lg) {\n --_radio-circle-inset: calc(var(--dimension-space-100) + var(--dimension-space-025));\n --_radio-circle: var(--dimension-iconography-md);\n --_radio-dot: calc(var(--dimension-size-base) * 5 / 4);\n --_radio-stroke: var(--dimension-stroke-width-018);\n}\n\n:host(.radio--md) {\n --_radio-circle-inset: var(--dimension-space-100);\n --_radio-circle: var(--dimension-iconography-sm);\n --_radio-dot: var(--dimension-size-100);\n --_radio-stroke: var(--dimension-stroke-width-015);\n}\n\n:host(.radio--sm) {\n --_radio-circle-inset: var(--dimension-space-075);\n --_radio-circle: var(--dimension-iconography-xs);\n --_radio-dot: var(--dimension-size-075);\n --_radio-stroke: var(--dimension-stroke-width-012);\n}\n\n:host(.radio--xs) {\n --_radio-circle-inset: var(--dimension-space-050);\n --_radio-circle: var(--dimension-size-100);\n --_radio-dot: var(--dimension-size-050);\n --_radio-stroke: calc(var(--dimension-stroke-width-base) * 3 / 32);\n}\n\n.radio__item {\n display: inline-flex;\n align-items: center;\n box-sizing: border-box;\n gap: var(--ds-control-gap);\n height: var(--ds-control-height);\n padding: 0 var(--ds-settings-row-control-padding-inline, var(--ds-control-padding-inline));\n border-radius: var(--ds-control-radius);\n cursor: default;\n user-select: none;\n}\n\n.radio__item--described {\n align-items: flex-start;\n height: auto;\n min-height: var(--ds-control-height);\n padding-block: var(--dimension-space-075);\n}\n\n.radio__placement {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--ds-control-icon);\n height: var(--ds-control-icon);\n}\n\n.radio__circle {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n box-sizing: border-box;\n width: var(--_radio-circle);\n height: var(--_radio-circle);\n border: 0;\n border-radius: var(--dimension-radius-half);\n box-shadow: inset 0 0 0 var(--_radio-stroke) var(--color-foreground-tertiary);\n transition:\n background-color var(--effect-motion-short-2),\n box-shadow var(--effect-motion-short-2);\n}\n\n.radio__circle--checked {\n background-color: var(--color-background-bold-brand);\n box-shadow: none;\n}\n\n.radio__dot {\n width: var(--_radio-dot);\n height: var(--_radio-dot);\n border-radius: var(--dimension-radius-half);\n background-color: var(--color-foreground-on-bold-background-primary);\n}\n\nds-text.radio__group-label {\n padding-inline: var(\n --ds-settings-row-control-padding-inline,\n calc(var(--ds-control-padding-inline) + var(--ds-control-label-inset))\n );\n}\n\n.radio__copy {\n display: grid;\n flex: 1;\n gap: var(--dimension-space-025);\n min-width: 0;\n padding: 0 var(--ds-control-label-inset);\n}\n\n@media (forced-colors: active) {\n .radio__circle {\n border: var(--_radio-stroke) solid var(--ds-forced-color-content);\n box-shadow: none;\n }\n\n .radio__circle--checked {\n background-color: var(--ds-forced-color-selected);\n border-color: var(--ds-forced-color-selected);\n }\n\n .radio__dot {\n background-color: var(--ds-forced-color-on-selected);\n }\n}\n",
421
463
  "type": "registry:ui"
422
464
  },
423
465
  {
424
466
  "path": "src/wc/components/Radio/Radio.tsx",
425
- "content": "import {\n AttachInternals,\n Component,\n Prop,\n State,\n Event,\n EventEmitter,\n Element,\n Listen,\n Method,\n Watch,\n h,\n Host,\n} from '@stencil/core';\nimport {\n CONTROL_SUPPORTING_TEXT_VARIANT,\n CONTROL_TEXT_VARIANT,\n DEFAULT_REQUIRED_MESSAGE,\n restoreStringFormState,\n setFormControlValue,\n setRequiredValidity,\n} from '../../utils';\n\nexport interface RadioOption {\n label: string;\n value: string;\n description?: string;\n isInactive?: boolean;\n}\n\nexport type RadioSize = 'lg' | 'md' | 'sm' | 'xs';\n\nlet radioIdCounter = 0;\n\n@Component({\n tag: 'ds-radio',\n styleUrl: 'Radio.css',\n scoped: true,\n formAssociated: true,\n})\nexport class Radio {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n private readonly instanceId = ++radioIdCounter;\n\n /** Visible choices in this one-of-many set. */\n @Prop() options: RadioOption[] = [];\n /** Selected option value. */\n @Prop({ mutable: true }) value: string = '';\n /** Visual and placement density for every option. */\n @Prop() size: RadioSize = 'md';\n /** Native form field name. */\n @Prop({ reflect: true }) name: string | undefined;\n /** Associates the radio set with a form by id when rendered outside that form. */\n @Prop({ reflect: true }) form: string | undefined;\n /** Native disabled state for the complete set. */\n @Prop({ reflect: true }) disabled: boolean = false;\n /** Require one option to be selected for form validity. */\n @Prop({ reflect: true }) required: boolean = false;\n /** Validation message used when a required set has no selection. */\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n /** Layout direction for the option set. */\n @Prop() direction: 'vertical' | 'horizontal' = 'vertical';\n /** Design-system inactive state for the complete set. */\n @Prop() isInactive: boolean = false;\n /** Accessible name when visible group labeling is unavailable. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n /** Id reference for a visible group label. */\n @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;\n\n /** Emitted after user selection with the selected option value. */\n @Event() dsChange!: EventEmitter<string>;\n\n private initialValue = '';\n @State() private formDisabled = false;\n\n componentWillLoad() {\n this.initialValue = this.value;\n this.syncFormValue();\n }\n\n @Watch('value')\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('required')\n syncFormValue() {\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n setFormControlValue(this.internals, this.value, { inactive });\n const missing = this.required && !inactive && this.value.length === 0;\n setRequiredValidity(this.internals, missing, this.requiredMessage);\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.value = this.initialValue;\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n this.value = restoreStringFormState(state);\n }\n\n /** Focus the selected option, or the first active option when nothing is selected. */\n @Method()\n async setFocus() {\n this.activeItems.find(item => item.tabIndex === 0)?.focus();\n }\n\n private get activeItems(): HTMLElement[] {\n return Array.from(\n this.el.querySelectorAll<HTMLElement>('[data-radio-item]:not([data-inactive])')\n );\n }\n\n @Listen('keydown')\n handleKeyDown(e: KeyboardEvent) {\n const items = this.activeItems;\n if (!items.length) return;\n\n const focused = items.find(item => item === document.activeElement);\n if (!focused) return;\n\n const idx = items.indexOf(focused);\n let next = -1;\n\n switch (e.key) {\n case 'ArrowDown':\n case 'ArrowRight':\n e.preventDefault();\n next = (idx + 1) % items.length;\n break;\n case 'ArrowUp':\n case 'ArrowLeft':\n e.preventDefault();\n next = (idx - 1 + items.length) % items.length;\n break;\n case 'Home':\n e.preventDefault();\n next = 0;\n break;\n case 'End':\n e.preventDefault();\n next = items.length - 1;\n break;\n }\n\n if (next >= 0) {\n items[next].focus();\n const nextValue = items[next].dataset['value'];\n if (nextValue && nextValue !== this.value) {\n this.value = nextValue;\n this.dsChange.emit(nextValue);\n }\n }\n }\n\n private selectItem(optValue: string) {\n if (this.isInactive || this.disabled || this.formDisabled) return;\n if (optValue !== this.value) {\n this.value = optValue;\n this.dsChange.emit(optValue);\n }\n }\n\n render() {\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n const invalid = this.required && !inactive && this.value.length === 0;\n const selectedIdx = this.options.findIndex(option => option.value === this.value);\n const firstActiveIdx = this.options.findIndex(option => !inactive && !option.isInactive);\n const focusableIdx =\n selectedIdx >= 0 && !inactive && !this.options[selectedIdx]?.isInactive\n ? selectedIdx\n : firstActiveIdx;\n\n return (\n <Host\n role=\"radiogroup\"\n aria-label={this.ariaLabel}\n aria-labelledby={this.ariaLabelledby}\n aria-required={this.required ? 'true' : undefined}\n aria-invalid={invalid ? 'true' : undefined}\n class={{\n radio: true,\n 'radio--horizontal': this.direction === 'horizontal',\n [`radio--${this.size}`]: true,\n }}\n >\n {this.options.map((option, index) => {\n const isItemInactive = inactive || !!option.isInactive;\n const isChecked = option.value === this.value;\n const tabIdx = isItemInactive ? -1 : index === focusableIdx ? 0 : -1;\n const labelId = `ds-radio-${this.instanceId}-label-${index}`;\n const descriptionId = `ds-radio-${this.instanceId}-description-${index}`;\n\n return (\n <div\n key={option.value}\n role=\"radio\"\n aria-checked={String(isChecked)}\n aria-disabled={isItemInactive ? 'true' : undefined}\n aria-labelledby={labelId}\n aria-describedby={option.description ? descriptionId : undefined}\n tabIndex={tabIdx}\n data-radio-item\n data-value={option.value}\n data-inactive={isItemInactive || undefined}\n class={{\n radio__item: true,\n 'radio__item--described': Boolean(option.description),\n [`ds-control--${this.size}`]: true,\n 'ds-control-inactive': isItemInactive,\n 'ds-focus-ring-inset': !isItemInactive,\n 'ds-interaction-fill': !isItemInactive,\n }}\n onClick={() => !isItemInactive && this.selectItem(option.value)}\n onKeyDown={(e: KeyboardEvent) => {\n if ((e.key === ' ' || e.key === 'Enter') && !isItemInactive) {\n e.preventDefault();\n this.selectItem(option.value);\n }\n }}\n >\n <span class=\"radio__placement ds-interaction-fill__content\" aria-hidden=\"true\">\n <span class={{ radio__circle: true, 'radio__circle--checked': isChecked }}>\n {isChecked && <span class=\"radio__dot\" />}\n </span>\n </span>\n <span class=\"radio__copy ds-interaction-fill__content\">\n <ds-text\n class=\"radio__label\"\n as=\"span\"\n variant={CONTROL_TEXT_VARIANT[this.size]}\n textId={labelId}\n >\n {option.label}\n </ds-text>\n {option.description ? (\n <ds-text\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n color=\"secondary\"\n textId={descriptionId}\n >\n {option.description}\n </ds-text>\n ) : null}\n </span>\n </div>\n );\n })}\n </Host>\n );\n }\n}\n",
467
+ "content": "import {\n AttachInternals,\n Component,\n Prop,\n State,\n Event,\n EventEmitter,\n Element,\n Listen,\n Method,\n Watch,\n h,\n Host,\n} from '@stencil/core';\nimport {\n CONTROL_SUPPORTING_TEXT_VARIANT,\n CONTROL_TEXT_VARIANT,\n DEFAULT_REQUIRED_MESSAGE,\n restoreStringFormState,\n setFormControlValue,\n setRequiredValidity,\n} from '../../utils';\n\nexport interface RadioOption {\n label: string;\n value: string;\n description?: string;\n isInactive?: boolean;\n}\n\nexport type RadioSize = 'lg' | 'md' | 'sm' | 'xs';\n\nlet radioIdCounter = 0;\n\n@Component({\n tag: 'ds-radio',\n styleUrl: 'Radio.css',\n scoped: true,\n formAssociated: true,\n})\nexport class Radio {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n private readonly instanceId = ++radioIdCounter;\n\n /** Visible choices in this one-of-many set. */\n @Prop() options: RadioOption[] = [];\n /** Optional visible group label rendered above the options. */\n @Prop() groupLabel: string = '';\n /** Selected option value. */\n @Prop({ mutable: true }) value: string = '';\n /** Visual and placement density for every option. */\n @Prop() size: RadioSize = 'md';\n /** Native form field name. */\n @Prop({ reflect: true }) name: string | undefined;\n /** Associates the radio set with a form by id when rendered outside that form. */\n @Prop({ reflect: true }) form: string | undefined;\n /** Native disabled state for the complete set. */\n @Prop({ reflect: true }) disabled: boolean = false;\n /** Require one option to be selected for form validity. */\n @Prop({ reflect: true }) required: boolean = false;\n /** Validation message used when a required set has no selection. */\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n /** Layout direction for the option set. */\n @Prop() direction: 'vertical' | 'horizontal' = 'vertical';\n /** Design-system inactive state for the complete set. */\n @Prop() isInactive: boolean = false;\n /** Show full-row hover and pressed feedback for choice-list presentations. */\n @Prop() hasInteractionFill: boolean = false;\n /** Accessible name when visible group labeling is unavailable. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n /** Id reference for a visible group label. */\n @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;\n\n /** Emitted after user selection with the selected option value. */\n @Event() dsChange!: EventEmitter<string>;\n\n private initialValue = '';\n @State() private formDisabled = false;\n\n private get hasGroupLabel(): boolean {\n return Boolean(this.groupLabel.trim());\n }\n\n /** Settings rows own a separate heading; never mount the form group label there. */\n private get showGroupLabel(): boolean {\n return this.hasGroupLabel && !this.el.closest('ds-setting-row-radio');\n }\n\n componentWillLoad() {\n this.initialValue = this.value;\n this.syncFormValue();\n }\n\n @Watch('value')\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('required')\n syncFormValue() {\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n setFormControlValue(this.internals, this.value, { inactive });\n const missing = this.required && !inactive && this.value.length === 0;\n setRequiredValidity(this.internals, missing, this.requiredMessage);\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.value = this.initialValue;\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n this.value = restoreStringFormState(state);\n }\n\n /** Focus the selected option, or the first active option when nothing is selected. */\n @Method()\n async setFocus() {\n this.activeItems.find(item => item.tabIndex === 0)?.focus();\n }\n\n private get activeItems(): HTMLElement[] {\n return Array.from(\n this.el.querySelectorAll<HTMLElement>('[data-radio-item]:not([data-inactive])')\n );\n }\n\n @Listen('keydown')\n handleKeyDown(e: KeyboardEvent) {\n const items = this.activeItems;\n if (!items.length) return;\n\n const focused = items.find(item => item === document.activeElement);\n if (!focused) return;\n\n const idx = items.indexOf(focused);\n let next = -1;\n\n switch (e.key) {\n case 'ArrowDown':\n case 'ArrowRight':\n e.preventDefault();\n next = (idx + 1) % items.length;\n break;\n case 'ArrowUp':\n case 'ArrowLeft':\n e.preventDefault();\n next = (idx - 1 + items.length) % items.length;\n break;\n case 'Home':\n e.preventDefault();\n next = 0;\n break;\n case 'End':\n e.preventDefault();\n next = items.length - 1;\n break;\n }\n\n if (next >= 0) {\n items[next].focus();\n const nextValue = items[next].dataset['value'];\n if (nextValue && nextValue !== this.value) {\n this.value = nextValue;\n this.dsChange.emit(nextValue);\n }\n }\n }\n\n private selectItem(optValue: string) {\n if (this.isInactive || this.disabled || this.formDisabled) return;\n if (optValue !== this.value) {\n this.value = optValue;\n this.dsChange.emit(optValue);\n }\n }\n\n private renderOptions(inactive: boolean, focusableIdx: number) {\n return this.options.map((option, index) => {\n const isItemInactive = inactive || !!option.isInactive;\n const isChecked = option.value === this.value;\n const tabIdx = isItemInactive ? -1 : index === focusableIdx ? 0 : -1;\n const labelId = `ds-radio-${this.instanceId}-label-${index}`;\n const descriptionId = `ds-radio-${this.instanceId}-description-${index}`;\n\n return (\n <div\n key={option.value}\n role=\"radio\"\n aria-checked={String(isChecked)}\n aria-disabled={isItemInactive ? 'true' : undefined}\n aria-labelledby={labelId}\n aria-describedby={option.description ? descriptionId : undefined}\n tabIndex={tabIdx}\n data-radio-item\n data-value={option.value}\n data-inactive={isItemInactive || undefined}\n class={{\n radio__item: true,\n 'radio__item--described': Boolean(option.description),\n [`ds-control--${this.size}`]: true,\n 'ds-control-inactive': isItemInactive,\n 'ds-focus-ring-inset': !isItemInactive,\n 'ds-interaction-fill': !isItemInactive && this.hasInteractionFill,\n }}\n onClick={() => !isItemInactive && this.selectItem(option.value)}\n onKeyDown={(e: KeyboardEvent) => {\n if ((e.key === ' ' || e.key === 'Enter') && !isItemInactive) {\n e.preventDefault();\n this.selectItem(option.value);\n }\n }}\n >\n <span class=\"radio__placement ds-interaction-fill__content\" aria-hidden=\"true\">\n <span class={{ radio__circle: true, 'radio__circle--checked': isChecked }}>\n {isChecked && <span class=\"radio__dot\" />}\n </span>\n </span>\n <span class=\"radio__copy ds-interaction-fill__content\">\n <ds-text\n class=\"radio__label\"\n as=\"span\"\n variant={CONTROL_TEXT_VARIANT[this.size]}\n textId={labelId}\n >\n {option.label}\n </ds-text>\n {option.description ? (\n <ds-text\n as=\"span\"\n variant={CONTROL_SUPPORTING_TEXT_VARIANT[this.size]}\n color=\"secondary\"\n textId={descriptionId}\n >\n {option.description}\n </ds-text>\n ) : null}\n </span>\n </div>\n );\n });\n }\n\n render() {\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n const invalid = this.required && !inactive && this.value.length === 0;\n const selectedIdx = this.options.findIndex(option => option.value === this.value);\n const firstActiveIdx = this.options.findIndex(option => !inactive && !option.isInactive);\n const focusableIdx =\n selectedIdx >= 0 && !inactive && !this.options[selectedIdx]?.isInactive\n ? selectedIdx\n : firstActiveIdx;\n const renderedOptions = this.renderOptions(inactive, focusableIdx);\n\n return (\n <Host\n role=\"radiogroup\"\n aria-label={this.showGroupLabel ? this.groupLabel : this.ariaLabel}\n aria-labelledby={this.ariaLabelledby}\n aria-required={this.required ? 'true' : undefined}\n aria-invalid={invalid ? 'true' : undefined}\n class={{\n radio: true,\n 'radio--horizontal': this.direction === 'horizontal',\n [`radio--${this.size}`]: true,\n 'radio--labeled': this.showGroupLabel,\n }}\n >\n {this.showGroupLabel ? (\n <ds-text\n key=\"radio-group-label\"\n class={`radio__group-label ds-control-section-heading ds-control--${this.size}`}\n as=\"span\"\n variant=\"text-body-small\"\n emphasis\n >\n {this.groupLabel}\n </ds-text>\n ) : null}\n <div key=\"radio-options\" class=\"radio__options\">\n {renderedOptions}\n </div>\n </Host>\n );\n }\n}\n",
426
468
  "type": "registry:ui"
427
469
  }
428
470
  ]