@ds-mo/ui 12.4.1 → 12.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. package/README.md +16 -0
  2. package/dist/.build-stamp +1 -1
  3. package/dist/agent-patterns.json +21 -12
  4. package/dist/agent.json +1541 -1426
  5. package/dist/angular/ds-bar-workflow.d.ts +1 -1
  6. package/dist/angular/ds-bar-workflow.js +3 -3
  7. package/dist/angular/ds-mobile-bar-nav.d.ts +18 -0
  8. package/dist/angular/{ds-shell-mobile-bar.js → ds-mobile-bar-nav.js} +15 -15
  9. package/dist/angular/ds-mobile-header.d.ts +23 -0
  10. package/dist/angular/ds-mobile-header.js +45 -0
  11. package/dist/angular/ds-mobile-section-switcher.d.ts +18 -0
  12. package/dist/angular/{ds-shell-mobile-section-nav.js → ds-mobile-section-switcher.js} +15 -15
  13. package/dist/angular/ds-mobile-sheet-nav.d.ts +20 -0
  14. package/dist/angular/{ds-shell-mobile-nav.js → ds-mobile-sheet-nav.js} +13 -16
  15. package/dist/angular/ds-shell-app.d.ts +1 -1
  16. package/dist/angular/ds-shell-app.js +3 -3
  17. package/dist/angular/ds-shell-page.d.ts +1 -1
  18. package/dist/angular/ds-shell-page.js +3 -3
  19. package/dist/angular/index.d.ts +1 -1
  20. package/dist/angular/index.js +4 -3
  21. package/dist/angular/proxies.d.ts +4 -3
  22. package/dist/angular/proxies.js +4 -3
  23. package/dist/components/ds-agent-activity.js +1 -1
  24. package/dist/components/ds-agent-response.js +1 -1
  25. package/dist/components/ds-agent-tool-call.js +1 -1
  26. package/dist/components/ds-avatar.js +1 -1
  27. package/dist/components/ds-avatar.js.map +1 -1
  28. package/dist/components/ds-badge.js +1 -1
  29. package/dist/components/ds-bar-nav.js +1 -1
  30. package/dist/components/ds-bar-nav.js.map +1 -1
  31. package/dist/components/ds-bar-title.js +1 -1
  32. package/dist/components/ds-bar-title.js.map +1 -1
  33. package/dist/components/ds-bar-workflow.js +1 -1
  34. package/dist/components/ds-bar-workflow.js.map +1 -1
  35. package/dist/components/ds-breadcrumb.js +1 -1
  36. package/dist/components/ds-button-filled.js +1 -1
  37. package/dist/components/ds-button-unfilled.js +1 -1
  38. package/dist/components/ds-card-data-viz-bar.js +1 -1
  39. package/dist/components/ds-card-data-viz-donut.js +1 -1
  40. package/dist/components/ds-card-data-viz-line.js +1 -1
  41. package/dist/components/ds-card-setting.js +1 -1
  42. package/dist/components/ds-chart-bar.js +1 -1
  43. package/dist/components/ds-chart-donut.js +1 -1
  44. package/dist/components/ds-chart-legend.js +1 -1
  45. package/dist/components/ds-chart-legend.js.map +1 -1
  46. package/dist/components/ds-chart-line.js +1 -1
  47. package/dist/components/ds-checkbox.js +1 -1
  48. package/dist/components/ds-chip.js +1 -1
  49. package/dist/components/ds-chip.js.map +1 -1
  50. package/dist/components/ds-code-block.js +1 -1
  51. package/dist/components/ds-conversation-list-item.js +1 -1
  52. package/dist/components/ds-conversation-list-item.js.map +1 -1
  53. package/dist/components/ds-conversation-list-section.js +1 -1
  54. package/dist/components/ds-conversation-list.js +1 -1
  55. package/dist/components/ds-conversation-list.js.map +1 -1
  56. package/dist/components/ds-empty-state.js +1 -1
  57. package/dist/components/ds-field.js +1 -1
  58. package/dist/components/ds-input.js +1 -1
  59. package/dist/components/ds-input.js.map +1 -1
  60. package/dist/components/ds-loader.js +1 -1
  61. package/dist/components/ds-markdown.js +1 -1
  62. package/dist/components/ds-menu.js +1 -1
  63. package/dist/components/ds-message-composer.js +1 -1
  64. package/dist/components/ds-message-composer.js.map +1 -1
  65. package/dist/components/ds-message-scroller.js +1 -1
  66. package/dist/components/ds-message-scroller.js.map +1 -1
  67. package/dist/components/ds-message.js +1 -1
  68. package/dist/components/{ds-shell-mobile-nav.d.ts → ds-mobile-bar-nav.d.ts} +4 -4
  69. package/dist/components/ds-mobile-bar-nav.js +2 -0
  70. package/dist/components/ds-mobile-bar-nav.js.map +1 -0
  71. package/dist/components/ds-mobile-header.d.ts +11 -0
  72. package/dist/components/ds-mobile-header.js +2 -0
  73. package/dist/components/ds-mobile-header.js.map +1 -0
  74. package/dist/components/ds-mobile-section-switcher.d.ts +11 -0
  75. package/dist/components/ds-mobile-section-switcher.js +2 -0
  76. package/dist/components/ds-mobile-section-switcher.js.map +1 -0
  77. package/dist/components/{ds-shell-mobile-bar.d.ts → ds-mobile-sheet-nav.d.ts} +4 -4
  78. package/dist/components/ds-mobile-sheet-nav.js +2 -0
  79. package/dist/components/ds-mobile-sheet-nav.js.map +1 -0
  80. package/dist/components/ds-modal.js +1 -1
  81. package/dist/components/ds-modal.js.map +1 -1
  82. package/dist/components/ds-panel-nav.js +1 -1
  83. package/dist/components/ds-panel-nav.js.map +1 -1
  84. package/dist/components/ds-panel-sub-nav.js +1 -1
  85. package/dist/components/ds-panel-sub-nav.js.map +1 -1
  86. package/dist/components/ds-panel-tool-header.js +1 -1
  87. package/dist/components/ds-panel-tool-search.js +1 -1
  88. package/dist/components/ds-panel-tool-search.js.map +1 -1
  89. package/dist/components/ds-panel-tools.js +1 -1
  90. package/dist/components/ds-radio.js +1 -1
  91. package/dist/components/ds-radio.js.map +1 -1
  92. package/dist/components/ds-select-multi.js +1 -1
  93. package/dist/components/ds-select-multi.js.map +1 -1
  94. package/dist/components/ds-select.js +1 -1
  95. package/dist/components/ds-select.js.map +1 -1
  96. package/dist/components/ds-shell-app.js +1 -1
  97. package/dist/components/ds-shell-app.js.map +1 -1
  98. package/dist/components/ds-shell-gradient-picker.js +1 -1
  99. package/dist/components/ds-shell-gradient-picker.js.map +1 -1
  100. package/dist/components/ds-shell-gradient-swatch.js +1 -1
  101. package/dist/components/ds-shell-page.js +1 -1
  102. package/dist/components/ds-shell-page.js.map +1 -1
  103. package/dist/components/ds-shell-tools.js +1 -1
  104. package/dist/components/ds-shell-tools.js.map +1 -1
  105. package/dist/components/ds-skeleton.js +1 -1
  106. package/dist/components/ds-skeleton.js.map +1 -1
  107. package/dist/components/ds-swatch-picker.js +1 -1
  108. package/dist/components/ds-switch.js +1 -1
  109. package/dist/components/ds-tab-group.js +1 -1
  110. package/dist/components/ds-tag.js +1 -1
  111. package/dist/components/ds-tag.js.map +1 -1
  112. package/dist/components/ds-toast.js +1 -1
  113. package/dist/components/ds-toast.js.map +1 -1
  114. package/dist/components/ds-tooltip-data-viz.js +1 -1
  115. package/dist/components/ds-tooltip.js +1 -1
  116. package/dist/components/ds-typing-indicator.js +1 -1
  117. package/dist/components/p--bSkr_yn.js +2 -0
  118. package/dist/components/p--bSkr_yn.js.map +1 -0
  119. package/dist/components/p-46nk9-RO.js +2 -0
  120. package/dist/components/p-46nk9-RO.js.map +1 -0
  121. package/dist/components/{p-9dQQSgwT.js → p-BHYrsGBw.js} +2 -2
  122. package/dist/components/p-BY0PzkUp.js +2 -0
  123. package/dist/components/p-BgGP65Nn.js +2 -0
  124. package/dist/components/p-BgGP65Nn.js.map +1 -0
  125. package/dist/components/p-BkAMkjIR.js +2 -0
  126. package/dist/components/{p-Co8hD3P-.js.map → p-BkAMkjIR.js.map} +1 -1
  127. package/dist/components/p-BrusnEYP.js.map +1 -1
  128. package/dist/components/p-C2ym_Ich.js +2 -0
  129. package/dist/components/{p-Biz6xo-z.js → p-CFpWrYAH.js} +2 -2
  130. package/dist/components/p-CICGtu7H.js +2 -0
  131. package/dist/components/{p-CtD24EyI.js.map → p-CICGtu7H.js.map} +1 -1
  132. package/dist/components/p-CLzlL3G4.js +2 -0
  133. package/dist/components/p-CLzlL3G4.js.map +1 -0
  134. package/dist/components/p-CXcjwduV.js +2 -0
  135. package/dist/components/{p-BhxwpOH8.js.map → p-CXcjwduV.js.map} +1 -1
  136. package/dist/components/p-Ca-I2UnB.js +2 -0
  137. package/dist/components/p-Ca-I2UnB.js.map +1 -0
  138. package/dist/components/p-CaRyxTMz.js +2 -0
  139. package/dist/components/p-CaRyxTMz.js.map +1 -0
  140. package/dist/components/{p-g40DTeV8.js → p-CvKKmZJj.js} +2 -2
  141. package/dist/components/p-D4o2D1hk.js +2 -0
  142. package/dist/components/p-D4o2D1hk.js.map +1 -0
  143. package/dist/components/{p-DXnFhmZF.js → p-D9iN80yX.js} +2 -2
  144. package/dist/components/p-DD1Lgs7O.js +2 -0
  145. package/dist/components/p-DD1Lgs7O.js.map +1 -0
  146. package/dist/components/{p-DjPZxzIG.js → p-DQDpDYyF.js} +2 -2
  147. package/dist/components/p-DTea1Du7.js +2 -0
  148. package/dist/components/p-DTea1Du7.js.map +1 -0
  149. package/dist/components/p-DWdRSOBV.js +2 -0
  150. package/dist/components/{p-BPS9VExm.js.map → p-DWdRSOBV.js.map} +1 -1
  151. package/dist/components/p-DcFfilOX.js +2 -0
  152. package/dist/components/p-DcFfilOX.js.map +1 -0
  153. package/dist/components/p-Dg3xLna0.js +2 -0
  154. package/dist/components/{p-C_0Q8Cnp.js.map → p-Dg3xLna0.js.map} +1 -1
  155. package/dist/components/p-DvVW0hxm.js +2 -0
  156. package/dist/components/p-DvVW0hxm.js.map +1 -0
  157. package/dist/components/p-YtgcuJYx.js +2 -0
  158. package/dist/components/{p-PZ6R_nGI.js.map → p-YtgcuJYx.js.map} +1 -1
  159. package/dist/components/p-loq_--ce.js +2 -0
  160. package/dist/components/{p-NUNsefU1.js.map → p-loq_--ce.js.map} +1 -1
  161. package/dist/docs/components.json +3586 -3064
  162. package/dist/lib/shell/index.d.ts +1 -1
  163. package/dist/lib/shell/index.js +2 -1
  164. package/dist/lib/shell/index.js.map +2 -2
  165. package/dist/lib/shell/shell-responsive.d.ts +3 -3
  166. package/dist/lib/utils/index.d.ts +1 -0
  167. package/dist/lib/utils/index.js +8 -0
  168. package/dist/lib/utils/index.js.map +3 -3
  169. package/dist/lib/utils/required-validity.d.ts +14 -0
  170. package/dist/mcp-data/registry/agent-activity.json +2 -2
  171. package/dist/mcp-data/registry/badge.json +2 -2
  172. package/dist/mcp-data/registry/bar-nav.json +2 -2
  173. package/dist/mcp-data/registry/bar-title.json +2 -2
  174. package/dist/mcp-data/registry/bar-workflow.json +25 -4
  175. package/dist/mcp-data/registry/breadcrumb.json +2 -2
  176. package/dist/mcp-data/registry/button-unfilled.json +3 -3
  177. package/dist/mcp-data/registry/chart-legend.json +1 -1
  178. package/dist/mcp-data/registry/checkbox.json +1 -1
  179. package/dist/mcp-data/registry/conversation-list-item.json +4 -3
  180. package/dist/mcp-data/registry/conversation-list.json +4 -3
  181. package/dist/mcp-data/registry/input.json +1 -1
  182. package/dist/mcp-data/registry/loader.json +2 -2
  183. package/dist/mcp-data/registry/message-scroller.json +3 -2
  184. package/dist/mcp-data/registry/mobile-bar-nav.json +333 -0
  185. package/dist/mcp-data/registry/mobile-header.json +351 -0
  186. package/dist/mcp-data/registry/mobile-section-switcher.json +210 -0
  187. package/dist/mcp-data/registry/mobile-sheet-nav.json +317 -0
  188. package/dist/mcp-data/registry/panel-nav.json +2 -2
  189. package/dist/mcp-data/registry/panel-tools.json +2 -2
  190. package/dist/mcp-data/registry/radio.json +1 -1
  191. package/dist/mcp-data/registry/registry.json +1549 -1431
  192. package/dist/mcp-data/registry/select-multi.json +1 -1
  193. package/dist/mcp-data/registry/select.json +2 -2
  194. package/dist/mcp-data/registry/shell-app.json +17 -17
  195. package/dist/mcp-data/registry/shell-page.json +26 -7
  196. package/dist/mcp-data/registry/shell-tools.json +10 -7
  197. package/dist/mcp-data/registry/switch.json +1 -1
  198. package/dist/mcp-data/registry/toast.json +2 -2
  199. package/dist/react/components.d.ts +4 -3
  200. package/dist/react/components.js +4 -3
  201. package/dist/react/ds-mobile-bar-nav.d.ts +13 -0
  202. package/dist/react/ds-mobile-bar-nav.js +15 -0
  203. package/dist/react/ds-mobile-header.d.ts +13 -0
  204. package/dist/react/ds-mobile-header.js +15 -0
  205. package/dist/react/ds-mobile-section-switcher.d.ts +12 -0
  206. package/dist/react/ds-mobile-section-switcher.js +12 -0
  207. package/dist/react/ds-mobile-sheet-nav.d.ts +14 -0
  208. package/dist/react/ds-mobile-sheet-nav.js +16 -0
  209. package/dist/styles/control-elevation.css +40 -0
  210. package/dist/types/components/BarWorkflow/BarWorkflow.d.ts +6 -0
  211. package/dist/types/components/ButtonUnfilled/ButtonUnfilled.d.ts +1 -1
  212. package/dist/types/components/MobileBarNav/MobileBarNav.d.ts +26 -0
  213. package/dist/types/components/MobileBarNav/index.d.ts +4 -0
  214. package/dist/types/components/MobileBarNav/mobile-bar-nav-types.d.ts +5 -0
  215. package/dist/types/components/MobileHeader/MobileHeader.d.ts +39 -0
  216. package/dist/types/components/MobileHeader/index.d.ts +2 -0
  217. package/dist/types/components/MobileHeader/mobile-header-types.d.ts +5 -0
  218. package/dist/types/components/MobileSectionSwitcher/MobileSectionSwitcher.d.ts +35 -0
  219. package/dist/types/components/MobileSectionSwitcher/index.d.ts +3 -0
  220. package/dist/types/components/MobileSectionSwitcher/mobile-section-switcher-types.d.ts +4 -0
  221. package/dist/types/components/MobileSectionSwitcher/mobile-section-switcher-utils.d.ts +10 -0
  222. package/dist/types/components/{ShellMobileNav/ShellMobileNav.d.ts → MobileSheetNav/MobileSheetNav.d.ts} +5 -7
  223. package/dist/types/components/MobileSheetNav/index.d.ts +1 -0
  224. package/dist/types/components/ShellApp/ShellApp.d.ts +4 -4
  225. package/dist/types/components/ShellPage/ShellPage.d.ts +4 -0
  226. package/dist/types/components.d.ts +530 -381
  227. package/dist/types/shell/index.d.ts +1 -1
  228. package/dist/types/shell/shell-responsive.d.ts +3 -3
  229. package/dist/types/utils/index.d.ts +1 -0
  230. package/dist/types/utils/required-validity.d.ts +15 -0
  231. package/package.json +2 -1
  232. package/dist/angular/ds-shell-mobile-bar.d.ts +0 -18
  233. package/dist/angular/ds-shell-mobile-nav.d.ts +0 -23
  234. package/dist/angular/ds-shell-mobile-section-nav.d.ts +0 -18
  235. package/dist/components/ds-shell-mobile-bar.js +0 -2
  236. package/dist/components/ds-shell-mobile-bar.js.map +0 -1
  237. package/dist/components/ds-shell-mobile-nav.js +0 -2
  238. package/dist/components/ds-shell-mobile-nav.js.map +0 -1
  239. package/dist/components/ds-shell-mobile-section-nav.d.ts +0 -11
  240. package/dist/components/ds-shell-mobile-section-nav.js +0 -2
  241. package/dist/components/ds-shell-mobile-section-nav.js.map +0 -1
  242. package/dist/components/p-B6Vuayt6.js +0 -2
  243. package/dist/components/p-B6Vuayt6.js.map +0 -1
  244. package/dist/components/p-BPS9VExm.js +0 -2
  245. package/dist/components/p-BVV_rlIP.js +0 -2
  246. package/dist/components/p-BVV_rlIP.js.map +0 -1
  247. package/dist/components/p-BhxwpOH8.js +0 -2
  248. package/dist/components/p-BtEjlcB9.js +0 -2
  249. package/dist/components/p-BtEjlcB9.js.map +0 -1
  250. package/dist/components/p-CH6cNXXF.js +0 -2
  251. package/dist/components/p-CH6cNXXF.js.map +0 -1
  252. package/dist/components/p-C_0Q8Cnp.js +0 -2
  253. package/dist/components/p-Ce52yVXF.js +0 -2
  254. package/dist/components/p-Ce52yVXF.js.map +0 -1
  255. package/dist/components/p-CgHTNfC1.js +0 -2
  256. package/dist/components/p-CgHTNfC1.js.map +0 -1
  257. package/dist/components/p-Co8hD3P-.js +0 -2
  258. package/dist/components/p-CoYFJD-w.js +0 -2
  259. package/dist/components/p-CoYFJD-w.js.map +0 -1
  260. package/dist/components/p-Cpr3K41-.js +0 -2
  261. package/dist/components/p-Cpr3K41-.js.map +0 -1
  262. package/dist/components/p-CtD24EyI.js +0 -2
  263. package/dist/components/p-DYqReipj.js +0 -2
  264. package/dist/components/p-DYqReipj.js.map +0 -1
  265. package/dist/components/p-NUNsefU1.js +0 -2
  266. package/dist/components/p-PZ6R_nGI.js +0 -2
  267. package/dist/components/p-jCWtxthA.js +0 -2
  268. package/dist/components/p-yhj1lIPx.js +0 -2
  269. package/dist/mcp-data/registry/shell-mobile-bar.json +0 -314
  270. package/dist/mcp-data/registry/shell-mobile-nav.json +0 -348
  271. package/dist/mcp-data/registry/shell-mobile-section-nav.json +0 -267
  272. package/dist/react/ds-shell-mobile-bar.d.ts +0 -13
  273. package/dist/react/ds-shell-mobile-bar.js +0 -15
  274. package/dist/react/ds-shell-mobile-nav.d.ts +0 -15
  275. package/dist/react/ds-shell-mobile-nav.js +0 -17
  276. package/dist/react/ds-shell-mobile-section-nav.d.ts +0 -12
  277. package/dist/react/ds-shell-mobile-section-nav.js +0 -12
  278. package/dist/types/components/ShellMobileBar/ShellMobileBar.d.ts +0 -25
  279. package/dist/types/components/ShellMobileBar/index.d.ts +0 -4
  280. package/dist/types/components/ShellMobileBar/shell-mobile-bar-types.d.ts +0 -5
  281. package/dist/types/components/ShellMobileNav/index.d.ts +0 -2
  282. package/dist/types/components/ShellMobileNav/shell-mobile-nav-types.d.ts +0 -6
  283. package/dist/types/components/ShellMobileSectionNav/ShellMobileSectionNav.d.ts +0 -30
  284. package/dist/types/components/ShellMobileSectionNav/index.d.ts +0 -2
  285. /package/dist/components/{p-9dQQSgwT.js.map → p-BHYrsGBw.js.map} +0 -0
  286. /package/dist/components/{p-jCWtxthA.js.map → p-BY0PzkUp.js.map} +0 -0
  287. /package/dist/components/{p-yhj1lIPx.js.map → p-C2ym_Ich.js.map} +0 -0
  288. /package/dist/components/{p-Biz6xo-z.js.map → p-CFpWrYAH.js.map} +0 -0
  289. /package/dist/components/{p-g40DTeV8.js.map → p-CvKKmZJj.js.map} +0 -0
  290. /package/dist/components/{p-DXnFhmZF.js.map → p-D9iN80yX.js.map} +0 -0
  291. /package/dist/components/{p-DjPZxzIG.js.map → p-DQDpDYyF.js.map} +0 -0
@@ -354,7 +354,7 @@
354
354
  },
355
355
  {
356
356
  "path": "src/wc/components/Checkbox/Checkbox.tsx",
357
- "content": "import { AttachInternals, Component, Prop, State, Event, EventEmitter, Watch, h, Host } from '@stencil/core';\n\nexport type CheckboxSize = 'md' | 'sm' | 'xs';\n\nconst LABEL_VARIANT: Record<CheckboxSize, 'text-body-medium' | 'text-body-small' | 'text-caption'> = {\n md: 'text-body-medium',\n sm: 'text-body-small',\n xs: 'text-caption',\n};\n\nlet idCounter = 0;\n\n@Component({\n tag: 'ds-checkbox',\n styleUrl: 'Checkbox.css',\n scoped: true,\n formAssociated: true,\n})\nexport class Checkbox {\n @AttachInternals() internals!: ElementInternals;\n private labelId = `ds-checkbox-label-${++idCounter}`;\n\n /** Visible label and accessible name. Omitted only in presentation mode. */\n @Prop() label!: string;\n /** Current checked state. */\n @Prop({ mutable: true }) checked: boolean = false;\n /** Visual and placement density. */\n @Prop() size: CheckboxSize = 'md';\n /** Native form field name. */\n @Prop({ reflect: true }) name: string | undefined;\n /** Submitted value when checked. */\n @Prop() value: string = 'on';\n /** Native disabled state. */\n @Prop({ reflect: true }) disabled: boolean = false;\n /** Require the checkbox to be checked for form validity. */\n @Prop({ reflect: true }) required: boolean = false;\n /** Validation message used when a required checkbox is unchecked. */\n @Prop() requiredMessage: string = 'This field is required.';\n /** Mixed visual state. Activation clears it before toggling checked. */\n @Prop({ mutable: true }) indeterminate: boolean = false;\n /** Design-system inactive state. */\n @Prop() isInactive: boolean = false;\n /** Visual-only indicator for a composite control that owns selection semantics. */\n @Prop() presentation: boolean = false;\n\n /** Emitted after user activation with the new checked state. */\n @Event() dsChange!: EventEmitter<boolean>;\n\n private initialChecked = false;\n private initialIndeterminate = false;\n @State() private formDisabled = false;\n\n componentWillLoad() {\n this.initialChecked = this.checked;\n this.initialIndeterminate = this.indeterminate;\n this.syncFormValue();\n }\n\n @Watch('checked')\n @Watch('value')\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('required')\n @Watch('presentation')\n syncFormValue() {\n const inactive = this.isInactive || this.disabled || this.formDisabled || this.presentation;\n this.internals.setFormValue(!inactive && this.checked ? this.value : null);\n const missing = this.required && !inactive && !this.checked;\n this.internals.setValidity(missing ? { valueMissing: true } : {}, missing ? this.requiredMessage : '');\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.checked = this.initialChecked;\n this.indeterminate = this.initialIndeterminate;\n }\n\n private handleActivate = () => {\n if (this.isInactive || this.disabled || this.formDisabled || this.presentation) return;\n if (this.indeterminate) this.indeterminate = false;\n this.checked = !this.checked;\n this.dsChange.emit(this.checked);\n };\n\n private handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n this.handleActivate();\n }\n };\n\n render() {\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n const isMarked = this.checked || this.indeterminate;\n const invalid = this.required && !inactive && !this.checked;\n\n return (\n <Host\n role={this.presentation ? undefined : 'checkbox'}\n aria-checked={this.presentation ? undefined : this.indeterminate ? 'mixed' : String(this.checked)}\n aria-disabled={!this.presentation && inactive ? 'true' : undefined}\n aria-required={!this.presentation && this.required ? 'true' : undefined}\n aria-invalid={!this.presentation && invalid ? 'true' : undefined}\n aria-labelledby={this.presentation ? undefined : this.labelId}\n aria-hidden={this.presentation ? 'true' : undefined}\n tabIndex={this.presentation || inactive ? -1 : 0}\n class={{\n checkbox: true,\n 'checkbox--presentation': this.presentation,\n [`checkbox--${this.size}`]: true,\n [`ds-control--${this.size}`]: true,\n 'ds-control-inactive': inactive && !this.presentation,\n 'ds-focus-ring-inset': !this.presentation,\n 'ds-interaction-fill': !inactive && !this.presentation,\n }}\n onClick={this.presentation ? undefined : this.handleActivate}\n onKeyDown={this.presentation ? undefined : this.handleKeyDown}\n >\n <span class=\"checkbox__placement ds-interaction-fill__content\" aria-hidden=\"true\">\n <span class={{ box: true, 'box--marked': isMarked }}>\n {isMarked && (\n // eslint-disable-next-line local/prefer-ds-icon -- Checkbox owns this density-specific state mark and its optical stroke.\n <svg\n class={{\n checkbox__mark: true,\n 'checkbox__mark--checked': !this.indeterminate,\n }}\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n focusable=\"false\"\n >\n <path\n d={this.indeterminate ? 'M4 8H12' : 'M3.5 8.25L6.75 11.5L12.5 4.75'}\n vector-effect=\"non-scaling-stroke\"\n />\n </svg>\n )}\n </span>\n </span>\n {!this.presentation && (\n <ds-text\n class=\"checkbox__label ds-interaction-fill__content\"\n as=\"span\"\n variant={LABEL_VARIANT[this.size]}\n textId={this.labelId}\n >\n {this.label}\n </ds-text>\n )}\n </Host>\n );\n }\n}\n",
357
+ "content": "import { AttachInternals, Component, Prop, State, Event, EventEmitter, Watch, h, Host } from '@stencil/core';\nimport { DEFAULT_REQUIRED_MESSAGE, setRequiredValidity } from '../../utils';\n\nexport type CheckboxSize = 'md' | 'sm' | 'xs';\n\nconst LABEL_VARIANT: Record<CheckboxSize, 'text-body-medium' | 'text-body-small' | 'text-caption'> = {\n md: 'text-body-medium',\n sm: 'text-body-small',\n xs: 'text-caption',\n};\n\nlet idCounter = 0;\n\n@Component({\n tag: 'ds-checkbox',\n styleUrl: 'Checkbox.css',\n scoped: true,\n formAssociated: true,\n})\nexport class Checkbox {\n @AttachInternals() internals!: ElementInternals;\n private labelId = `ds-checkbox-label-${++idCounter}`;\n\n /** Visible label and accessible name. Omitted only in presentation mode. */\n @Prop() label!: string;\n /** Current checked state. */\n @Prop({ mutable: true }) checked: boolean = false;\n /** Visual and placement density. */\n @Prop() size: CheckboxSize = 'md';\n /** Native form field name. */\n @Prop({ reflect: true }) name: string | undefined;\n /** Submitted value when checked. */\n @Prop() value: string = 'on';\n /** Native disabled state. */\n @Prop({ reflect: true }) disabled: boolean = false;\n /** Require the checkbox to be checked for form validity. */\n @Prop({ reflect: true }) required: boolean = false;\n /** Validation message used when a required checkbox is unchecked. */\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n /** Mixed visual state. Activation clears it before toggling checked. */\n @Prop({ mutable: true }) indeterminate: boolean = false;\n /** Design-system inactive state. */\n @Prop() isInactive: boolean = false;\n /** Visual-only indicator for a composite control that owns selection semantics. */\n @Prop() presentation: boolean = false;\n\n /** Emitted after user activation with the new checked state. */\n @Event() dsChange!: EventEmitter<boolean>;\n\n private initialChecked = false;\n private initialIndeterminate = false;\n @State() private formDisabled = false;\n\n componentWillLoad() {\n this.initialChecked = this.checked;\n this.initialIndeterminate = this.indeterminate;\n this.syncFormValue();\n }\n\n @Watch('checked')\n @Watch('value')\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('required')\n @Watch('presentation')\n syncFormValue() {\n const inactive = this.isInactive || this.disabled || this.formDisabled || this.presentation;\n this.internals.setFormValue(!inactive && this.checked ? this.value : null);\n const missing = this.required && !inactive && !this.checked;\n setRequiredValidity(this.internals, missing, this.requiredMessage);\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.checked = this.initialChecked;\n this.indeterminate = this.initialIndeterminate;\n }\n\n private handleActivate = () => {\n if (this.isInactive || this.disabled || this.formDisabled || this.presentation) return;\n if (this.indeterminate) this.indeterminate = false;\n this.checked = !this.checked;\n this.dsChange.emit(this.checked);\n };\n\n private handleKeyDown = (e: KeyboardEvent) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n this.handleActivate();\n }\n };\n\n render() {\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n const isMarked = this.checked || this.indeterminate;\n const invalid = this.required && !inactive && !this.checked;\n\n return (\n <Host\n role={this.presentation ? undefined : 'checkbox'}\n aria-checked={this.presentation ? undefined : this.indeterminate ? 'mixed' : String(this.checked)}\n aria-disabled={!this.presentation && inactive ? 'true' : undefined}\n aria-required={!this.presentation && this.required ? 'true' : undefined}\n aria-invalid={!this.presentation && invalid ? 'true' : undefined}\n aria-labelledby={this.presentation ? undefined : this.labelId}\n aria-hidden={this.presentation ? 'true' : undefined}\n tabIndex={this.presentation || inactive ? -1 : 0}\n class={{\n checkbox: true,\n 'checkbox--presentation': this.presentation,\n [`checkbox--${this.size}`]: true,\n [`ds-control--${this.size}`]: true,\n 'ds-control-inactive': inactive && !this.presentation,\n 'ds-focus-ring-inset': !this.presentation,\n 'ds-interaction-fill': !inactive && !this.presentation,\n }}\n onClick={this.presentation ? undefined : this.handleActivate}\n onKeyDown={this.presentation ? undefined : this.handleKeyDown}\n >\n <span class=\"checkbox__placement ds-interaction-fill__content\" aria-hidden=\"true\">\n <span class={{ box: true, 'box--marked': isMarked }}>\n {isMarked && (\n // eslint-disable-next-line local/prefer-ds-icon -- Checkbox owns this density-specific state mark and its optical stroke.\n <svg\n class={{\n checkbox__mark: true,\n 'checkbox__mark--checked': !this.indeterminate,\n }}\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n focusable=\"false\"\n >\n <path\n d={this.indeterminate ? 'M4 8H12' : 'M3.5 8.25L6.75 11.5L12.5 4.75'}\n vector-effect=\"non-scaling-stroke\"\n />\n </svg>\n )}\n </span>\n </span>\n {!this.presentation && (\n <ds-text\n class=\"checkbox__label ds-interaction-fill__content\"\n as=\"span\"\n variant={LABEL_VARIANT[this.size]}\n textId={this.labelId}\n >\n {this.label}\n </ds-text>\n )}\n </Host>\n );\n }\n}\n",
358
358
  "type": "registry:ui"
359
359
  }
360
360
  ]
@@ -40,7 +40,8 @@
40
40
  "Any positive unreadCount renders one dot on the title row rather than a visible counter; the accessible row name may still announce the exact unread quantity.",
41
41
  "Hover and press use the shared interaction hover and pressed overlays. Only the selected conversation uses the persistent active fill.",
42
42
  "The title uses body-medium, not title-small, and the body-small preview truncates after one line. Unread titles use emphasis with primary foreground; read titles use regular weight with secondary foreground.",
43
- "The actions slot overlays the row at an 8px right inset without changing title or preview measure, and paints above the row interaction wash while that wash remains active."
43
+ "The actions slot overlays the row at an 8px right inset without changing title or preview measure, and paints above the row interaction wash while that wash remains active.",
44
+ "Contextual actions use the shared md control-elevation wrapper so their transparent backdrop blur is unchanged while the split inset highlight remains above the borderless slotted action."
44
45
  ],
45
46
  "responsiveBehavior": [
46
47
  "Long titles and previews truncate without displacing trailing status."
@@ -271,12 +272,12 @@
271
272
  "files": [
272
273
  {
273
274
  "path": "src/wc/components/ConversationListItem/ConversationListItem.css",
274
- "content": "@import '../../utils/choice-list.css';\n\n:host {\n display: block;\n box-sizing: border-box;\n -webkit-user-select: none;\n user-select: none;\n}\n\n:host(:hover),\n:host(:focus-within),\n:host(.conversation-list-item--actions-open) {\n position: relative;\n z-index: var(--dimension-z-index-floating);\n}\n\n.conversation-list-item__row {\n position: relative;\n isolation: isolate;\n}\n\n.conversation-list-item {\n color: var(--color-foreground-primary);\n}\n\n.conversation-list-item--error {\n --ds-interaction-border-width: var(--dimension-stroke-width-012);\n --ds-interaction-border-color: var(--color-border-bold-negative);\n}\n\n.conversation-list-item__leading:empty,\n.conversation-list-item__trailing:empty {\n display: none;\n}\n\n.conversation-list-item__leading,\n.conversation-list-item__trailing,\n.conversation-list-item__preview-row {\n display: flex;\n align-items: center;\n}\n\n.conversation-list-item__leading {\n align-self: center;\n width: auto;\n height: auto;\n margin-inline-start: var(--ds-control-label-inset, var(--dimension-space-025));\n margin-inline-end: var(--dimension-space-050);\n}\n\n.conversation-list-item__content {\n min-width: 0;\n}\n\n.conversation-list-item__title-row {\n display: flex;\n align-items: center;\n min-width: 0;\n}\n\n.conversation-list-item__preview-row {\n min-width: 0;\n gap: var(--dimension-space-050);\n}\n\nds-text.conversation-list-item__title,\nds-text.conversation-list-item__preview {\n display: block;\n min-width: 0;\n}\n\nds-text.conversation-list-item__title {\n flex: 1;\n}\n\n.conversation-list-item__unread {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--dimension-size-200);\n margin-inline-end: var(--dimension-space-025);\n}\n\nds-text.conversation-list-item__preview.ds-choice-item__subtext {\n -webkit-line-clamp: 1;\n}\n\n.conversation-list-item__actions {\n position: absolute;\n top: 50%;\n right: var(--dimension-space-100);\n z-index: var(--dimension-z-index-floating);\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--dimension-radius-half);\n background: var(--color-background-transparent);\n box-shadow: var(--effect-elevation-elevated-md);\n -webkit-backdrop-filter: blur(var(--effect-blur-sm));\n backdrop-filter: blur(var(--effect-blur-sm));\n opacity: 0;\n pointer-events: none;\n transform: translateY(-50%);\n}\n\n.conversation-list-item__row:hover .conversation-list-item__actions,\n.conversation-list-item__row:has(:focus-visible) .conversation-list-item__actions,\n:host(.conversation-list-item--actions-open) .conversation-list-item__actions {\n opacity: 1;\n pointer-events: auto;\n}\n\n/* Keep the row wash continuous while the pointer moves onto its sibling action. */\n.conversation-list-item__row:hover .conversation-list-item::after {\n background: var(--ds-interaction-hover);\n}\n\n:host(.conversation-list-item--actions-open) .conversation-list-item::after {\n background: var(--ds-interaction-hover);\n}\n\n.conversation-list-item__row:active .conversation-list-item::after {\n background: var(--ds-interaction-pressed);\n}\n\n@media (hover: none) {\n .conversation-list-item__actions {\n opacity: 1;\n pointer-events: auto;\n }\n\n :host(.conversation-list-item--has-actions) .conversation-list-item__title-row {\n padding-inline-end: var(--dimension-size-400);\n }\n}\n\n.conversation-list-item__time {\n position: absolute;\n width: var(--dimension-size-050);\n height: var(--dimension-size-050);\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n}\n",
275
+ "content": "@import '../../utils/choice-list.css';\n@import '../../utils/visually-hidden.css';\n@import '../../styles/control-elevation.css';\n\n:host {\n display: block;\n box-sizing: border-box;\n -webkit-user-select: none;\n user-select: none;\n}\n\n:host(:hover),\n:host(:focus-within),\n:host(.conversation-list-item--actions-open) {\n position: relative;\n z-index: var(--dimension-z-index-floating);\n}\n\n.conversation-list-item__row {\n position: relative;\n isolation: isolate;\n}\n\n.conversation-list-item {\n color: var(--color-foreground-primary);\n}\n\n.conversation-list-item--error {\n --ds-interaction-border-width: var(--dimension-stroke-width-012);\n --ds-interaction-border-color: var(--color-border-bold-negative);\n}\n\n.conversation-list-item__leading:empty,\n.conversation-list-item__trailing:empty {\n display: none;\n}\n\n.conversation-list-item__leading,\n.conversation-list-item__trailing,\n.conversation-list-item__preview-row {\n display: flex;\n align-items: center;\n}\n\n.conversation-list-item__leading {\n align-self: center;\n width: auto;\n height: auto;\n margin-inline-start: var(--ds-control-label-inset, var(--dimension-space-025));\n margin-inline-end: var(--dimension-space-050);\n}\n\n.conversation-list-item__content {\n min-width: 0;\n}\n\n.conversation-list-item__title-row {\n display: flex;\n align-items: center;\n min-width: 0;\n}\n\n.conversation-list-item__preview-row {\n min-width: 0;\n gap: var(--dimension-space-050);\n}\n\nds-text.conversation-list-item__title,\nds-text.conversation-list-item__preview {\n display: block;\n min-width: 0;\n}\n\nds-text.conversation-list-item__title {\n flex: 1;\n}\n\n.conversation-list-item__unread {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n width: var(--dimension-size-200);\n margin-inline-end: var(--dimension-space-025);\n}\n\nds-text.conversation-list-item__preview.ds-choice-item__subtext {\n -webkit-line-clamp: 1;\n}\n\n.conversation-list-item__actions {\n position: absolute;\n top: 50%;\n right: var(--dimension-space-100);\n z-index: var(--dimension-z-index-floating);\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--dimension-radius-half);\n background: var(--color-background-transparent);\n -webkit-backdrop-filter: blur(var(--effect-blur-sm));\n backdrop-filter: blur(var(--effect-blur-sm));\n opacity: 0;\n pointer-events: none;\n transform: translateY(-50%);\n}\n\n.conversation-list-item__row:hover .conversation-list-item__actions,\n.conversation-list-item__row:has(:focus-visible) .conversation-list-item__actions,\n:host(.conversation-list-item--actions-open) .conversation-list-item__actions {\n opacity: 1;\n pointer-events: auto;\n}\n\n/* Keep the row wash continuous while the pointer moves onto its sibling action. */\n.conversation-list-item__row:hover .conversation-list-item::after {\n background: var(--ds-interaction-hover);\n}\n\n:host(.conversation-list-item--actions-open) .conversation-list-item::after {\n background: var(--ds-interaction-hover);\n}\n\n.conversation-list-item__row:active .conversation-list-item::after {\n background: var(--ds-interaction-pressed);\n}\n\n@media (hover: none) {\n .conversation-list-item__actions {\n opacity: 1;\n pointer-events: auto;\n }\n\n :host(.conversation-list-item--has-actions) .conversation-list-item__title-row {\n padding-inline-end: var(--dimension-size-400);\n }\n}\n\n",
275
276
  "type": "registry:ui"
276
277
  },
277
278
  {
278
279
  "path": "src/wc/components/ConversationListItem/ConversationListItem.tsx",
279
- "content": "import { Component, Element, Event, EventEmitter, h, Host, Prop } from '@stencil/core';\nimport type { ConversationItemState } from '../conversation-types';\n\n@Component({\n tag: 'ds-conversation-list-item',\n styleUrl: 'ConversationListItem.css',\n scoped: true,\n})\nexport class ConversationListItem {\n @Element() el!: HTMLElement;\n\n @Prop() conversationId: string = '';\n /** Visible conversation title. Named to avoid the native HTMLElement.title contract. */\n @Prop() conversationTitle: string = '';\n @Prop() preview: string = '';\n /** ISO timestamp for semantic activity metadata. */\n @Prop() updatedAt: string = '';\n @Prop() selected: boolean = false;\n @Prop() state: ConversationItemState = 'default';\n @Prop() statusLabel: string = '';\n @Prop() unreadCount: number = 0;\n /** Keep the contextual-actions surface visible while its popup is open or closing. */\n @Prop({ reflect: true }) actionsOpen: boolean = false;\n\n @Event() dsSelect!: EventEmitter<{ id: string }>;\n\n private handleClick = () => this.dsSelect.emit({ id: this.conversationId });\n\n private handleActionsSlotChange = (event: Event) => {\n const slot = event.target as HTMLSlotElement;\n this.el.classList.toggle(\n 'conversation-list-item--has-actions',\n slot.assignedElements().length > 0\n );\n };\n\n render() {\n const busy = this.state === 'busy';\n const subtext = busy && this.statusLabel ? this.statusLabel : this.preview;\n const accessibleName = [\n this.conversationTitle,\n subtext,\n this.unreadCount > 0 ? `${this.unreadCount} unread` : '',\n ]\n .filter(Boolean)\n .join('. ');\n\n return (\n <Host\n role=\"listitem\"\n class={{ 'conversation-list-item--actions-open': this.actionsOpen }}\n >\n <div class=\"conversation-list-item__row\">\n <button\n type=\"button\"\n class={{\n 'conversation-list-item': true,\n 'ds-choice-item': true,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--selected': this.selected,\n 'conversation-list-item--error': this.state === 'error',\n }}\n aria-label={accessibleName}\n aria-current={this.selected ? 'true' : undefined}\n aria-busy={busy ? 'true' : undefined}\n onClick={this.handleClick}\n >\n <span class=\"conversation-list-item__leading ds-choice-item__icon ds-interaction-fill__content\">\n <slot name=\"leading\" />\n </span>\n <span class=\"conversation-list-item__content ds-choice-item__content ds-interaction-fill__content\">\n <span class=\"conversation-list-item__title-row\">\n <ds-text\n class=\"conversation-list-item__title ds-choice-item__label\"\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis={this.unreadCount > 0}\n color={this.unreadCount > 0 ? 'primary' : 'secondary'}\n lineTruncation={1}\n >\n {this.conversationTitle}\n </ds-text>\n {this.unreadCount > 0 ? (\n <span class=\"conversation-list-item__unread\">\n <ds-badge variant=\"dot\" surface=\"secondary\" />\n </span>\n ) : null}\n </span>\n <span class=\"conversation-list-item__preview-row\">\n {busy ? <ds-loader size=\"sm\" color=\"secondary\" /> : null}\n <ds-text\n class=\"conversation-list-item__preview ds-choice-item__subtext\"\n as=\"span\"\n variant=\"text-body-small\"\n color={this.state === 'error' ? 'negative' : 'secondary'}\n lineTruncation={1}\n >\n {subtext}\n </ds-text>\n </span>\n {this.updatedAt ? (\n <time class=\"conversation-list-item__time\" dateTime={this.updatedAt}>\n {this.updatedAt}\n </time>\n ) : null}\n </span>\n <span class=\"conversation-list-item__trailing ds-interaction-fill__content\">\n <slot name=\"trailing\" />\n </span>\n </button>\n <div class=\"conversation-list-item__actions\">\n <slot name=\"actions\" onSlotchange={this.handleActionsSlotChange} />\n </div>\n </div>\n </Host>\n );\n }\n}\n",
280
+ "content": "import { Component, Element, Event, EventEmitter, h, Host, Prop } from '@stencil/core';\nimport type { ConversationItemState } from '../conversation-types';\n\n@Component({\n tag: 'ds-conversation-list-item',\n styleUrl: 'ConversationListItem.css',\n scoped: true,\n})\nexport class ConversationListItem {\n @Element() el!: HTMLElement;\n\n @Prop() conversationId: string = '';\n /** Visible conversation title. Named to avoid the native HTMLElement.title contract. */\n @Prop() conversationTitle: string = '';\n @Prop() preview: string = '';\n /** ISO timestamp for semantic activity metadata. */\n @Prop() updatedAt: string = '';\n @Prop() selected: boolean = false;\n @Prop() state: ConversationItemState = 'default';\n @Prop() statusLabel: string = '';\n @Prop() unreadCount: number = 0;\n /** Keep the contextual-actions surface visible while its popup is open or closing. */\n @Prop({ reflect: true }) actionsOpen: boolean = false;\n\n @Event() dsSelect!: EventEmitter<{ id: string }>;\n\n private handleClick = () => this.dsSelect.emit({ id: this.conversationId });\n\n private handleActionsSlotChange = (event: Event) => {\n const slot = event.target as HTMLSlotElement;\n this.el.classList.toggle(\n 'conversation-list-item--has-actions',\n slot.assignedElements().length > 0\n );\n };\n\n render() {\n const busy = this.state === 'busy';\n const subtext = busy && this.statusLabel ? this.statusLabel : this.preview;\n const accessibleName = [\n this.conversationTitle,\n subtext,\n this.unreadCount > 0 ? `${this.unreadCount} unread` : '',\n ]\n .filter(Boolean)\n .join('. ');\n\n return (\n <Host\n role=\"listitem\"\n class={{ 'conversation-list-item--actions-open': this.actionsOpen }}\n >\n <div class=\"conversation-list-item__row\">\n <button\n type=\"button\"\n class={{\n 'conversation-list-item': true,\n 'ds-choice-item': true,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--selected': this.selected,\n 'conversation-list-item--error': this.state === 'error',\n }}\n aria-label={accessibleName}\n aria-current={this.selected ? 'true' : undefined}\n aria-busy={busy ? 'true' : undefined}\n onClick={this.handleClick}\n >\n <span class=\"conversation-list-item__leading ds-choice-item__icon ds-interaction-fill__content\">\n <slot name=\"leading\" />\n </span>\n <span class=\"conversation-list-item__content ds-choice-item__content ds-interaction-fill__content\">\n <span class=\"conversation-list-item__title-row\">\n <ds-text\n class=\"conversation-list-item__title ds-choice-item__label\"\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis={this.unreadCount > 0}\n color={this.unreadCount > 0 ? 'primary' : 'secondary'}\n lineTruncation={1}\n >\n {this.conversationTitle}\n </ds-text>\n {this.unreadCount > 0 ? (\n <span class=\"conversation-list-item__unread\">\n <ds-badge variant=\"dot\" surface=\"secondary\" />\n </span>\n ) : null}\n </span>\n <span class=\"conversation-list-item__preview-row\">\n {busy ? <ds-loader size=\"sm\" color=\"secondary\" /> : null}\n <ds-text\n class=\"conversation-list-item__preview ds-choice-item__subtext\"\n as=\"span\"\n variant=\"text-body-small\"\n color={this.state === 'error' ? 'negative' : 'secondary'}\n lineTruncation={1}\n >\n {subtext}\n </ds-text>\n </span>\n {this.updatedAt ? (\n <time class=\"conversation-list-item__time ds-visually-hidden\" dateTime={this.updatedAt}>\n {this.updatedAt}\n </time>\n ) : null}\n </span>\n <span class=\"conversation-list-item__trailing ds-interaction-fill__content\">\n <slot name=\"trailing\" />\n </span>\n </button>\n <div class=\"conversation-list-item__actions ds-control-elevation ds-control-elevation--md\">\n <slot name=\"actions\" onSlotchange={this.handleActionsSlotChange} />\n </div>\n </div>\n </Host>\n );\n }\n}\n",
280
281
  "type": "registry:ui"
281
282
  }
282
283
  ]
@@ -37,7 +37,8 @@
37
37
  "The default floating action stays at the lower end corner.",
38
38
  "actionLayout footer stretches a long action between 8px side and bottom insets.",
39
39
  "Both action layouts share the always-active scroll-overlay fade controller, reserve measured end clearance, and begin fading 8px before the visible action.",
40
- "ConversationList paints its measured fade on a separate surface-aware layer so contextual row actions retain a real backdrop for blur; hovered, focused, and menu-open rows rise above that decorative fade."
40
+ "ConversationList paints its measured fade on a separate surface-aware layer so contextual row actions retain a real backdrop for blur; hovered, focused, and menu-open rows rise above that decorative fade.",
41
+ "The persistent action wrapper splits its md outer shadow from a top inset-highlight overlay so opaque filled actions keep the complete TokoMo elevation; slotted controls must disable optional resting borders."
41
42
  ],
42
43
  "responsiveBehavior": [
43
44
  "The list fills its owner, scrolls independently, and keeps its persistent action above scrolling rows."
@@ -116,12 +117,12 @@
116
117
  "files": [
117
118
  {
118
119
  "path": "src/wc/components/ConversationList/ConversationList.css",
119
- "content": ":host {\n display: block;\n position: relative;\n width: 100%;\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n -webkit-user-select: none;\n user-select: none;\n}\n\n.conversation-list {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n}\n\n.conversation-list__viewport {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n box-sizing: border-box;\n}\n\n.conversation-list__content {\n position: relative;\n min-height: 100%;\n padding: var(--dimension-space-100);\n padding-bottom: calc(\n var(--dimension-space-100) + var(--ds-scroll-overlay-block-size, 0px)\n );\n box-sizing: border-box;\n}\n\n.conversation-list__fade {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1;\n height: var(--ds-scroll-overlay-fade-block-size, 0);\n pointer-events: none;\n background-color: var(\n --ds-scroll-overlay-surface-color,\n var(--_shell-chrome-bg, var(--color-background-primary))\n );\n --ds-scroll-edge-fade-size: 100%;\n}\n\n.conversation-list__fade::before {\n content: '';\n position: absolute;\n inset: 0;\n background-image: var(\n --ds-scroll-overlay-surface-image,\n var(--ds-shell-gradient-image, none)\n );\n background-position: var(--ds-scroll-overlay-surface-position, 0 0);\n background-attachment: fixed;\n background-size: var(\n --ds-scroll-overlay-surface-size,\n var(--ds-shell-gradient-size, auto)\n );\n background-repeat: no-repeat;\n opacity: var(\n --ds-scroll-overlay-surface-image-opacity,\n var(--ds-shell-gradient-opacity, 1)\n );\n}\n\n.conversation-list__empty {\n display: contents;\n}\n\n.conversation-list__actions {\n position: absolute;\n right: 0;\n bottom: 0;\n z-index: var(--dimension-z-index-floating);\n padding: 0 var(--dimension-space-200) var(--dimension-space-200) 0;\n}\n\n.conversation-list__actions--footer {\n left: 0;\n padding: 0 var(--dimension-space-100) var(--dimension-space-100);\n}\n\n.conversation-list__actions-surface {\n display: flex;\n border-radius: var(--dimension-radius-half);\n box-shadow: var(--effect-elevation-elevated-md);\n}\n\n.conversation-list__actions--footer .conversation-list__actions-surface {\n width: 100%;\n}\n\n.conversation-list__actions-surface:empty {\n display: none;\n}\n",
120
+ "content": "@import '../../styles/control-elevation.css';\n\n:host {\n display: block;\n position: relative;\n width: 100%;\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n -webkit-user-select: none;\n user-select: none;\n}\n\n.conversation-list {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n}\n\n.conversation-list__viewport {\n position: relative;\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n box-sizing: border-box;\n}\n\n.conversation-list__content {\n position: relative;\n min-height: 100%;\n padding: var(--dimension-space-100);\n padding-bottom: calc(\n var(--dimension-space-100) + var(--ds-scroll-overlay-block-size, 0px)\n );\n box-sizing: border-box;\n}\n\n.conversation-list__fade {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1;\n height: var(--ds-scroll-overlay-fade-block-size, 0);\n pointer-events: none;\n background-color: var(\n --ds-scroll-overlay-surface-color,\n var(--_shell-chrome-bg, var(--color-background-primary))\n );\n --ds-scroll-edge-fade-size: 100%;\n}\n\n.conversation-list__fade::before {\n content: '';\n position: absolute;\n inset: 0;\n background-image: var(\n --ds-scroll-overlay-surface-image,\n var(--ds-shell-gradient-image, none)\n );\n background-position: var(--ds-scroll-overlay-surface-position, 0 0);\n background-attachment: fixed;\n background-size: var(\n --ds-scroll-overlay-surface-size,\n var(--ds-shell-gradient-size, auto)\n );\n background-repeat: no-repeat;\n opacity: var(\n --ds-scroll-overlay-surface-image-opacity,\n var(--ds-shell-gradient-opacity, 1)\n );\n}\n\n.conversation-list__empty {\n display: contents;\n}\n\n.conversation-list__actions {\n position: absolute;\n right: 0;\n bottom: 0;\n z-index: var(--dimension-z-index-floating);\n padding: 0 var(--dimension-space-200) var(--dimension-space-200) 0;\n}\n\n.conversation-list__actions--footer {\n left: 0;\n padding: 0 var(--dimension-space-100) var(--dimension-space-100);\n}\n\n.conversation-list__actions-surface {\n display: flex;\n border-radius: var(--dimension-radius-half);\n}\n\n.conversation-list__actions--footer .conversation-list__actions-surface {\n width: 100%;\n}\n\n.conversation-list__actions-surface:empty {\n display: none;\n}\n",
120
121
  "type": "registry:ui"
121
122
  },
122
123
  {
123
124
  "path": "src/wc/components/ConversationList/ConversationList.tsx",
124
- "content": "import { Component, Element, h, Host, Prop } from '@stencil/core';\nimport { ScrollOverlayController } from '../../utils/scroll-overlay-controller';\n\nexport type ConversationListActionLayout = 'floating' | 'footer';\n\n@Component({\n tag: 'ds-conversation-list',\n styleUrl: 'ConversationList.css',\n styleUrls: ['../../utils/scroll-edge-fade.css'],\n scoped: true,\n})\nexport class ConversationList {\n @Element() el!: HTMLElement;\n\n /** Corner icon action or full-width persistent footer action. */\n @Prop() actionLayout: ConversationListActionLayout = 'floating';\n\n private viewport?: HTMLElement;\n private content?: HTMLElement;\n private overlay?: HTMLElement;\n private scrollOverlayController?: ScrollOverlayController;\n private emptySlotObserver?: MutationObserver;\n\n componentDidLoad(): void {\n if (!this.viewport || !this.content || !this.overlay) return;\n this.scrollOverlayController = new ScrollOverlayController({\n host: this.el,\n viewport: this.viewport,\n content: this.content,\n overlay: this.overlay,\n overlayContentSelector: '[slot=\"actions\"]',\n });\n this.scrollOverlayController.connect();\n this.syncEmptyStateLayout();\n this.emptySlotObserver = new MutationObserver(this.syncEmptyStateLayout);\n this.emptySlotObserver.observe(this.el, { childList: true, subtree: true });\n }\n\n disconnectedCallback(): void {\n this.scrollOverlayController?.disconnect();\n this.emptySlotObserver?.disconnect();\n }\n\n private syncEmptyStateLayout = (): void => {\n const emptyState = this.el.querySelector<HTMLElement>('[slot=\"empty\"]');\n if (emptyState) {\n emptyState.style.setProperty('position', 'absolute');\n emptyState.style.setProperty('inset-block-start', '0');\n emptyState.style.setProperty('inset-inline', '0');\n emptyState.style.setProperty(\n 'inset-block-end',\n 'var(--ds-scroll-overlay-block-size, 0)',\n );\n emptyState.style.setProperty('height', 'auto');\n emptyState.style.setProperty('box-sizing', 'border-box');\n }\n this.scrollOverlayController?.sync();\n };\n\n render() {\n return (\n <Host>\n <div class=\"conversation-list\">\n <div\n class=\"conversation-list__viewport\"\n ref={element => {\n this.viewport = element;\n }}\n onScroll={() => this.scrollOverlayController?.sync()}\n >\n <div\n class=\"conversation-list__content\"\n ref={element => {\n this.content = element;\n }}\n >\n <slot />\n <div class=\"conversation-list__empty\">\n <slot name=\"empty\" />\n </div>\n </div>\n </div>\n <div\n class=\"conversation-list__fade scroll-edge-fade scroll-edge-fade--top\"\n aria-hidden=\"true\"\n />\n <div\n class={{\n 'conversation-list__actions': true,\n 'conversation-list__actions--footer': this.actionLayout === 'footer',\n }}\n ref={element => {\n this.overlay = element;\n }}\n >\n <div class=\"conversation-list__actions-surface\">\n <slot name=\"actions\" />\n </div>\n </div>\n </div>\n </Host>\n );\n }\n}\n",
125
+ "content": "import { Component, Element, h, Host, Prop } from '@stencil/core';\nimport { ScrollOverlayController } from '../../utils/scroll-overlay-controller';\n\nexport type ConversationListActionLayout = 'floating' | 'footer';\n\n@Component({\n tag: 'ds-conversation-list',\n styleUrl: 'ConversationList.css',\n styleUrls: ['../../utils/scroll-edge-fade.css'],\n scoped: true,\n})\nexport class ConversationList {\n @Element() el!: HTMLElement;\n\n /** Corner icon action or full-width persistent footer action. */\n @Prop() actionLayout: ConversationListActionLayout = 'floating';\n\n private viewport?: HTMLElement;\n private content?: HTMLElement;\n private overlay?: HTMLElement;\n private scrollOverlayController?: ScrollOverlayController;\n private emptySlotObserver?: MutationObserver;\n\n componentDidLoad(): void {\n if (!this.viewport || !this.content || !this.overlay) return;\n this.scrollOverlayController = new ScrollOverlayController({\n host: this.el,\n viewport: this.viewport,\n content: this.content,\n overlay: this.overlay,\n overlayContentSelector: '[slot=\"actions\"]',\n });\n this.scrollOverlayController.connect();\n this.syncEmptyStateLayout();\n this.emptySlotObserver = new MutationObserver(this.syncEmptyStateLayout);\n this.emptySlotObserver.observe(this.el, { childList: true, subtree: true });\n }\n\n disconnectedCallback(): void {\n this.scrollOverlayController?.disconnect();\n this.emptySlotObserver?.disconnect();\n }\n\n private syncEmptyStateLayout = (): void => {\n const emptyState = this.el.querySelector<HTMLElement>('[slot=\"empty\"]');\n if (emptyState) {\n emptyState.style.setProperty('position', 'absolute');\n emptyState.style.setProperty('inset-block-start', '0');\n emptyState.style.setProperty('inset-inline', '0');\n emptyState.style.setProperty(\n 'inset-block-end',\n 'var(--ds-scroll-overlay-block-size, 0)',\n );\n emptyState.style.setProperty('height', 'auto');\n emptyState.style.setProperty('box-sizing', 'border-box');\n }\n this.scrollOverlayController?.sync();\n };\n\n render() {\n return (\n <Host>\n <div class=\"conversation-list\">\n <div\n class=\"conversation-list__viewport\"\n ref={element => {\n this.viewport = element;\n }}\n onScroll={() => this.scrollOverlayController?.sync()}\n >\n <div\n class=\"conversation-list__content\"\n ref={element => {\n this.content = element;\n }}\n >\n <slot />\n <div class=\"conversation-list__empty\">\n <slot name=\"empty\" />\n </div>\n </div>\n </div>\n <div\n class=\"conversation-list__fade scroll-edge-fade scroll-edge-fade--top\"\n aria-hidden=\"true\"\n />\n <div\n class={{\n 'conversation-list__actions': true,\n 'conversation-list__actions--footer': this.actionLayout === 'footer',\n }}\n ref={element => {\n this.overlay = element;\n }}\n >\n <div class=\"conversation-list__actions-surface ds-control-elevation ds-control-elevation--md\">\n <slot name=\"actions\" />\n </div>\n </div>\n </div>\n </Host>\n );\n }\n}\n",
125
126
  "type": "registry:ui"
126
127
  }
127
128
  ]
@@ -590,7 +590,7 @@
590
590
  },
591
591
  {
592
592
  "path": "src/wc/components/Input/Input.tsx",
593
- "content": "import { AttachInternals, Component, Prop, State, Event, EventEmitter, Element, Method, Watch, h, Host } from '@stencil/core';\nimport { controlWidthClass, CONTROL_TEXT_VARIANT, type ControlWidth } from '../../utils';\n\nexport type InputType = 'text' | 'email' | 'tel' | 'url' | 'search' | 'password';\nexport type InputSize = 'md' | 'sm' | 'xs';\nexport type InputWidth = ControlWidth;\n\nconst ICON_SIZE: Record<InputSize, 'md' | 'sm' | 'xs'> = {\n md: 'md',\n sm: 'sm',\n xs: 'xs',\n};\n\nconst CLEAR_BUTTON_SIZE: Record<InputSize, 'sm' | 'xs'> = {\n md: 'sm',\n sm: 'xs',\n xs: 'xs',\n};\n\nlet idCounter = 0;\n\n@Component({\n tag: 'ds-input',\n styleUrl: 'Input.css',\n scoped: true,\n formAssociated: true,\n})\nexport class Input {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n private generatedId = `ds-input-${++idCounter}`;\n private errorId = `${this.generatedId}-error`;\n\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 /** Keeps the value focusable and submittable while preventing edits. */\n @Prop({ reflect: true }) readOnly: boolean = false;\n @Prop({ reflect: true }) required: boolean = false;\n @Prop() requiredMessage: string = 'This field is required.';\n @Prop() clearLabel: string = 'Clear';\n @Prop() placeholder: string | undefined;\n @Prop() type: InputType = 'text';\n /** Native browser autofill hint. */\n @Prop({ attribute: 'autocomplete' }) autoComplete: string | undefined;\n /** Preferred virtual keyboard without changing the value semantics. */\n @Prop({ attribute: 'inputmode' }) inputMode: string = '';\n /** Preferred virtual-keyboard action label. */\n @Prop({ attribute: 'enterkeyhint' }) enterKeyHint: string = '';\n /** Control density. */\n @Prop() size: InputSize = 'md';\n /** Width fit — fill the parent (default) or hug the available content. */\n @Prop() width: InputWidth = 'fill';\n /** Show the standard inset border. */\n @Prop() hasBorder: boolean = true;\n /** Optional leading icon name. */\n @Prop() icon: string | undefined;\n @Prop() isInactive: boolean = false;\n @Prop() autoFocus: boolean = false;\n @Prop() error: boolean = false;\n @Prop() errorMessage: string | undefined;\n /** Associates the internal input with an external <label>. */\n @Prop() inputId: string | undefined;\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\n @Event() dsChange!: EventEmitter<string>;\n @Event() dsClear!: EventEmitter<void>;\n\n private initialValue = '';\n @State() private formDisabled = false;\n @State() private hasSuffix = false;\n @State() private focused = false;\n @State() private touched = false;\n\n componentWillLoad() {\n this.initialValue = this.value;\n this.hasSuffix = Boolean(this.el.querySelector('[slot=\"suffix\"]'));\n this.syncFormValue();\n }\n\n componentDidRender() {\n const hasSuffix = Boolean(this.el.querySelector('[slot=\"suffix\"]'));\n if (hasSuffix !== this.hasSuffix) this.hasSuffix = hasSuffix;\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 this.internals.setFormValue(inactive ? null : this.value);\n const missing = this.required && !inactive && this.value.length === 0;\n this.internals.setValidity(missing ? { valueMissing: true } : {}, missing ? this.requiredMessage : '');\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.value = this.initialValue;\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n this.value = typeof state === 'string' ? state : '';\n }\n\n @Method()\n async setFocus() {\n this.el.querySelector('input')?.focus();\n }\n\n private handleInput = (e: Event) => {\n this.value = (e.target as HTMLInputElement).value;\n this.dsChange.emit(this.value);\n };\n\n private handleFocus = () => {\n this.focused = true;\n };\n\n private handleBlur = () => {\n this.focused = false;\n this.touched = true;\n };\n\n private handleClear = () => {\n this.value = '';\n this.dsChange.emit('');\n this.dsClear.emit();\n this.el.querySelector('input')?.focus();\n };\n\n render() {\n const inputId = this.inputId ?? this.generatedId;\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n const filled = this.value.length > 0;\n const dirty = this.value !== this.initialValue;\n const showClear = this.type === 'search' && filled && !inactive && !this.readOnly;\n const showError = this.error && Boolean(this.errorMessage);\n const textVariant = CONTROL_TEXT_VARIANT[this.size];\n const iconSize = ICON_SIZE[this.size];\n\n const describedBy = [\n this.ariaDescribedby,\n showError ? this.errorId : undefined,\n ].filter(Boolean).join(' ') || undefined;\n\n return (\n <Host\n class={{\n 'input-host': true,\n 'ds-control-inactive': inactive,\n [`ds-control--${this.size}`]: true,\n ...controlWidthClass(this.width),\n }}\n data-disabled={inactive ? '' : undefined}\n data-readonly={this.readOnly ? '' : undefined}\n data-required={this.required ? '' : undefined}\n data-invalid={this.error ? '' : undefined}\n data-filled={filled ? '' : undefined}\n data-focused={this.focused ? '' : undefined}\n data-dirty={dirty ? '' : undefined}\n data-touched={this.touched ? '' : undefined}\n >\n <div\n class={{\n 'input-control': true,\n 'input-control--bordered': this.hasBorder || this.error,\n 'input-control--error': this.error,\n 'ds-interaction-fill': true,\n [`ds-control--${this.size}`]: true,\n }}\n >\n {this.icon && (\n <span class=\"input-control__prefix ds-interaction-fill__content\" aria-hidden=\"true\">\n <ds-icon name={this.icon} size={iconSize} color=\"inherit\" />\n </span>\n )}\n <input\n type={this.type}\n id={inputId}\n value={this.value}\n placeholder={this.placeholder}\n disabled={inactive}\n readOnly={this.readOnly}\n required={this.required}\n autoFocus={this.autoFocus}\n autoComplete={this.autoComplete}\n inputMode={this.inputMode || undefined}\n enterKeyHint={this.enterKeyHint || undefined}\n class={`native-input ds-text--${textVariant.replace('text-', '')} ds-text--regular ds-interaction-fill__content`}\n aria-label={this.ariaLabel}\n aria-labelledby={this.ariaLabelledby}\n aria-describedby={describedBy}\n aria-invalid={this.error ? 'true' : undefined}\n onInput={this.handleInput}\n onFocus={this.handleFocus}\n onBlur={this.handleBlur}\n />\n <span class={{ 'input-control__suffix': true, 'input-control__suffix--empty': !this.hasSuffix }}>\n <slot name=\"suffix\" />\n </span>\n {showClear && (\n <ds-button-unfilled\n class=\"input-control__clear\"\n variant=\"icon\"\n size={CLEAR_BUTTON_SIZE[this.size]}\n icon=\"CrossCircle\"\n hasBorder={false}\n rounded\n ariaLabel={this.clearLabel}\n onDsClick={this.handleClear}\n />\n )}\n </div>\n {showError && (\n <ds-text\n class=\"error-text\"\n as=\"div\"\n variant=\"text-body-small\"\n color=\"negative\"\n textId={this.errorId}\n role=\"alert\"\n >\n {this.errorMessage}\n </ds-text>\n )}\n </Host>\n );\n }\n}\n",
593
+ "content": "import { AttachInternals, Component, Prop, State, Event, EventEmitter, Element, Method, Watch, h, Host } from '@stencil/core';\nimport {\n controlWidthClass,\n CONTROL_TEXT_VARIANT,\n DEFAULT_REQUIRED_MESSAGE,\n setRequiredValidity,\n type ControlWidth,\n} from '../../utils';\n\nexport type InputType = 'text' | 'email' | 'tel' | 'url' | 'search' | 'password';\nexport type InputSize = 'md' | 'sm' | 'xs';\nexport type InputWidth = ControlWidth;\n\nconst ICON_SIZE: Record<InputSize, 'md' | 'sm' | 'xs'> = {\n md: 'md',\n sm: 'sm',\n xs: 'xs',\n};\n\nconst CLEAR_BUTTON_SIZE: Record<InputSize, 'sm' | 'xs'> = {\n md: 'sm',\n sm: 'xs',\n xs: 'xs',\n};\n\nlet idCounter = 0;\n\n@Component({\n tag: 'ds-input',\n styleUrl: 'Input.css',\n scoped: true,\n formAssociated: true,\n})\nexport class Input {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n private generatedId = `ds-input-${++idCounter}`;\n private errorId = `${this.generatedId}-error`;\n\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 /** Keeps the value focusable and submittable while preventing edits. */\n @Prop({ reflect: true }) readOnly: boolean = false;\n @Prop({ reflect: true }) required: boolean = false;\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n @Prop() clearLabel: string = 'Clear';\n @Prop() placeholder: string | undefined;\n @Prop() type: InputType = 'text';\n /** Native browser autofill hint. */\n @Prop({ attribute: 'autocomplete' }) autoComplete: string | undefined;\n /** Preferred virtual keyboard without changing the value semantics. */\n @Prop({ attribute: 'inputmode' }) inputMode: string = '';\n /** Preferred virtual-keyboard action label. */\n @Prop({ attribute: 'enterkeyhint' }) enterKeyHint: string = '';\n /** Control density. */\n @Prop() size: InputSize = 'md';\n /** Width fit — fill the parent (default) or hug the available content. */\n @Prop() width: InputWidth = 'fill';\n /** Show the standard inset border. */\n @Prop() hasBorder: boolean = true;\n /** Optional leading icon name. */\n @Prop() icon: string | undefined;\n @Prop() isInactive: boolean = false;\n @Prop() autoFocus: boolean = false;\n @Prop() error: boolean = false;\n @Prop() errorMessage: string | undefined;\n /** Associates the internal input with an external <label>. */\n @Prop() inputId: string | undefined;\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\n @Event() dsChange!: EventEmitter<string>;\n @Event() dsClear!: EventEmitter<void>;\n\n private initialValue = '';\n @State() private formDisabled = false;\n @State() private hasSuffix = false;\n @State() private focused = false;\n @State() private touched = false;\n\n componentWillLoad() {\n this.initialValue = this.value;\n this.hasSuffix = Boolean(this.el.querySelector('[slot=\"suffix\"]'));\n this.syncFormValue();\n }\n\n componentDidRender() {\n const hasSuffix = Boolean(this.el.querySelector('[slot=\"suffix\"]'));\n if (hasSuffix !== this.hasSuffix) this.hasSuffix = hasSuffix;\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 this.internals.setFormValue(inactive ? null : this.value);\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 = typeof state === 'string' ? state : '';\n }\n\n @Method()\n async setFocus() {\n this.el.querySelector('input')?.focus();\n }\n\n private handleInput = (e: Event) => {\n this.value = (e.target as HTMLInputElement).value;\n this.dsChange.emit(this.value);\n };\n\n private handleFocus = () => {\n this.focused = true;\n };\n\n private handleBlur = () => {\n this.focused = false;\n this.touched = true;\n };\n\n private handleClear = () => {\n this.value = '';\n this.dsChange.emit('');\n this.dsClear.emit();\n this.el.querySelector('input')?.focus();\n };\n\n render() {\n const inputId = this.inputId ?? this.generatedId;\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n const filled = this.value.length > 0;\n const dirty = this.value !== this.initialValue;\n const showClear = this.type === 'search' && filled && !inactive && !this.readOnly;\n const showError = this.error && Boolean(this.errorMessage);\n const textVariant = CONTROL_TEXT_VARIANT[this.size];\n const iconSize = ICON_SIZE[this.size];\n\n const describedBy = [\n this.ariaDescribedby,\n showError ? this.errorId : undefined,\n ].filter(Boolean).join(' ') || undefined;\n\n return (\n <Host\n class={{\n 'input-host': true,\n 'ds-control-inactive': inactive,\n [`ds-control--${this.size}`]: true,\n ...controlWidthClass(this.width),\n }}\n data-disabled={inactive ? '' : undefined}\n data-readonly={this.readOnly ? '' : undefined}\n data-required={this.required ? '' : undefined}\n data-invalid={this.error ? '' : undefined}\n data-filled={filled ? '' : undefined}\n data-focused={this.focused ? '' : undefined}\n data-dirty={dirty ? '' : undefined}\n data-touched={this.touched ? '' : undefined}\n >\n <div\n class={{\n 'input-control': true,\n 'input-control--bordered': this.hasBorder || this.error,\n 'input-control--error': this.error,\n 'ds-interaction-fill': true,\n [`ds-control--${this.size}`]: true,\n }}\n >\n {this.icon && (\n <span class=\"input-control__prefix ds-interaction-fill__content\" aria-hidden=\"true\">\n <ds-icon name={this.icon} size={iconSize} color=\"inherit\" />\n </span>\n )}\n <input\n type={this.type}\n id={inputId}\n value={this.value}\n placeholder={this.placeholder}\n disabled={inactive}\n readOnly={this.readOnly}\n required={this.required}\n autoFocus={this.autoFocus}\n autoComplete={this.autoComplete}\n inputMode={this.inputMode || undefined}\n enterKeyHint={this.enterKeyHint || undefined}\n class={`native-input ds-text--${textVariant.replace('text-', '')} ds-text--regular ds-interaction-fill__content`}\n aria-label={this.ariaLabel}\n aria-labelledby={this.ariaLabelledby}\n aria-describedby={describedBy}\n aria-invalid={this.error ? 'true' : undefined}\n onInput={this.handleInput}\n onFocus={this.handleFocus}\n onBlur={this.handleBlur}\n />\n <span class={{ 'input-control__suffix': true, 'input-control__suffix--empty': !this.hasSuffix }}>\n <slot name=\"suffix\" />\n </span>\n {showClear && (\n <ds-button-unfilled\n class=\"input-control__clear\"\n variant=\"icon\"\n size={CLEAR_BUTTON_SIZE[this.size]}\n icon=\"CrossCircle\"\n hasBorder={false}\n rounded\n ariaLabel={this.clearLabel}\n onDsClick={this.handleClear}\n />\n )}\n </div>\n {showError && (\n <ds-text\n class=\"error-text\"\n as=\"div\"\n variant=\"text-body-small\"\n color=\"negative\"\n textId={this.errorId}\n role=\"alert\"\n >\n {this.errorMessage}\n </ds-text>\n )}\n </Host>\n );\n }\n}\n",
594
594
  "type": "registry:ui"
595
595
  }
596
596
  ]
@@ -180,12 +180,12 @@
180
180
  "files": [
181
181
  {
182
182
  "path": "src/wc/components/Loader/Loader.css",
183
- "content": "/* ── Base loader (mirrors ds-icon box metrics) ───────────────────────────── */\n\n.loader {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n line-height: 0;\n color: inherit;\n width: var(--_loader-sz, var(--dimension-iconography-md));\n height: var(--_loader-sz, var(--dimension-iconography-md));\n animation: loader-spin var(--effect-animation-duration-long-2) var(--effect-animation-easing-linear) infinite;\n}\n\n.loader__svg {\n display: block;\n flex-shrink: 0;\n width: 100%;\n height: 100%;\n}\n\n.status {\n display: inline-flex;\n align-items: center;\n}\n\n.visually-hidden {\n position: absolute;\n /* stylelint-disable declaration-property-value-disallowed-list -- classic visually-hidden clip pattern */\n width: 1px;\n height: 1px;\n padding: 0;\n margin: -1px;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n white-space: nowrap;\n border: 0;\n /* stylelint-enable declaration-property-value-disallowed-list */\n}\n\n/* Anti-clockwise continuous spin — 1000ms via long-2. */\n@keyframes loader-spin {\n from { transform: rotate(0deg); }\n to { transform: rotate(-360deg); }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .loader {\n animation: none;\n transform: none;\n }\n}\n\n/* ── Sizes (same tokens as ds-icon) ──────────────────────────────────────── */\n\n.loader--size-xs { --_loader-sz: var(--dimension-iconography-xs); }\n.loader--size-sm { --_loader-sz: var(--dimension-iconography-sm); }\n.loader--size-md { --_loader-sz: var(--dimension-iconography-md); }\n.loader--size-lg { --_loader-sz: var(--dimension-iconography-lg); }\n.loader--size-xl { --_loader-sz: var(--dimension-iconography-xl); }\n.loader--size-2xl { --_loader-sz: var(--dimension-iconography-2xl); }\n.loader--size-3xl { --_loader-sz: var(--dimension-iconography-3xl); }\n\n/* ── Colors — hierarchy ──────────────────────────────────────────────────── */\n\n.loader--color-primary { color: var(--color-foreground-primary); }\n.loader--color-secondary { color: var(--color-foreground-secondary); }\n.loader--color-tertiary { color: var(--color-foreground-tertiary); }\n.loader--color-quaternary { color: var(--color-foreground-quaternary); }\n.loader--color-inherit { color: inherit; }\n\n/* ── Colors — intent (bold contrast default) ─────────────────────────────── */\n\n.loader--color-brand { color: var(--color-foreground-bold-brand); }\n.loader--color-negative { color: var(--color-foreground-bold-negative); }\n.loader--color-positive { color: var(--color-foreground-bold-positive); }\n.loader--color-warning { color: var(--color-foreground-bold-warning); }\n.loader--color-caution { color: var(--color-foreground-bold-caution); }\n.loader--color-ai { color: var(--color-foreground-bold-ai); }\n.loader--color-guide { color: var(--color-foreground-bold-guide); }\n.loader--color-neutral { color: var(--color-foreground-bold-neutral); }\n\n/* ── Colors — intent faint ───────────────────────────────────────────────── */\n\n.loader--color-faint-brand { color: var(--color-foreground-faint-brand); }\n.loader--color-faint-negative { color: var(--color-foreground-faint-negative); }\n.loader--color-faint-positive { color: var(--color-foreground-faint-positive); }\n.loader--color-faint-warning { color: var(--color-foreground-faint-warning); }\n.loader--color-faint-caution { color: var(--color-foreground-faint-caution); }\n.loader--color-faint-ai { color: var(--color-foreground-faint-ai); }\n.loader--color-faint-neutral { color: var(--color-foreground-faint-neutral); }\n\n/* ── Colors — intent medium ─────────────────────────────────────────────── */\n\n.loader--color-medium-brand { color: var(--color-foreground-medium-brand); }\n.loader--color-medium-negative { color: var(--color-foreground-medium-negative); }\n.loader--color-medium-positive { color: var(--color-foreground-medium-positive); }\n.loader--color-medium-warning { color: var(--color-foreground-medium-warning); }\n.loader--color-medium-caution { color: var(--color-foreground-medium-caution); }\n.loader--color-medium-ai { color: var(--color-foreground-medium-ai); }\n.loader--color-medium-neutral { color: var(--color-foreground-medium-neutral); }\n\n/* ── Colors — intent bold (explicit) ────────────────────────────────────── */\n\n.loader--color-bold-brand { color: var(--color-foreground-bold-brand); }\n.loader--color-bold-negative { color: var(--color-foreground-bold-negative); }\n.loader--color-bold-positive { color: var(--color-foreground-bold-positive); }\n.loader--color-bold-warning { color: var(--color-foreground-bold-warning); }\n.loader--color-bold-caution { color: var(--color-foreground-bold-caution); }\n.loader--color-bold-ai { color: var(--color-foreground-bold-ai); }\n.loader--color-bold-neutral { color: var(--color-foreground-bold-neutral); }\n\n/* ── Colors — intent strong ──────────────────────────────────────────────── */\n\n.loader--color-strong-brand { color: var(--color-foreground-strong-brand); }\n.loader--color-strong-negative { color: var(--color-foreground-strong-negative); }\n.loader--color-strong-positive { color: var(--color-foreground-strong-positive); }\n.loader--color-strong-warning { color: var(--color-foreground-strong-warning); }\n.loader--color-strong-caution { color: var(--color-foreground-strong-caution); }\n.loader--color-strong-ai { color: var(--color-foreground-strong-ai); }\n.loader--color-strong-neutral { color: var(--color-foreground-strong-neutral); }\n\n/* ── Colors — contextual ────────────────────────────────────────────────── */\n\n.loader--color-on-strong { color: var(--color-foreground-on-strong-background-primary); }\n.loader--color-on-bold { color: var(--color-foreground-on-bold-background-primary); }\n",
183
+ "content": "@import '../../utils/visually-hidden.css';\n\n/* ── Base loader (mirrors ds-icon box metrics) ───────────────────────────── */\n\n.loader {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n line-height: 0;\n color: inherit;\n width: var(--_loader-sz, var(--dimension-iconography-md));\n height: var(--_loader-sz, var(--dimension-iconography-md));\n animation: loader-spin var(--effect-animation-duration-long-2) var(--effect-animation-easing-linear) infinite;\n}\n\n.loader__svg {\n display: block;\n flex-shrink: 0;\n width: 100%;\n height: 100%;\n}\n\n.status {\n display: inline-flex;\n align-items: center;\n}\n\n/* Anti-clockwise continuous spin — 1000ms via long-2. */\n@keyframes loader-spin {\n from { transform: rotate(0deg); }\n to { transform: rotate(-360deg); }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .loader {\n animation: none;\n transform: none;\n }\n}\n\n/* ── Sizes (same tokens as ds-icon) ──────────────────────────────────────── */\n\n.loader--size-xs { --_loader-sz: var(--dimension-iconography-xs); }\n.loader--size-sm { --_loader-sz: var(--dimension-iconography-sm); }\n.loader--size-md { --_loader-sz: var(--dimension-iconography-md); }\n.loader--size-lg { --_loader-sz: var(--dimension-iconography-lg); }\n.loader--size-xl { --_loader-sz: var(--dimension-iconography-xl); }\n.loader--size-2xl { --_loader-sz: var(--dimension-iconography-2xl); }\n.loader--size-3xl { --_loader-sz: var(--dimension-iconography-3xl); }\n\n/* ── Colors — hierarchy ──────────────────────────────────────────────────── */\n\n.loader--color-primary { color: var(--color-foreground-primary); }\n.loader--color-secondary { color: var(--color-foreground-secondary); }\n.loader--color-tertiary { color: var(--color-foreground-tertiary); }\n.loader--color-quaternary { color: var(--color-foreground-quaternary); }\n.loader--color-inherit { color: inherit; }\n\n/* ── Colors — intent (bold contrast default) ─────────────────────────────── */\n\n.loader--color-brand { color: var(--color-foreground-bold-brand); }\n.loader--color-negative { color: var(--color-foreground-bold-negative); }\n.loader--color-positive { color: var(--color-foreground-bold-positive); }\n.loader--color-warning { color: var(--color-foreground-bold-warning); }\n.loader--color-caution { color: var(--color-foreground-bold-caution); }\n.loader--color-ai { color: var(--color-foreground-bold-ai); }\n.loader--color-guide { color: var(--color-foreground-bold-guide); }\n.loader--color-neutral { color: var(--color-foreground-bold-neutral); }\n\n/* ── Colors — intent faint ───────────────────────────────────────────────── */\n\n.loader--color-faint-brand { color: var(--color-foreground-faint-brand); }\n.loader--color-faint-negative { color: var(--color-foreground-faint-negative); }\n.loader--color-faint-positive { color: var(--color-foreground-faint-positive); }\n.loader--color-faint-warning { color: var(--color-foreground-faint-warning); }\n.loader--color-faint-caution { color: var(--color-foreground-faint-caution); }\n.loader--color-faint-ai { color: var(--color-foreground-faint-ai); }\n.loader--color-faint-neutral { color: var(--color-foreground-faint-neutral); }\n\n/* ── Colors — intent medium ─────────────────────────────────────────────── */\n\n.loader--color-medium-brand { color: var(--color-foreground-medium-brand); }\n.loader--color-medium-negative { color: var(--color-foreground-medium-negative); }\n.loader--color-medium-positive { color: var(--color-foreground-medium-positive); }\n.loader--color-medium-warning { color: var(--color-foreground-medium-warning); }\n.loader--color-medium-caution { color: var(--color-foreground-medium-caution); }\n.loader--color-medium-ai { color: var(--color-foreground-medium-ai); }\n.loader--color-medium-neutral { color: var(--color-foreground-medium-neutral); }\n\n/* ── Colors — intent bold (explicit) ────────────────────────────────────── */\n\n.loader--color-bold-brand { color: var(--color-foreground-bold-brand); }\n.loader--color-bold-negative { color: var(--color-foreground-bold-negative); }\n.loader--color-bold-positive { color: var(--color-foreground-bold-positive); }\n.loader--color-bold-warning { color: var(--color-foreground-bold-warning); }\n.loader--color-bold-caution { color: var(--color-foreground-bold-caution); }\n.loader--color-bold-ai { color: var(--color-foreground-bold-ai); }\n.loader--color-bold-neutral { color: var(--color-foreground-bold-neutral); }\n\n/* ── Colors — intent strong ──────────────────────────────────────────────── */\n\n.loader--color-strong-brand { color: var(--color-foreground-strong-brand); }\n.loader--color-strong-negative { color: var(--color-foreground-strong-negative); }\n.loader--color-strong-positive { color: var(--color-foreground-strong-positive); }\n.loader--color-strong-warning { color: var(--color-foreground-strong-warning); }\n.loader--color-strong-caution { color: var(--color-foreground-strong-caution); }\n.loader--color-strong-ai { color: var(--color-foreground-strong-ai); }\n.loader--color-strong-neutral { color: var(--color-foreground-strong-neutral); }\n\n/* ── Colors — contextual ────────────────────────────────────────────────── */\n\n.loader--color-on-strong { color: var(--color-foreground-on-strong-background-primary); }\n.loader--color-on-bold { color: var(--color-foreground-on-bold-background-primary); }\n",
184
184
  "type": "registry:ui"
185
185
  },
186
186
  {
187
187
  "path": "src/wc/components/Loader/Loader.tsx",
188
- "content": "import { Component, Prop, h, Host } from '@stencil/core';\nimport type { IconColor, IconSize } from '../Icon/Icon';\n\nexport type LoaderSize = IconSize;\nexport type LoaderColor = IconColor;\n\n@Component({\n tag: 'ds-loader',\n styleUrl: 'Loader.css',\n shadow: true,\n})\nexport class Loader {\n /** Iconography size token. Maps to `--dimension-iconography-{size}`. Default `md` = 20 px. */\n @Prop() size: LoaderSize = 'md';\n /**\n * Semantic foreground color token, or a raw CSS var reference. Same tokens\n * as `ds-icon`. Do not use `tertiary` or `quaternary` for a standalone or\n * informative Loader; inherit a readable color from the owning busy control\n * or region.\n */\n @Prop() color: LoaderColor = 'inherit';\n /**\n * Accessible label for standalone usage. Wraps the spinner in a live region\n * and renders the label visually-hidden. Omit when the host element already\n * conveys busy state via aria-busy.\n */\n @Prop() label: string | undefined;\n\n render() {\n const isCustomColor = typeof this.color === 'string' && this.color.startsWith('var(--');\n\n const cls: Record<string, boolean> = {\n loader: true,\n [`loader--size-${this.size}`]: true,\n [`loader--color-${this.color}`]: !!this.color && !isCustomColor && this.color !== 'inherit',\n 'loader--color-inherit': this.color === 'inherit',\n };\n\n const style = isCustomColor ? { color: this.color as string } : undefined;\n\n const spinner = (\n <span class={cls} style={style} aria-hidden=\"true\">\n <svg class=\"loader__svg\" viewBox=\"0 0 16 16\" fill=\"none\" focusable=\"false\">\n <path\n opacity=\"0.1\"\n d=\"M3 8C3 5.23858 5.23858 3 8 3V2C4.68629 2 2 4.68629 2 8C2 9.99456 2.97323 11.7618 4.47072 12.8528L5.05894 12.044C3.81102 11.1348 3 9.66213 3 8Z\"\n fill=\"currentColor\"\n />\n <path\n opacity=\"0.2\"\n fill-rule=\"evenodd\"\n clip-rule=\"evenodd\"\n d=\"M5.05892 12.0438C5.88425 12.6451 6.90069 12.9998 7.99998 12.9998C9.66211 12.9998 11.1348 12.1888 12.0439 10.9409L12.8527 11.5291C11.7618 13.0266 9.99454 13.9998 7.99998 13.9998C6.68083 13.9998 5.46111 13.5741 4.4707 12.8526L5.05892 12.0438Z\"\n fill=\"currentColor\"\n />\n <path\n opacity=\"0.4\"\n fill-rule=\"evenodd\"\n clip-rule=\"evenodd\"\n d=\"M12.0435 10.9408C12.6447 10.1155 12.9995 9.09906 12.9995 7.99976C12.9995 7.45884 12.9136 6.93798 12.7547 6.45007L13.7057 6.14014C13.8964 6.72562 13.9995 7.35065 13.9995 7.99976C13.9995 9.31891 13.5738 10.5386 12.8523 11.529L12.0435 10.9408Z\"\n fill=\"currentColor\"\n />\n <path\n opacity=\"0.6\"\n fill-rule=\"evenodd\"\n clip-rule=\"evenodd\"\n d=\"M12.7555 6.45152C12.4263 5.43958 11.7831 4.56933 10.9409 3.95577L11.5291 3.14697C12.5398 3.88325 13.3116 4.92755 13.7067 6.14188L12.7555 6.45152Z\"\n fill=\"currentColor\"\n />\n <path\n opacity=\"0.8\"\n fill-rule=\"evenodd\"\n clip-rule=\"evenodd\"\n d=\"M10.941 3.95583C10.5307 3.65688 10.0731 3.41886 9.58154 3.25509L9.89786 2.30615C10.4877 2.50267 11.0368 2.78829 11.5293 3.14704L10.941 3.95583Z\"\n fill=\"currentColor\"\n />\n <path\n fill-rule=\"evenodd\"\n clip-rule=\"evenodd\"\n d=\"M9.89788 2.30636C9.3014 2.10762 8.66327 2 8 2V3C8.55272 3 9.0845 3.08968 9.58157 3.2553L9.89788 2.30636Z\"\n fill=\"currentColor\"\n />\n </svg>\n </span>\n );\n\n if (!this.label) {\n return (\n <Host style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>\n {spinner}\n </Host>\n );\n }\n\n return (\n <Host style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>\n <span role=\"status\" aria-live=\"polite\" class=\"status\">\n {spinner}\n <span class=\"visually-hidden\">{this.label}</span>\n </span>\n </Host>\n );\n }\n}\n",
188
+ "content": "import { Component, Prop, h, Host } from '@stencil/core';\nimport type { IconColor, IconSize } from '../Icon/Icon';\n\nexport type LoaderSize = IconSize;\nexport type LoaderColor = IconColor;\n\n@Component({\n tag: 'ds-loader',\n styleUrl: 'Loader.css',\n shadow: true,\n})\nexport class Loader {\n /** Iconography size token. Maps to `--dimension-iconography-{size}`. Default `md` = 20 px. */\n @Prop() size: LoaderSize = 'md';\n /**\n * Semantic foreground color token, or a raw CSS var reference. Same tokens\n * as `ds-icon`. Do not use `tertiary` or `quaternary` for a standalone or\n * informative Loader; inherit a readable color from the owning busy control\n * or region.\n */\n @Prop() color: LoaderColor = 'inherit';\n /**\n * Accessible label for standalone usage. Wraps the spinner in a live region\n * and renders the label visually-hidden. Omit when the host element already\n * conveys busy state via aria-busy.\n */\n @Prop() label: string | undefined;\n\n render() {\n const isCustomColor = typeof this.color === 'string' && this.color.startsWith('var(--');\n\n const cls: Record<string, boolean> = {\n loader: true,\n [`loader--size-${this.size}`]: true,\n [`loader--color-${this.color}`]: !!this.color && !isCustomColor && this.color !== 'inherit',\n 'loader--color-inherit': this.color === 'inherit',\n };\n\n const style = isCustomColor ? { color: this.color as string } : undefined;\n\n const spinner = (\n <span class={cls} style={style} aria-hidden=\"true\">\n <svg class=\"loader__svg\" viewBox=\"0 0 16 16\" fill=\"none\" focusable=\"false\">\n <path\n opacity=\"0.1\"\n d=\"M3 8C3 5.23858 5.23858 3 8 3V2C4.68629 2 2 4.68629 2 8C2 9.99456 2.97323 11.7618 4.47072 12.8528L5.05894 12.044C3.81102 11.1348 3 9.66213 3 8Z\"\n fill=\"currentColor\"\n />\n <path\n opacity=\"0.2\"\n fill-rule=\"evenodd\"\n clip-rule=\"evenodd\"\n d=\"M5.05892 12.0438C5.88425 12.6451 6.90069 12.9998 7.99998 12.9998C9.66211 12.9998 11.1348 12.1888 12.0439 10.9409L12.8527 11.5291C11.7618 13.0266 9.99454 13.9998 7.99998 13.9998C6.68083 13.9998 5.46111 13.5741 4.4707 12.8526L5.05892 12.0438Z\"\n fill=\"currentColor\"\n />\n <path\n opacity=\"0.4\"\n fill-rule=\"evenodd\"\n clip-rule=\"evenodd\"\n d=\"M12.0435 10.9408C12.6447 10.1155 12.9995 9.09906 12.9995 7.99976C12.9995 7.45884 12.9136 6.93798 12.7547 6.45007L13.7057 6.14014C13.8964 6.72562 13.9995 7.35065 13.9995 7.99976C13.9995 9.31891 13.5738 10.5386 12.8523 11.529L12.0435 10.9408Z\"\n fill=\"currentColor\"\n />\n <path\n opacity=\"0.6\"\n fill-rule=\"evenodd\"\n clip-rule=\"evenodd\"\n d=\"M12.7555 6.45152C12.4263 5.43958 11.7831 4.56933 10.9409 3.95577L11.5291 3.14697C12.5398 3.88325 13.3116 4.92755 13.7067 6.14188L12.7555 6.45152Z\"\n fill=\"currentColor\"\n />\n <path\n opacity=\"0.8\"\n fill-rule=\"evenodd\"\n clip-rule=\"evenodd\"\n d=\"M10.941 3.95583C10.5307 3.65688 10.0731 3.41886 9.58154 3.25509L9.89786 2.30615C10.4877 2.50267 11.0368 2.78829 11.5293 3.14704L10.941 3.95583Z\"\n fill=\"currentColor\"\n />\n <path\n fill-rule=\"evenodd\"\n clip-rule=\"evenodd\"\n d=\"M9.89788 2.30636C9.3014 2.10762 8.66327 2 8 2V3C8.55272 3 9.0845 3.08968 9.58157 3.2553L9.89788 2.30636Z\"\n fill=\"currentColor\"\n />\n </svg>\n </span>\n );\n\n if (!this.label) {\n return (\n <Host style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>\n {spinner}\n </Host>\n );\n }\n\n return (\n <Host style={{ display: 'inline-flex', alignItems: 'center', justifyContent: 'center' }}>\n <span role=\"status\" aria-live=\"polite\" class=\"status\">\n {spinner}\n <span class=\"visually-hidden ds-visually-hidden\">{this.label}</span>\n </span>\n </Host>\n );\n }\n}\n",
189
189
  "type": "registry:ui"
190
190
  }
191
191
  ]
@@ -36,6 +36,7 @@
36
36
  "Selection and transcript interaction release automatic following.",
37
37
  "Opening saved history, message jumps, and prepended history preserve deliberate position.",
38
38
  "The scroll-to-latest action is absent at the live edge, appears only after the reader moves away, and disappears again after returning to the newest message.",
39
+ "Scroll to latest uses the shared md control-elevation wrapper with a pointer-transparent top highlight, preserving its transparent backdrop blur and borderless unfilled action.",
39
40
  "Place a composer container with 8px padding on every side in the overlay slot so messages scroll behind it; the shared scroll-overlay controller tracks every composer growth step, reserves matching transcript clearance, begins the fade 8px before the visible composer, and keeps scroll-to-latest above it."
40
41
  ],
41
42
  "responsiveBehavior": [
@@ -208,12 +209,12 @@
208
209
  "files": [
209
210
  {
210
211
  "path": "src/wc/components/MessageScroller/MessageScroller.css",
211
- "content": ":host {\n display: block;\n width: 100%;\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n}\n\n.message-scroller {\n position: relative;\n width: 100%;\n height: 100%;\n min-height: 0;\n}\n\n.message-scroller__viewport {\n width: 100%;\n height: 100%;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n box-sizing: border-box;\n}\n\n.message-scroller__viewport:focus-visible {\n outline: var(--dimension-stroke-width-025) solid var(--color-border-bold-brand);\n outline-offset: calc(-1 * var(--dimension-stroke-width-025));\n}\n\n.message-scroller__content {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-300);\n width: 100%;\n max-width: var(\n --ds-message-scroller-content-max-width,\n var(--dimension-panel-width-2xl)\n );\n min-height: 100%;\n margin-inline: auto;\n padding-block-start: var(--dimension-space-200);\n padding-inline: var(--dimension-space-200);\n padding-block-end: calc(\n var(--dimension-space-200) + var(--ds-scroll-overlay-block-size, 0px)\n );\n box-sizing: border-box;\n}\n\n.message-scroller__overlay {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: var(--dimension-z-index-floating);\n}\n\n.message-scroller__control {\n position: absolute;\n bottom: calc(\n var(--ds-scroll-overlay-content-block-size, 0px) + var(--dimension-space-200)\n );\n left: 50%;\n z-index: var(--dimension-z-index-floating);\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--dimension-radius-half);\n background: var(--color-background-transparent);\n box-shadow: var(--effect-elevation-elevated-md);\n transform: translateX(-50%);\n -webkit-backdrop-filter: blur(var(--effect-blur-sm));\n backdrop-filter: blur(var(--effect-blur-sm));\n}\n",
212
+ "content": "@import '../../styles/control-elevation.css';\n\n:host {\n display: block;\n width: 100%;\n height: 100%;\n min-height: 0;\n box-sizing: border-box;\n}\n\n.message-scroller {\n position: relative;\n width: 100%;\n height: 100%;\n min-height: 0;\n}\n\n.message-scroller__viewport {\n width: 100%;\n height: 100%;\n min-height: 0;\n overflow: auto;\n overscroll-behavior: contain;\n box-sizing: border-box;\n}\n\n.message-scroller__viewport:focus-visible {\n outline: var(--dimension-stroke-width-025) solid var(--color-border-bold-brand);\n outline-offset: calc(-1 * var(--dimension-stroke-width-025));\n}\n\n.message-scroller__content {\n display: flex;\n flex-direction: column;\n gap: var(--dimension-space-300);\n width: 100%;\n max-width: var(\n --ds-message-scroller-content-max-width,\n var(--dimension-panel-width-2xl)\n );\n min-height: 100%;\n margin-inline: auto;\n padding-block-start: var(--dimension-space-200);\n padding-inline: var(--dimension-space-200);\n padding-block-end: calc(\n var(--dimension-space-200) + var(--ds-scroll-overlay-block-size, 0px)\n );\n box-sizing: border-box;\n}\n\n.message-scroller__overlay {\n position: absolute;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: var(--dimension-z-index-floating);\n}\n\n.message-scroller__control {\n position: absolute;\n bottom: calc(\n var(--ds-scroll-overlay-content-block-size, 0px) + var(--dimension-space-200)\n );\n left: 50%;\n z-index: var(--dimension-z-index-floating);\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--dimension-radius-half);\n background: var(--color-background-transparent);\n transform: translateX(-50%);\n -webkit-backdrop-filter: blur(var(--effect-blur-sm));\n backdrop-filter: blur(var(--effect-blur-sm));\n}\n",
212
213
  "type": "registry:ui"
213
214
  },
214
215
  {
215
216
  "path": "src/wc/components/MessageScroller/MessageScroller.tsx",
216
- "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n State,\n} from '@stencil/core';\nimport type { MessageScrollerPosition } from '../conversation-types';\nimport { ScrollOverlayController } from '../../utils/scroll-overlay-controller';\n\nconst LIVE_EDGE_THRESHOLD = 24;\n\n@Component({\n tag: 'ds-message-scroller',\n styleUrl: 'MessageScroller.css',\n styleUrls: ['../../utils/scroll-edge-fade.css'],\n scoped: true,\n})\nexport class MessageScroller {\n @Element() el!: HTMLElement;\n\n @Prop() busy: boolean = false;\n @Prop() autoFollow: boolean = true;\n @Prop() defaultPosition: MessageScrollerPosition = 'last-anchor';\n @Prop() messagesLabel: string = 'Messages';\n\n @Event() dsReachStart!: EventEmitter<void>;\n\n @State() private showScrollToLatest = false;\n\n private viewport?: HTMLElement;\n private content?: HTMLElement;\n private overlay?: HTMLElement;\n private scrollOverlayController?: ScrollOverlayController;\n private mutationObserver?: MutationObserver;\n private following = true;\n private atStart = false;\n private programmatic = false;\n private previousScrollHeight = 0;\n\n componentDidLoad() {\n this.connectObservers();\n requestAnimationFrame(() => this.applyDefaultPosition());\n document.addEventListener('selectionchange', this.handleSelectionChange);\n }\n\n disconnectedCallback() {\n this.scrollOverlayController?.disconnect();\n this.mutationObserver?.disconnect();\n document.removeEventListener('selectionchange', this.handleSelectionChange);\n }\n\n @Method()\n async scrollToMessage(id: string): Promise<boolean> {\n const message = Array.from(\n this.el.querySelectorAll<HTMLElement & { messageId?: string }>('ds-message')\n ).find(item => item.messageId === id || item.getAttribute('message-id') === id);\n if (!message) return false;\n this.following = false;\n this.showScrollToLatest = true;\n message.scrollIntoView({ block: 'start', behavior: this.motionBehavior() });\n return true;\n }\n\n @Method()\n async scrollToStart() {\n this.scrollTo(0);\n this.following = false;\n this.showScrollToLatest = Boolean(\n this.viewport && this.viewport.scrollHeight > this.viewport.clientHeight\n );\n }\n\n @Method()\n async scrollToEnd() {\n this.following = true;\n this.showScrollToLatest = false;\n this.scrollTo(this.viewport?.scrollHeight ?? 0);\n }\n\n private motionBehavior(): ScrollBehavior {\n return matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth';\n }\n\n private scrollTo(top: number) {\n if (!this.viewport) return;\n this.programmatic = true;\n this.viewport.scrollTo({ top, behavior: this.motionBehavior() });\n requestAnimationFrame(() => {\n this.programmatic = false;\n });\n }\n\n private connectObservers() {\n if (!this.viewport || !this.content || !this.overlay) return;\n this.previousScrollHeight = this.viewport.scrollHeight;\n this.scrollOverlayController = new ScrollOverlayController({\n host: this.el,\n viewport: this.viewport,\n content: this.content,\n overlay: this.overlay,\n onGeometryChange: () => this.handleContentGrowth(),\n });\n this.scrollOverlayController.connect();\n this.mutationObserver = new MutationObserver(() => this.handleContentGrowth());\n this.mutationObserver.observe(this.content, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n }\n\n private handleContentGrowth() {\n if (!this.viewport) return;\n const nextHeight = this.viewport.scrollHeight;\n const delta = nextHeight - this.previousScrollHeight;\n if (delta > 0 && this.viewport.scrollTop <= LIVE_EDGE_THRESHOLD && !this.following) {\n this.viewport.scrollTop += delta;\n } else if (this.autoFollow && this.following) {\n this.viewport.scrollTop = nextHeight;\n }\n this.previousScrollHeight = nextHeight;\n this.showScrollToLatest = !this.following && !this.isAtLiveEdge();\n this.scrollOverlayController?.sync();\n }\n\n private applyDefaultPosition() {\n if (!this.viewport) return;\n if (this.defaultPosition === 'start') {\n this.viewport.scrollTop = 0;\n this.following = false;\n this.showScrollToLatest = this.viewport.scrollHeight > this.viewport.clientHeight;\n this.scrollOverlayController?.sync();\n return;\n }\n if (this.defaultPosition === 'last-anchor') {\n const anchors = this.el.querySelectorAll<HTMLElement>('ds-message[scroll-anchor]');\n const last = anchors.item(anchors.length - 1);\n if (last) {\n last.scrollIntoView({ block: 'start' });\n this.following = this.isAtLiveEdge();\n this.showScrollToLatest = !this.following;\n this.scrollOverlayController?.sync();\n return;\n }\n }\n this.viewport.scrollTop = this.viewport.scrollHeight;\n this.following = true;\n this.showScrollToLatest = false;\n this.scrollOverlayController?.sync();\n }\n\n private isAtLiveEdge(): boolean {\n if (!this.viewport) return true;\n return (\n this.viewport.scrollHeight - this.viewport.clientHeight - this.viewport.scrollTop <=\n LIVE_EDGE_THRESHOLD\n );\n }\n\n private handleScroll = () => {\n if (!this.viewport) return;\n this.scrollOverlayController?.sync();\n if (this.programmatic) return;\n this.following = this.isAtLiveEdge();\n this.showScrollToLatest = !this.following;\n const nextAtStart = this.viewport.scrollTop <= LIVE_EDGE_THRESHOLD;\n if (nextAtStart && !this.atStart) this.dsReachStart.emit();\n this.atStart = nextAtStart;\n };\n\n private releaseFollow = () => {\n this.following = false;\n this.showScrollToLatest = !this.isAtLiveEdge();\n };\n\n private handleSelectionChange = () => {\n const selection = document.getSelection();\n if (\n selection &&\n !selection.isCollapsed &&\n selection.anchorNode &&\n this.el.contains(selection.anchorNode)\n ) {\n this.following = false;\n this.showScrollToLatest = !this.isAtLiveEdge();\n }\n };\n\n render() {\n return (\n <Host>\n <div class=\"message-scroller\">\n <div\n class=\"message-scroller__viewport\"\n ref={element => {\n this.viewport = element;\n }}\n role=\"region\"\n aria-label={this.messagesLabel}\n tabIndex={0}\n onScroll={this.handleScroll}\n onPointerDown={this.releaseFollow}\n onKeyDown={(event: KeyboardEvent) => {\n if (['ArrowUp', 'PageUp', 'Home'].includes(event.key)) this.releaseFollow();\n }}\n >\n <div\n class=\"message-scroller__content scroll-edge-fade--block-window\"\n ref={element => {\n this.content = element;\n }}\n role=\"log\"\n aria-live=\"polite\"\n aria-relevant=\"additions\"\n aria-busy={this.busy ? 'true' : undefined}\n >\n <slot />\n </div>\n </div>\n <div\n class=\"message-scroller__overlay\"\n ref={element => {\n this.overlay = element;\n }}\n >\n <slot name=\"overlay\" />\n </div>\n {this.showScrollToLatest ? (\n <div class=\"message-scroller__control\">\n <ds-tooltip label=\"Scroll to latest message\" side=\"top\" size=\"sm\">\n <ds-button-unfilled\n variant=\"icon\"\n icon=\"ChevronDown\"\n size=\"md\"\n rounded\n hasBorder={false}\n aria-label=\"Scroll to latest message\"\n onDsClick={() => void this.scrollToEnd()}\n />\n </ds-tooltip>\n </div>\n ) : null}\n </div>\n </Host>\n );\n }\n}\n",
217
+ "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n State,\n} from '@stencil/core';\nimport type { MessageScrollerPosition } from '../conversation-types';\nimport { ScrollOverlayController } from '../../utils/scroll-overlay-controller';\n\nconst LIVE_EDGE_THRESHOLD = 24;\n\n@Component({\n tag: 'ds-message-scroller',\n styleUrl: 'MessageScroller.css',\n styleUrls: ['../../utils/scroll-edge-fade.css'],\n scoped: true,\n})\nexport class MessageScroller {\n @Element() el!: HTMLElement;\n\n @Prop() busy: boolean = false;\n @Prop() autoFollow: boolean = true;\n @Prop() defaultPosition: MessageScrollerPosition = 'last-anchor';\n @Prop() messagesLabel: string = 'Messages';\n\n @Event() dsReachStart!: EventEmitter<void>;\n\n @State() private showScrollToLatest = false;\n\n private viewport?: HTMLElement;\n private content?: HTMLElement;\n private overlay?: HTMLElement;\n private scrollOverlayController?: ScrollOverlayController;\n private mutationObserver?: MutationObserver;\n private following = true;\n private atStart = false;\n private programmatic = false;\n private previousScrollHeight = 0;\n\n componentDidLoad() {\n this.connectObservers();\n requestAnimationFrame(() => this.applyDefaultPosition());\n document.addEventListener('selectionchange', this.handleSelectionChange);\n }\n\n disconnectedCallback() {\n this.scrollOverlayController?.disconnect();\n this.mutationObserver?.disconnect();\n document.removeEventListener('selectionchange', this.handleSelectionChange);\n }\n\n @Method()\n async scrollToMessage(id: string): Promise<boolean> {\n const message = Array.from(\n this.el.querySelectorAll<HTMLElement & { messageId?: string }>('ds-message')\n ).find(item => item.messageId === id || item.getAttribute('message-id') === id);\n if (!message) return false;\n this.following = false;\n this.showScrollToLatest = true;\n message.scrollIntoView({ block: 'start', behavior: this.motionBehavior() });\n return true;\n }\n\n @Method()\n async scrollToStart() {\n this.scrollTo(0);\n this.following = false;\n this.showScrollToLatest = Boolean(\n this.viewport && this.viewport.scrollHeight > this.viewport.clientHeight\n );\n }\n\n @Method()\n async scrollToEnd() {\n this.following = true;\n this.showScrollToLatest = false;\n this.scrollTo(this.viewport?.scrollHeight ?? 0);\n }\n\n private motionBehavior(): ScrollBehavior {\n return matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth';\n }\n\n private scrollTo(top: number) {\n if (!this.viewport) return;\n this.programmatic = true;\n this.viewport.scrollTo({ top, behavior: this.motionBehavior() });\n requestAnimationFrame(() => {\n this.programmatic = false;\n });\n }\n\n private connectObservers() {\n if (!this.viewport || !this.content || !this.overlay) return;\n this.previousScrollHeight = this.viewport.scrollHeight;\n this.scrollOverlayController = new ScrollOverlayController({\n host: this.el,\n viewport: this.viewport,\n content: this.content,\n overlay: this.overlay,\n onGeometryChange: () => this.handleContentGrowth(),\n });\n this.scrollOverlayController.connect();\n this.mutationObserver = new MutationObserver(() => this.handleContentGrowth());\n this.mutationObserver.observe(this.content, {\n childList: true,\n subtree: true,\n characterData: true,\n });\n }\n\n private handleContentGrowth() {\n if (!this.viewport) return;\n const nextHeight = this.viewport.scrollHeight;\n const delta = nextHeight - this.previousScrollHeight;\n if (delta > 0 && this.viewport.scrollTop <= LIVE_EDGE_THRESHOLD && !this.following) {\n this.viewport.scrollTop += delta;\n } else if (this.autoFollow && this.following) {\n this.viewport.scrollTop = nextHeight;\n }\n this.previousScrollHeight = nextHeight;\n this.showScrollToLatest = !this.following && !this.isAtLiveEdge();\n this.scrollOverlayController?.sync();\n }\n\n private applyDefaultPosition() {\n if (!this.viewport) return;\n if (this.defaultPosition === 'start') {\n this.viewport.scrollTop = 0;\n this.following = false;\n this.showScrollToLatest = this.viewport.scrollHeight > this.viewport.clientHeight;\n this.scrollOverlayController?.sync();\n return;\n }\n if (this.defaultPosition === 'last-anchor') {\n const anchors = this.el.querySelectorAll<HTMLElement>('ds-message[scroll-anchor]');\n const last = anchors.item(anchors.length - 1);\n if (last) {\n last.scrollIntoView({ block: 'start' });\n this.following = this.isAtLiveEdge();\n this.showScrollToLatest = !this.following;\n this.scrollOverlayController?.sync();\n return;\n }\n }\n this.viewport.scrollTop = this.viewport.scrollHeight;\n this.following = true;\n this.showScrollToLatest = false;\n this.scrollOverlayController?.sync();\n }\n\n private isAtLiveEdge(): boolean {\n if (!this.viewport) return true;\n return (\n this.viewport.scrollHeight - this.viewport.clientHeight - this.viewport.scrollTop <=\n LIVE_EDGE_THRESHOLD\n );\n }\n\n private handleScroll = () => {\n if (!this.viewport) return;\n this.scrollOverlayController?.sync();\n if (this.programmatic) return;\n this.following = this.isAtLiveEdge();\n this.showScrollToLatest = !this.following;\n const nextAtStart = this.viewport.scrollTop <= LIVE_EDGE_THRESHOLD;\n if (nextAtStart && !this.atStart) this.dsReachStart.emit();\n this.atStart = nextAtStart;\n };\n\n private releaseFollow = () => {\n this.following = false;\n this.showScrollToLatest = !this.isAtLiveEdge();\n };\n\n private handleSelectionChange = () => {\n const selection = document.getSelection();\n if (\n selection &&\n !selection.isCollapsed &&\n selection.anchorNode &&\n this.el.contains(selection.anchorNode)\n ) {\n this.following = false;\n this.showScrollToLatest = !this.isAtLiveEdge();\n }\n };\n\n render() {\n return (\n <Host>\n <div class=\"message-scroller\">\n <div\n class=\"message-scroller__viewport\"\n ref={element => {\n this.viewport = element;\n }}\n role=\"region\"\n aria-label={this.messagesLabel}\n tabIndex={0}\n onScroll={this.handleScroll}\n onPointerDown={this.releaseFollow}\n onKeyDown={(event: KeyboardEvent) => {\n if (['ArrowUp', 'PageUp', 'Home'].includes(event.key)) this.releaseFollow();\n }}\n >\n <div\n class=\"message-scroller__content scroll-edge-fade--block-window\"\n ref={element => {\n this.content = element;\n }}\n role=\"log\"\n aria-live=\"polite\"\n aria-relevant=\"additions\"\n aria-busy={this.busy ? 'true' : undefined}\n >\n <slot />\n </div>\n </div>\n <div\n class=\"message-scroller__overlay\"\n ref={element => {\n this.overlay = element;\n }}\n >\n <slot name=\"overlay\" />\n </div>\n {this.showScrollToLatest ? (\n <div class=\"message-scroller__control ds-control-elevation ds-control-elevation--md\">\n <ds-tooltip label=\"Scroll to latest message\" side=\"top\" size=\"sm\">\n <ds-button-unfilled\n variant=\"icon\"\n icon=\"ChevronDown\"\n size=\"md\"\n rounded\n hasBorder={false}\n aria-label=\"Scroll to latest message\"\n onDsClick={() => void this.scrollToEnd()}\n />\n </ds-tooltip>\n </div>\n ) : null}\n </div>\n </Host>\n );\n }\n}\n",
217
218
  "type": "registry:ui"
218
219
  }
219
220
  ]