@ds-mo/ui 12.6.2 → 12.7.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 (242) hide show
  1. package/README.md +44 -115
  2. package/dist/.build-stamp +1 -1
  3. package/dist/agent-patterns.json +5 -3
  4. package/dist/agent.json +56 -32
  5. package/dist/angular/ds-menu.d.ts +1 -1
  6. package/dist/angular/ds-menu.js +3 -3
  7. package/dist/components/ds-agent-activity.js +1 -1
  8. package/dist/components/ds-agent-response.js +1 -1
  9. package/dist/components/ds-agent-source-list.js +1 -1
  10. package/dist/components/ds-agent-tool-call.js +1 -1
  11. package/dist/components/ds-attachment-list.js +1 -1
  12. package/dist/components/ds-badge.js +1 -1
  13. package/dist/components/ds-bar-nav.js +1 -1
  14. package/dist/components/ds-bar-nav.js.map +1 -1
  15. package/dist/components/ds-bar-title.js +1 -1
  16. package/dist/components/ds-bar-title.js.map +1 -1
  17. package/dist/components/ds-bar-workflow.js +1 -1
  18. package/dist/components/ds-bar-workflow.js.map +1 -1
  19. package/dist/components/ds-breadcrumb.js +1 -1
  20. package/dist/components/ds-button-filled.js +1 -1
  21. package/dist/components/ds-button-unfilled.js +1 -1
  22. package/dist/components/ds-card-data-viz-bar.js +1 -1
  23. package/dist/components/ds-card-data-viz-bar.js.map +1 -1
  24. package/dist/components/ds-card-data-viz-donut.js +1 -1
  25. package/dist/components/ds-card-data-viz-donut.js.map +1 -1
  26. package/dist/components/ds-card-data-viz-line.js +1 -1
  27. package/dist/components/ds-card-data-viz-line.js.map +1 -1
  28. package/dist/components/ds-card-setting.js +1 -1
  29. package/dist/components/ds-card-setting.js.map +1 -1
  30. package/dist/components/ds-card-shell-data-viz.js +1 -1
  31. package/dist/components/ds-chart-bar.js +1 -1
  32. package/dist/components/ds-chart-donut.js +1 -1
  33. package/dist/components/ds-chart-legend.js +1 -1
  34. package/dist/components/ds-chart-line.js +1 -1
  35. package/dist/components/ds-checkbox.js +1 -1
  36. package/dist/components/ds-chip.js +1 -1
  37. package/dist/components/ds-code-block.js +1 -1
  38. package/dist/components/ds-conversation-list-item.js +1 -1
  39. package/dist/components/ds-conversation-list-item.js.map +1 -1
  40. package/dist/components/ds-conversation-list-section.js +1 -1
  41. package/dist/components/ds-conversation-list.js +1 -1
  42. package/dist/components/ds-conversation-list.js.map +1 -1
  43. package/dist/components/ds-empty-state.js +1 -1
  44. package/dist/components/ds-field.js +1 -1
  45. package/dist/components/ds-field.js.map +1 -1
  46. package/dist/components/ds-input.js +1 -1
  47. package/dist/components/ds-input.js.map +1 -1
  48. package/dist/components/ds-markdown.js +1 -1
  49. package/dist/components/ds-menu.js +1 -1
  50. package/dist/components/ds-message-composer.js +1 -1
  51. package/dist/components/ds-message-composer.js.map +1 -1
  52. package/dist/components/ds-message-scroller.js +1 -1
  53. package/dist/components/ds-message-scroller.js.map +1 -1
  54. package/dist/components/ds-message.js +1 -1
  55. package/dist/components/ds-mobile-bar-nav.js +1 -1
  56. package/dist/components/ds-mobile-bar-nav.js.map +1 -1
  57. package/dist/components/ds-mobile-header.js +1 -1
  58. package/dist/components/ds-mobile-section-switcher.js +1 -1
  59. package/dist/components/ds-mobile-sheet-nav.js +1 -1
  60. package/dist/components/ds-mobile-sheet-nav.js.map +1 -1
  61. package/dist/components/ds-modal.js +1 -1
  62. package/dist/components/ds-modal.js.map +1 -1
  63. package/dist/components/ds-panel-nav.js +1 -1
  64. package/dist/components/ds-panel-nav.js.map +1 -1
  65. package/dist/components/ds-panel-sub-nav.js +1 -1
  66. package/dist/components/ds-panel-tool-header.js +1 -1
  67. package/dist/components/ds-panel-tool-search.js +1 -1
  68. package/dist/components/ds-panel-tool-search.js.map +1 -1
  69. package/dist/components/ds-panel-tools.js +1 -1
  70. package/dist/components/ds-radio.js +1 -1
  71. package/dist/components/ds-radio.js.map +1 -1
  72. package/dist/components/ds-scroll-overlay.js +1 -1
  73. package/dist/components/ds-scroll-overlay.js.map +1 -1
  74. package/dist/components/ds-select-multi.js +1 -1
  75. package/dist/components/ds-select-multi.js.map +1 -1
  76. package/dist/components/ds-select.js +1 -1
  77. package/dist/components/ds-select.js.map +1 -1
  78. package/dist/components/ds-shell-app.js +1 -1
  79. package/dist/components/ds-shell-app.js.map +1 -1
  80. package/dist/components/ds-shell-gradient-picker.js +1 -1
  81. package/dist/components/ds-shell-gradient-picker.js.map +1 -1
  82. package/dist/components/ds-shell-gradient-swatch.js +1 -1
  83. package/dist/components/ds-shell-page.js +1 -1
  84. package/dist/components/ds-shell-tools.js +1 -1
  85. package/dist/components/ds-shell-tools.js.map +1 -1
  86. package/dist/components/ds-skeleton.js +1 -1
  87. package/dist/components/ds-slider.js +1 -1
  88. package/dist/components/ds-slider.js.map +1 -1
  89. package/dist/components/ds-swatch-picker.js +1 -1
  90. package/dist/components/ds-switch.js +1 -1
  91. package/dist/components/ds-tab-group.js +1 -1
  92. package/dist/components/ds-tag.js +1 -1
  93. package/dist/components/ds-text.js +1 -1
  94. package/dist/components/ds-toast.js +1 -1
  95. package/dist/components/ds-tooltip-data-viz.js +1 -1
  96. package/dist/components/ds-tooltip.js +1 -1
  97. package/dist/components/ds-typing-indicator.js +1 -1
  98. package/dist/components/{p-DwX7UmLl.js → p--eKXTylf.js} +2 -2
  99. package/dist/components/p-1darPzys.js +2 -0
  100. package/dist/components/{p-DXLWm4PF.js.map → p-1darPzys.js.map} +1 -1
  101. package/dist/components/{p-DIoiApa-.js → p-5-QAFB4j.js} +2 -2
  102. package/dist/components/{p-D-xttdpn.js → p-B-9A0e_0.js} +2 -2
  103. package/dist/components/{p-CaRyxTMz.js → p-BT5hgg5i.js} +2 -2
  104. package/dist/components/{p-DQDpDYyF.js → p-B_5nxOeB.js} +2 -2
  105. package/dist/components/p-BcfC6phQ.js +2 -0
  106. package/dist/components/p-BcfC6phQ.js.map +1 -0
  107. package/dist/components/{p-BkAMkjIR.js → p-Bl001G0Y.js} +2 -2
  108. package/dist/components/p-BuiZcSga.js +2 -0
  109. package/dist/components/p-BuiZcSga.js.map +1 -0
  110. package/dist/components/p-Bv9frkvW.js +2 -0
  111. package/dist/components/p-Bv9frkvW.js.map +1 -0
  112. package/dist/components/{p-DWdRSOBV.js → p-C0-K8vnW.js} +2 -2
  113. package/dist/components/p-C61yJkfr.js +2 -0
  114. package/dist/components/p-C61yJkfr.js.map +1 -0
  115. package/dist/components/p-C6xC48hG.js +2 -0
  116. package/dist/components/p-C6xC48hG.js.map +1 -0
  117. package/dist/components/p-C9h13YnX.js +2 -0
  118. package/dist/components/{p-CnHrJTD5.js → p-CUAYsK44.js} +2 -2
  119. package/dist/components/p-C_9ZH0A3.js +2 -0
  120. package/dist/components/p-C_9ZH0A3.js.map +1 -0
  121. package/dist/components/p-Ca9-fnFl.js +2 -0
  122. package/dist/components/p-Ca9-fnFl.js.map +1 -0
  123. package/dist/components/p-CnAkobDW.js +2 -0
  124. package/dist/components/p-CnAkobDW.js.map +1 -0
  125. package/dist/components/{p-CWTih3Gc.js → p-CqWV4Hl_.js} +2 -2
  126. package/dist/components/{p-B9bxDfZV.js → p-Crsn82rz.js} +2 -2
  127. package/dist/components/p-CuwopElg.js +2 -0
  128. package/dist/components/p-CuwopElg.js.map +1 -0
  129. package/dist/components/{p-BwsGYxZx.js → p-CvyTZ4Mg.js} +2 -2
  130. package/dist/components/p-DIa73GJP.js +2 -0
  131. package/dist/components/p-DIa73GJP.js.map +1 -0
  132. package/dist/components/{p-DqubRds2.js → p-DNFd4Ao-.js} +2 -2
  133. package/dist/components/p-De7NquTI.js +2 -0
  134. package/dist/components/p-De7NquTI.js.map +1 -0
  135. package/dist/components/p-DjbTFy2B.js +2 -0
  136. package/dist/components/p-DjbTFy2B.js.map +1 -0
  137. package/dist/components/p-guQ7hv6W.js +2 -0
  138. package/dist/components/p-guQ7hv6W.js.map +1 -0
  139. package/dist/components/{p-BwGnhInj.js → p-k0Hmj1KV.js} +2 -2
  140. package/dist/components/p-pgxlkvUy.js +2 -0
  141. package/dist/docs/components.json +110 -25
  142. package/dist/lib/utils/control-text.d.ts +3 -1
  143. package/dist/lib/utils/index.d.ts +1 -1
  144. package/dist/lib/utils/index.js +8 -0
  145. package/dist/lib/utils/index.js.map +2 -2
  146. package/dist/mcp-data/registry/bar-nav.json +2 -2
  147. package/dist/mcp-data/registry/bar-title.json +2 -2
  148. package/dist/mcp-data/registry/bar-workflow.json +2 -2
  149. package/dist/mcp-data/registry/button-filled.json +5 -4
  150. package/dist/mcp-data/registry/button-unfilled.json +3 -2
  151. package/dist/mcp-data/registry/card-data-viz-bar.json +3 -2
  152. package/dist/mcp-data/registry/card-data-viz-donut.json +3 -2
  153. package/dist/mcp-data/registry/card-data-viz-line.json +3 -2
  154. package/dist/mcp-data/registry/card-setting.json +2 -2
  155. package/dist/mcp-data/registry/card-shell-data-viz.json +2 -2
  156. package/dist/mcp-data/registry/checkbox.json +9 -9
  157. package/dist/mcp-data/registry/conversation-list-item.json +2 -2
  158. package/dist/mcp-data/registry/conversation-list.json +2 -2
  159. package/dist/mcp-data/registry/empty-state.json +5 -1
  160. package/dist/mcp-data/registry/field.json +3 -2
  161. package/dist/mcp-data/registry/input.json +8 -7
  162. package/dist/mcp-data/registry/menu.json +22 -3
  163. package/dist/mcp-data/registry/message-scroller.json +2 -2
  164. package/dist/mcp-data/registry/mobile-bar-nav.json +3 -3
  165. package/dist/mcp-data/registry/mobile-header.json +2 -2
  166. package/dist/mcp-data/registry/mobile-sheet-nav.json +5 -3
  167. package/dist/mcp-data/registry/modal.json +2 -2
  168. package/dist/mcp-data/registry/panel-nav.json +2 -2
  169. package/dist/mcp-data/registry/panel-tool-header.json +2 -2
  170. package/dist/mcp-data/registry/panel-tool-search.json +2 -2
  171. package/dist/mcp-data/registry/panel-tools.json +2 -2
  172. package/dist/mcp-data/registry/radio.json +9 -9
  173. package/dist/mcp-data/registry/registry.json +55 -31
  174. package/dist/mcp-data/registry/select-multi.json +9 -7
  175. package/dist/mcp-data/registry/select.json +9 -7
  176. package/dist/mcp-data/registry/shell-app.json +2 -1
  177. package/dist/mcp-data/registry/shell-tools.json +1 -1
  178. package/dist/mcp-data/registry/skeleton.json +2 -2
  179. package/dist/mcp-data/registry/slider.json +3 -2
  180. package/dist/mcp-data/registry/switch.json +7 -7
  181. package/dist/styles/control-elevation.css +27 -2
  182. package/dist/types/components/ButtonFilled/ButtonFilled.d.ts +1 -1
  183. package/dist/types/components/Checkbox/Checkbox.d.ts +1 -1
  184. package/dist/types/components/Input/Input.d.ts +1 -1
  185. package/dist/types/components/Menu/Menu.d.ts +4 -1
  186. package/dist/types/components/MobileSheetNav/MobileSheetNav.d.ts +0 -1
  187. package/dist/types/components/Radio/Radio.d.ts +1 -1
  188. package/dist/types/components/Select/Select.d.ts +6 -23
  189. package/dist/types/components/SelectMulti/SelectMulti.d.ts +5 -20
  190. package/dist/types/components/ShellTools/ShellTools.d.ts +1 -0
  191. package/dist/types/components/Switch/Switch.d.ts +1 -1
  192. package/dist/types/components.d.ts +13 -2
  193. package/dist/types/utils/choice-list-parts.d.ts +4 -0
  194. package/dist/types/utils/control-press.d.ts +9 -0
  195. package/dist/types/utils/control-text.d.ts +3 -1
  196. package/dist/types/utils/index.d.ts +1 -1
  197. package/dist/types/utils/select-controller.d.ts +67 -0
  198. package/package.json +1 -1
  199. package/dist/components/p-B9xVWER6.js +0 -2
  200. package/dist/components/p-B9xVWER6.js.map +0 -1
  201. package/dist/components/p-BLea0TrP.js +0 -2
  202. package/dist/components/p-BLea0TrP.js.map +0 -1
  203. package/dist/components/p-BY0PzkUp.js +0 -2
  204. package/dist/components/p-BbgPGF2a.js +0 -2
  205. package/dist/components/p-BbgPGF2a.js.map +0 -1
  206. package/dist/components/p-BhC1YyId.js +0 -2
  207. package/dist/components/p-BhC1YyId.js.map +0 -1
  208. package/dist/components/p-Ccj5Op6E.js +0 -2
  209. package/dist/components/p-CiqnkENu.js +0 -2
  210. package/dist/components/p-CiqnkENu.js.map +0 -1
  211. package/dist/components/p-Cx-ywr1e.js +0 -2
  212. package/dist/components/p-Cx-ywr1e.js.map +0 -1
  213. package/dist/components/p-DPME1wUJ.js +0 -2
  214. package/dist/components/p-DPME1wUJ.js.map +0 -1
  215. package/dist/components/p-DSa0Hnd3.js +0 -2
  216. package/dist/components/p-DSa0Hnd3.js.map +0 -1
  217. package/dist/components/p-DUlqUNbE.js +0 -2
  218. package/dist/components/p-DUlqUNbE.js.map +0 -1
  219. package/dist/components/p-DXLWm4PF.js +0 -2
  220. package/dist/components/p-DYcC5-R9.js +0 -2
  221. package/dist/components/p-DYcC5-R9.js.map +0 -1
  222. package/dist/components/p-DbpfpeQT.js +0 -2
  223. package/dist/components/p-DbpfpeQT.js.map +0 -1
  224. package/dist/components/p-DuFm-ieS.js +0 -2
  225. package/dist/components/p-DuFm-ieS.js.map +0 -1
  226. package/dist/components/p-uQhuDEVt.js +0 -2
  227. package/dist/components/p-uQhuDEVt.js.map +0 -1
  228. /package/dist/components/{p-DwX7UmLl.js.map → p--eKXTylf.js.map} +0 -0
  229. /package/dist/components/{p-DIoiApa-.js.map → p-5-QAFB4j.js.map} +0 -0
  230. /package/dist/components/{p-D-xttdpn.js.map → p-B-9A0e_0.js.map} +0 -0
  231. /package/dist/components/{p-CaRyxTMz.js.map → p-BT5hgg5i.js.map} +0 -0
  232. /package/dist/components/{p-DQDpDYyF.js.map → p-B_5nxOeB.js.map} +0 -0
  233. /package/dist/components/{p-BkAMkjIR.js.map → p-Bl001G0Y.js.map} +0 -0
  234. /package/dist/components/{p-DWdRSOBV.js.map → p-C0-K8vnW.js.map} +0 -0
  235. /package/dist/components/{p-Ccj5Op6E.js.map → p-C9h13YnX.js.map} +0 -0
  236. /package/dist/components/{p-CnHrJTD5.js.map → p-CUAYsK44.js.map} +0 -0
  237. /package/dist/components/{p-CWTih3Gc.js.map → p-CqWV4Hl_.js.map} +0 -0
  238. /package/dist/components/{p-B9bxDfZV.js.map → p-Crsn82rz.js.map} +0 -0
  239. /package/dist/components/{p-BwsGYxZx.js.map → p-CvyTZ4Mg.js.map} +0 -0
  240. /package/dist/components/{p-DqubRds2.js.map → p-DNFd4Ao-.js.map} +0 -0
  241. /package/dist/components/{p-BwGnhInj.js.map → p-k0Hmj1KV.js.map} +0 -0
  242. /package/dist/components/{p-BY0PzkUp.js.map → p-pgxlkvUy.js.map} +0 -0
@@ -53,6 +53,7 @@
53
53
  "Read-only sliders remain focusable and submittable; inactive or disabled sliders leave the tab order and form submission."
54
54
  ],
55
55
  "states": [
56
+ "The label, slider control, value display, and supporting content follow the shared 4px vertical field flow.",
56
57
  "value is mutable: user pointer, keyboard, or assistive-technology input updates it and emits dsChange continuously.",
57
58
  "dsCommit emits once a pointer interaction completes or the native range input commits a keyboard or assistive-technology change.",
58
59
  "Assign a two-number array through the JavaScript value property to create a range; values are ordered, clamped, stepped, and prevented from crossing.",
@@ -645,12 +646,12 @@
645
646
  "files": [
646
647
  {
647
648
  "path": "src/wc/components/Slider/Slider.css",
648
- "content": "@import '../../utils/control-inactive.css';\n\n:host {\n --ds-slider-control-size: var(--dimension-size-400);\n --ds-slider-thumb-size: var(--dimension-size-200);\n --ds-slider-track-size: var(--dimension-size-100);\n --ds-slider-track-stroke: var(--dimension-stroke-width-012);\n --ds-slider-vertical-length: calc(var(--dimension-size-800) * 3);\n display: flex;\n flex-direction: column;\n box-sizing: border-box;\n gap: var(--dimension-space-050);\n width: 100%;\n color: var(--color-foreground-primary);\n}\n\n:host(.slider--sm) {\n --ds-slider-control-size: var(--dimension-size-300);\n --ds-slider-thumb-size: var(--dimension-size-150);\n --ds-slider-track-size: var(--dimension-size-075);\n}\n\n:host(.slider--xs) {\n --ds-slider-control-size: var(--dimension-size-200);\n --ds-slider-thumb-size: var(--dimension-size-100);\n --ds-slider-track-size: var(--dimension-size-050);\n}\n\n:host(.slider--vertical) {\n width: fit-content;\n min-width: var(--dimension-form-width-xs);\n}\n\n.slider__label-row {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: var(--dimension-space-100);\n min-width: 0;\n}\n\n.slider__label {\n min-width: 0;\n}\n\n.slider__value {\n flex-shrink: 0;\n}\n\n.slider__control {\n position: relative;\n width: 100%;\n height: var(--ds-slider-control-size);\n border-radius: var(--dimension-radius-half);\n cursor: default;\n touch-action: none;\n user-select: none;\n}\n\n:host(.slider--vertical) .slider__control {\n align-self: center;\n width: var(--ds-slider-control-size);\n height: var(--ds-slider-vertical-length);\n}\n\n:host(.slider--readonly) .slider__control {\n cursor: default;\n}\n\n/* Inactive: `.ds-control-inactive` (50% opacity) on the host. */\n:host(.ds-control-inactive) .slider__control {\n cursor: not-allowed;\n}\n\n.slider__rail {\n position: absolute;\n top: 50%;\n right: 0;\n left: 0;\n height: var(--ds-slider-track-size);\n overflow: hidden;\n border-radius: var(--dimension-radius-025);\n background-color: transparent;\n box-shadow: inset 0 0 0 var(--ds-slider-track-stroke) var(--color-border-secondary);\n transform: translateY(-50%);\n pointer-events: none;\n}\n\n:host(.slider--thumb-center) .slider__rail {\n right: calc(var(--ds-slider-thumb-size) / 2);\n left: calc(var(--ds-slider-thumb-size) / 2);\n}\n\n.slider__indicator {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: calc(\n var(--ds-slider-thumb-size) / 2 +\n (var(--ds-slider-end-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size))\n );\n border-radius: inherit;\n background-color: var(--color-background-bold-brand);\n}\n\n:host(.slider--range) .slider__indicator {\n left: calc(\n var(--ds-slider-thumb-size) / 2 +\n (var(--ds-slider-start-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size))\n );\n width: calc(\n ((var(--ds-slider-end-pct, 0) - var(--ds-slider-start-pct, 0)) / 100) *\n (100% - var(--ds-slider-thumb-size))\n );\n}\n\n:host(.slider--thumb-center) .slider__indicator {\n left: calc(var(--ds-slider-start-pct, 0) * 1%);\n width: calc((var(--ds-slider-end-pct, 0) - var(--ds-slider-start-pct, 0)) * 1%);\n}\n\n.slider__thumb {\n position: absolute;\n top: 50%;\n left: calc((var(--ds-slider-thumb-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size)));\n width: var(--ds-slider-thumb-size);\n height: var(--ds-slider-thumb-size);\n transform: translateY(-50%);\n z-index: 1;\n}\n\n.slider__thumb--active {\n z-index: 2;\n}\n\n.slider__input {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n margin: 0;\n padding: 0;\n border: 0;\n opacity: 0;\n appearance: none;\n pointer-events: none;\n z-index: 2;\n}\n\n.slider__thumb-visual {\n position: absolute;\n inset: 0;\n box-sizing: border-box;\n overflow: visible;\n border-radius: var(--dimension-radius-025);\n background-color: var(--color-background-primary);\n box-shadow: var(--effect-elevation-elevated-sm);\n pointer-events: none;\n transition:\n background-color var(--effect-motion-short-2),\n box-shadow var(--effect-motion-short-2);\n}\n\n.slider__thumb-wash {\n position: absolute;\n inset: 0;\n border-radius: inherit;\n background-color: transparent;\n transition: background-color var(--effect-motion-short-2);\n}\n\n:host(:not(.slider--readonly):not(.ds-control-inactive)) .slider__thumb:hover .slider__thumb-wash {\n background-color: var(--color-interaction-hover);\n}\n\n:host([data-dragging]:not(.slider--readonly):not(.ds-control-inactive))\n .slider__thumb--active .slider__thumb-wash {\n background-color: var(--color-interaction-pressed);\n}\n\n.slider__input:focus-visible + .slider__thumb-visual,\n:host([data-review-focus]) .slider__thumb[data-index='0'] .slider__thumb-visual {\n outline: var(--dimension-stroke-width-025) solid var(--color-interaction-focus);\n outline-offset: var(--dimension-space-025);\n}\n\n:host(.slider--vertical) .slider__rail {\n top: 0;\n right: auto;\n bottom: 0;\n left: 50%;\n width: var(--ds-slider-track-size);\n height: auto;\n transform: translateX(-50%);\n}\n\n:host(.slider--vertical.slider--thumb-center) .slider__rail {\n top: calc(var(--ds-slider-thumb-size) / 2);\n bottom: calc(var(--ds-slider-thumb-size) / 2);\n}\n\n:host(.slider--vertical) .slider__indicator {\n top: auto;\n right: 0;\n bottom: 0;\n left: 0;\n width: auto;\n height: calc(\n var(--ds-slider-thumb-size) / 2 +\n (var(--ds-slider-end-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size))\n );\n}\n\n:host(.slider--vertical.slider--range) .slider__indicator {\n bottom: calc(\n var(--ds-slider-thumb-size) / 2 +\n (var(--ds-slider-start-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size))\n );\n height: calc(\n ((var(--ds-slider-end-pct, 0) - var(--ds-slider-start-pct, 0)) / 100) *\n (100% - var(--ds-slider-thumb-size))\n );\n}\n\n:host(.slider--vertical.slider--thumb-center) .slider__indicator {\n bottom: calc(var(--ds-slider-start-pct, 0) * 1%);\n height: calc((var(--ds-slider-end-pct, 0) - var(--ds-slider-start-pct, 0)) * 1%);\n}\n\n:host(.slider--vertical) .slider__thumb {\n top: auto;\n bottom: calc((var(--ds-slider-thumb-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size)));\n left: 50%;\n transform: translateX(-50%);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .slider__thumb-visual,\n .slider__thumb-wash {\n transition: none;\n }\n}\n",
649
+ "content": "@import '../../utils/control-inactive.css';\n@import '../../utils/field-stack.css';\n\n:host {\n --ds-slider-control-size: var(--dimension-size-400);\n --ds-slider-thumb-size: var(--dimension-size-200);\n --ds-slider-track-size: var(--dimension-size-100);\n --ds-slider-track-stroke: var(--dimension-stroke-width-012);\n --ds-slider-vertical-length: calc(var(--dimension-size-800) * 3);\n box-sizing: border-box;\n width: 100%;\n color: var(--color-foreground-primary);\n}\n\n:host(.slider--sm) {\n --ds-slider-control-size: var(--dimension-size-300);\n --ds-slider-thumb-size: var(--dimension-size-150);\n --ds-slider-track-size: var(--dimension-size-075);\n}\n\n:host(.slider--xs) {\n --ds-slider-control-size: var(--dimension-size-200);\n --ds-slider-thumb-size: var(--dimension-size-100);\n --ds-slider-track-size: var(--dimension-size-050);\n}\n\n:host(.slider--vertical) {\n width: fit-content;\n min-width: var(--dimension-form-width-xs);\n}\n\n.slider__label-row {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: var(--dimension-space-100);\n min-width: 0;\n}\n\n.slider__label {\n min-width: 0;\n}\n\n.slider__value {\n flex-shrink: 0;\n}\n\n.slider__control {\n position: relative;\n width: 100%;\n height: var(--ds-slider-control-size);\n border-radius: var(--dimension-radius-half);\n cursor: default;\n touch-action: none;\n user-select: none;\n}\n\n:host(.slider--vertical) .slider__control {\n align-self: center;\n width: var(--ds-slider-control-size);\n height: var(--ds-slider-vertical-length);\n}\n\n:host(.slider--readonly) .slider__control {\n cursor: default;\n}\n\n/* Inactive: `.ds-control-inactive` (50% opacity) on the host. */\n:host(.ds-control-inactive) .slider__control {\n cursor: not-allowed;\n}\n\n.slider__rail {\n position: absolute;\n top: 50%;\n right: 0;\n left: 0;\n height: var(--ds-slider-track-size);\n overflow: hidden;\n border-radius: var(--dimension-radius-025);\n background-color: transparent;\n box-shadow: inset 0 0 0 var(--ds-slider-track-stroke) var(--color-border-secondary);\n transform: translateY(-50%);\n pointer-events: none;\n}\n\n:host(.slider--thumb-center) .slider__rail {\n right: calc(var(--ds-slider-thumb-size) / 2);\n left: calc(var(--ds-slider-thumb-size) / 2);\n}\n\n.slider__indicator {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: calc(\n var(--ds-slider-thumb-size) / 2 +\n (var(--ds-slider-end-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size))\n );\n border-radius: inherit;\n background-color: var(--color-background-bold-brand);\n}\n\n:host(.slider--range) .slider__indicator {\n left: calc(\n var(--ds-slider-thumb-size) / 2 +\n (var(--ds-slider-start-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size))\n );\n width: calc(\n ((var(--ds-slider-end-pct, 0) - var(--ds-slider-start-pct, 0)) / 100) *\n (100% - var(--ds-slider-thumb-size))\n );\n}\n\n:host(.slider--thumb-center) .slider__indicator {\n left: calc(var(--ds-slider-start-pct, 0) * 1%);\n width: calc((var(--ds-slider-end-pct, 0) - var(--ds-slider-start-pct, 0)) * 1%);\n}\n\n.slider__thumb {\n position: absolute;\n top: 50%;\n left: calc((var(--ds-slider-thumb-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size)));\n width: var(--ds-slider-thumb-size);\n height: var(--ds-slider-thumb-size);\n transform: translateY(-50%);\n z-index: 1;\n}\n\n.slider__thumb--active {\n z-index: 2;\n}\n\n.slider__input {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n margin: 0;\n padding: 0;\n border: 0;\n opacity: 0;\n appearance: none;\n pointer-events: none;\n z-index: 2;\n}\n\n.slider__thumb-visual {\n position: absolute;\n inset: 0;\n box-sizing: border-box;\n overflow: visible;\n border-radius: var(--dimension-radius-025);\n background-color: var(--color-background-primary);\n box-shadow: var(--effect-elevation-elevated-sm);\n pointer-events: none;\n transition:\n background-color var(--effect-motion-short-2),\n box-shadow var(--effect-motion-short-2);\n}\n\n.slider__thumb-wash {\n position: absolute;\n inset: 0;\n border-radius: inherit;\n background-color: transparent;\n transition: background-color var(--effect-motion-short-2);\n}\n\n:host(:not(.slider--readonly):not(.ds-control-inactive)) .slider__thumb:hover .slider__thumb-wash {\n background-color: var(--color-interaction-hover);\n}\n\n:host([data-dragging]:not(.slider--readonly):not(.ds-control-inactive))\n .slider__thumb--active .slider__thumb-wash {\n background-color: var(--color-interaction-pressed);\n}\n\n.slider__input:focus-visible + .slider__thumb-visual,\n:host([data-review-focus]) .slider__thumb[data-index='0'] .slider__thumb-visual {\n outline: var(--dimension-stroke-width-025) solid var(--color-interaction-focus);\n outline-offset: var(--dimension-space-025);\n}\n\n:host(.slider--vertical) .slider__rail {\n top: 0;\n right: auto;\n bottom: 0;\n left: 50%;\n width: var(--ds-slider-track-size);\n height: auto;\n transform: translateX(-50%);\n}\n\n:host(.slider--vertical.slider--thumb-center) .slider__rail {\n top: calc(var(--ds-slider-thumb-size) / 2);\n bottom: calc(var(--ds-slider-thumb-size) / 2);\n}\n\n:host(.slider--vertical) .slider__indicator {\n top: auto;\n right: 0;\n bottom: 0;\n left: 0;\n width: auto;\n height: calc(\n var(--ds-slider-thumb-size) / 2 +\n (var(--ds-slider-end-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size))\n );\n}\n\n:host(.slider--vertical.slider--range) .slider__indicator {\n bottom: calc(\n var(--ds-slider-thumb-size) / 2 +\n (var(--ds-slider-start-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size))\n );\n height: calc(\n ((var(--ds-slider-end-pct, 0) - var(--ds-slider-start-pct, 0)) / 100) *\n (100% - var(--ds-slider-thumb-size))\n );\n}\n\n:host(.slider--vertical.slider--thumb-center) .slider__indicator {\n bottom: calc(var(--ds-slider-start-pct, 0) * 1%);\n height: calc((var(--ds-slider-end-pct, 0) - var(--ds-slider-start-pct, 0)) * 1%);\n}\n\n:host(.slider--vertical) .slider__thumb {\n top: auto;\n bottom: calc((var(--ds-slider-thumb-pct, 0) / 100) * (100% - var(--ds-slider-thumb-size)));\n left: 50%;\n transform: translateX(-50%);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .slider__thumb-visual,\n .slider__thumb-wash {\n transition: none;\n }\n}\n",
649
650
  "type": "registry:ui"
650
651
  },
651
652
  {
652
653
  "path": "src/wc/components/Slider/Slider.tsx",
653
- "content": "import {\n AttachInternals,\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\n\nexport type SliderValue = number | number[];\nexport type SliderSize = 'md' | 'sm' | 'xs';\nexport type SliderOrientation = 'horizontal' | 'vertical';\nexport type SliderThumbAlignment = 'edge' | 'center';\n\nlet sliderId = 0;\nlet sliderExternalLabelId = 0;\n\n@Component({\n tag: 'ds-slider',\n styleUrl: 'Slider.css',\n scoped: true,\n formAssociated: true,\n})\nexport class Slider {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n /** Current value. Assign a two-number array through the JavaScript property for a range slider. */\n @Prop({ mutable: true }) value: SliderValue = 0;\n @Prop() min: number = 0;\n @Prop() max: number = 100;\n @Prop() step: number = 1;\n /** Minimum number of steps kept between two range thumbs. */\n @Prop() minStepsBetweenValues: number = 0;\n /** Visible field label. Supply aria-label when the label is intentionally hidden. */\n @Prop() label: string | undefined;\n /** Show the formatted current value beside the visible label. */\n @Prop() showValue: boolean = true;\n @Prop() size: SliderSize = 'md';\n @Prop() orientation: SliderOrientation = 'horizontal';\n /** Align the thumb edge with full-width rail endpoints, or its center with inset rail endpoints. */\n @Prop() thumbAlignment: SliderThumbAlignment = 'edge';\n @Prop({ reflect: true }) name: string | undefined;\n @Prop({ reflect: true }) form: string | undefined;\n @Prop({ reflect: true }) disabled: boolean = false;\n /** Keep the value focusable and submittable while preventing changes. */\n @Prop({ reflect: true }) readOnly: boolean = false;\n @Prop() isInactive: boolean = false;\n /** Direct accessible name for a single-thumb slider without a visible label. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n /** Id references for a visible single-thumb slider label. */\n @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;\n /** Id references for supporting guidance or error text. */\n @Prop({ attribute: 'aria-describedby' }) ariaDescribedby: string | undefined;\n /** Accessible label for the lower thumb in a range slider. */\n @Prop() startLabel: string = 'Minimum value';\n /** Accessible label for the upper thumb in a range slider. */\n @Prop() endLabel: string = 'Maximum value';\n /** Human-readable value text for a single slider or both range thumbs. */\n @Prop() valueText: string | undefined;\n /** Per-thumb human-readable value text for a range slider. Assign through the JavaScript property. */\n @Prop() valueTexts: string[] = [];\n /** Locale used by Intl.NumberFormat for the visible value. */\n @Prop() locale: string | undefined;\n /** Intl.NumberFormat options. Assign objects through the JavaScript property. */\n @Prop() formatOptions: Intl.NumberFormatOptions | undefined;\n @Prop() valuePrefix: string = '';\n @Prop() valueSuffix: string = '';\n @Prop() rangeSeparator: string = '–';\n /** Id applied to the first native range input. */\n @Prop() inputId: string | undefined;\n\n /** Emitted continuously while the value changes. */\n @Event() dsChange!: EventEmitter<SliderValue>;\n /** Emitted when a pointer or keyboard value change is committed. */\n @Event() dsCommit!: EventEmitter<SliderValue>;\n\n @State() private formDisabled = false;\n @State() private dragging = false;\n @State() private focused = false;\n @State() private touched = false;\n @State() private activeThumbIndex = 0;\n @State() private externalLabelledby: string | undefined;\n\n private readonly generatedId = `ds-slider-${++sliderId}`;\n private readonly labelId = `${this.generatedId}-label`;\n private initialValue: SliderValue = 0;\n private lastCommittedValue: SliderValue = 0;\n private pointerId: number | null = null;\n private pointerStartValue: SliderValue = 0;\n private controlEl: HTMLDivElement | null = null;\n\n componentWillLoad() {\n this.initialValue = this.cloneValue(this.publicValue);\n this.lastCommittedValue = this.cloneValue(this.publicValue);\n this.syncFormValue();\n }\n\n componentDidLoad() {\n this.syncNativeLabels();\n }\n\n @Watch('value')\n @Watch('min')\n @Watch('max')\n @Watch('step')\n @Watch('minStepsBetweenValues')\n @Watch('name')\n @Watch('disabled')\n @Watch('isInactive')\n syncFormValue() {\n const values = this.resolvedValues;\n if (this.isDisabled || !this.name) {\n this.internals.setFormValue(null);\n return;\n }\n\n if (this.isRange) {\n const data = new FormData();\n values.forEach(value => data.append(this.name as string, String(value)));\n this.internals.setFormValue(data, JSON.stringify(values));\n } else {\n this.internals.setFormValue(String(values[0]), String(values[0]));\n }\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.value = this.cloneValue(this.initialValue);\n this.lastCommittedValue = this.cloneValue(this.initialValue);\n this.dragging = false;\n this.focused = false;\n this.touched = false;\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n if (typeof state !== 'string') return;\n if (this.isRange) {\n try {\n const restored = JSON.parse(state);\n if (Array.isArray(restored)) this.value = restored.map(Number).slice(0, 2);\n } catch {\n this.value = this.cloneValue(this.initialValue);\n }\n } else {\n const restored = Number(state);\n if (Number.isFinite(restored)) this.value = restored;\n }\n this.lastCommittedValue = this.cloneValue(this.publicValue);\n }\n\n @Method()\n async setFocus(index: number = 0) {\n const inputs = this.el.querySelectorAll<HTMLInputElement>('.slider__input');\n inputs[Math.max(0, Math.min(index, inputs.length - 1))]?.focus();\n }\n\n private get isDisabled(): boolean {\n return this.disabled || this.isInactive || this.formDisabled;\n }\n\n private get isRange(): boolean {\n return Array.isArray(this.value);\n }\n\n private get safeStep(): number {\n return Number.isFinite(this.step) && this.step > 0 ? this.step : 1;\n }\n\n private get safeMin(): number {\n return Number.isFinite(this.min) ? this.min : 0;\n }\n\n private get safeMax(): number {\n const minimum = this.safeMin;\n return Number.isFinite(this.max) && this.max > minimum ? this.max : minimum + this.safeStep;\n }\n\n private get minimumGap(): number {\n const steps = Number.isFinite(this.minStepsBetweenValues)\n ? Math.max(0, Math.floor(this.minStepsBetweenValues))\n : 0;\n return steps * this.safeStep;\n }\n\n private get resolvedValues(): number[] {\n const minimum = this.safeMin;\n const maximum = this.safeMax;\n if (!this.isRange) return [this.snap(Number(this.value))];\n\n const raw = this.value as number[];\n let lower = this.snap(Number(raw[0] ?? minimum));\n let upper = this.snap(Number(raw[1] ?? maximum));\n if (lower > upper) [lower, upper] = [upper, lower];\n\n const gap = Math.min(this.minimumGap, maximum - minimum);\n if (upper - lower < gap) {\n upper = Math.min(maximum, this.snap(lower + gap));\n lower = Math.max(minimum, this.snap(upper - gap));\n }\n return [lower, upper];\n }\n\n private get publicValue(): SliderValue {\n const values = this.resolvedValues;\n return this.isRange ? [...values] : values[0];\n }\n\n private cloneValue(value: SliderValue): SliderValue {\n return Array.isArray(value) ? [...value] : value;\n }\n\n private valuesEqual(a: SliderValue, b: SliderValue): boolean {\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((value, index) => value === b[index]);\n }\n return !Array.isArray(a) && !Array.isArray(b) && a === b;\n }\n\n private decimalPlaces(value: number): number {\n const text = String(value).toLowerCase();\n if (text.includes('e-')) return Number(text.split('e-')[1]) || 0;\n return text.includes('.') ? text.split('.')[1].length : 0;\n }\n\n private snap(value: number): number {\n const minimum = this.safeMin;\n const maximum = this.safeMax;\n const candidate = Number.isFinite(value) ? value : minimum;\n const stepped = minimum + Math.round((candidate - minimum) / this.safeStep) * this.safeStep;\n const precision = Math.min(10, Math.max(\n this.decimalPlaces(minimum),\n this.decimalPlaces(maximum),\n this.decimalPlaces(this.safeStep),\n ));\n return Number(Math.min(maximum, Math.max(minimum, stepped)).toFixed(precision));\n }\n\n private percent(value: number): number {\n return ((value - this.safeMin) / (this.safeMax - this.safeMin)) * 100;\n }\n\n private allowedMin(index: number, values = this.resolvedValues): number {\n return this.isRange && index === 1\n ? Math.min(this.safeMax, values[0] + this.minimumGap)\n : this.safeMin;\n }\n\n private allowedMax(index: number, values = this.resolvedValues): number {\n return this.isRange && index === 0\n ? Math.max(this.safeMin, values[1] - this.minimumGap)\n : this.safeMax;\n }\n\n private setValueAt(index: number, next: number): boolean {\n const previous = this.publicValue;\n const values = this.resolvedValues;\n const constrained = Math.min(this.allowedMax(index, values), Math.max(this.allowedMin(index, values), this.snap(next)));\n values[index] = constrained;\n const nextValue: SliderValue = this.isRange ? [...values] : values[0];\n if (this.valuesEqual(previous, nextValue)) return false;\n\n this.value = nextValue;\n this.dsChange.emit(this.cloneValue(nextValue));\n return true;\n }\n\n private commitIfChanged() {\n const current = this.publicValue;\n if (this.valuesEqual(current, this.lastCommittedValue)) return;\n this.lastCommittedValue = this.cloneValue(current);\n this.dsCommit.emit(this.cloneValue(current));\n }\n\n private syncNativeLabels() {\n if (this.label || this.ariaLabel || this.ariaLabelledby || this.isRange) return;\n const ids = (Array.from(this.internals.labels) as HTMLLabelElement[]).map(label => {\n if (!label.id) label.id = `ds-slider-label-${++sliderExternalLabelId}`;\n return label.id;\n });\n if (ids.length) this.externalLabelledby = ids.join(' ');\n }\n\n private formattedValue(value: number, index: number): string {\n const authored = this.valueTexts[index] ?? this.valueText;\n if (authored) return authored;\n let number: string;\n try {\n number = new Intl.NumberFormat(this.locale, this.formatOptions).format(value);\n } catch {\n number = String(value);\n }\n return `${this.valuePrefix}${number}${this.valueSuffix}`;\n }\n\n private ariaValueText(value: number, index: number): string | undefined {\n if (this.valueTexts[index] || this.valueText || this.valuePrefix || this.valueSuffix || this.formatOptions) {\n return this.formattedValue(value, index);\n }\n return undefined;\n }\n\n private handleNativeInput = (event: Event, index: number) => {\n const input = event.target as HTMLInputElement;\n this.activeThumbIndex = index;\n if (this.readOnly || this.isDisabled) {\n input.value = String(this.resolvedValues[index]);\n return;\n }\n this.setValueAt(index, Number(input.value));\n };\n\n private handleNativeChange = () => {\n if (!this.readOnly && !this.isDisabled) this.commitIfChanged();\n };\n\n private handleKeyDown = (event: KeyboardEvent) => {\n if (!this.readOnly) return;\n if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'PageUp', 'PageDown'].includes(event.key)) {\n event.preventDefault();\n }\n };\n\n private handleFocus = (index: number) => {\n this.activeThumbIndex = index;\n this.focused = true;\n };\n\n private handleBlur = (event: FocusEvent) => {\n const next = event.relatedTarget;\n if (next instanceof Node && this.el.contains(next)) return;\n this.focused = false;\n this.touched = true;\n };\n\n private handleHostClick = (event: MouseEvent) => {\n if (event.target !== this.el || this.isDisabled) return;\n const inputs = this.el.querySelectorAll<HTMLInputElement>('.slider__input');\n inputs[Math.min(this.activeThumbIndex, inputs.length - 1)]?.focus();\n };\n\n private pointerPercent(event: PointerEvent): number {\n const control = this.controlEl;\n if (!control) return 0;\n const rect = control.getBoundingClientRect();\n const thumb = this.el.querySelector<HTMLElement>('.slider__thumb');\n const thumbSize = thumb\n ? this.orientation === 'horizontal' ? thumb.offsetWidth : thumb.offsetHeight\n : 0;\n // Both alignment modes keep the complete thumb inside the control. Edge\n // alignment lets the rail span the control while center alignment insets\n // the rail to the same thumb-center travel.\n const inset = thumbSize / 2;\n\n if (this.orientation === 'vertical') {\n const travel = Math.max(1, rect.height - inset * 2);\n return Math.min(1, Math.max(0, (rect.bottom - inset - event.clientY) / travel));\n }\n\n const travel = Math.max(1, rect.width - inset * 2);\n const percent = (event.clientX - rect.left - inset) / travel;\n return Math.min(1, Math.max(0, percent));\n }\n\n private valueFromPointer(event: PointerEvent): number {\n return this.snap(this.safeMin + this.pointerPercent(event) * (this.safeMax - this.safeMin));\n }\n\n private closestThumbIndex(value: number, target: EventTarget | null): number {\n const targetElement = target instanceof HTMLElement\n ? target.closest<HTMLElement>('.slider__thumb')\n : null;\n if (targetElement?.dataset['index']) return Number(targetElement.dataset['index']);\n if (!this.isRange) return 0;\n\n const values = this.resolvedValues;\n const lowerDistance = Math.abs(value - values[0]);\n const upperDistance = Math.abs(value - values[1]);\n if (lowerDistance === upperDistance) return value >= values[0] ? 1 : 0;\n return lowerDistance < upperDistance ? 0 : 1;\n }\n\n private handlePointerDown = (event: PointerEvent) => {\n if (event.button !== 0 || this.isDisabled || this.readOnly) return;\n event.preventDefault();\n const value = this.valueFromPointer(event);\n const index = this.closestThumbIndex(value, event.target);\n this.activeThumbIndex = index;\n this.pointerId = event.pointerId;\n this.pointerStartValue = this.cloneValue(this.publicValue);\n this.dragging = true;\n this.setValueAt(index, value);\n this.controlEl?.setPointerCapture(event.pointerId);\n requestAnimationFrame(() => this.el.querySelectorAll<HTMLInputElement>('.slider__input')[index]?.focus({ preventScroll: true }));\n };\n\n private handlePointerMove = (event: PointerEvent) => {\n if (this.pointerId !== event.pointerId || !this.dragging) return;\n event.preventDefault();\n this.setValueAt(this.activeThumbIndex, this.valueFromPointer(event));\n };\n\n private endPointerInteraction(event: PointerEvent, canceled: boolean) {\n if (this.pointerId !== event.pointerId) return;\n if (this.controlEl?.hasPointerCapture(event.pointerId)) this.controlEl.releasePointerCapture(event.pointerId);\n this.pointerId = null;\n this.dragging = false;\n this.touched = true;\n if (!canceled && !this.valuesEqual(this.pointerStartValue, this.publicValue)) this.commitIfChanged();\n }\n\n private renderThumb(value: number, index: number) {\n const inputId = index === 0\n ? this.inputId ?? `${this.generatedId}-input`\n : `${this.inputId ?? `${this.generatedId}-input`}-end`;\n const rangeLabel = index === 0 ? this.startLabel : this.endLabel;\n const labelledby = this.isRange\n ? undefined\n : this.ariaLabelledby ?? (this.label ? this.labelId : this.externalLabelledby);\n const directLabel = this.isRange ? rangeLabel : labelledby ? undefined : this.ariaLabel;\n\n return (\n <div\n class={{\n slider__thumb: true,\n 'slider__thumb--active': index === this.activeThumbIndex,\n }}\n data-index={String(index)}\n style={{ '--ds-slider-thumb-pct': String(this.percent(value)) } as Record<string, string>}\n >\n <input\n class=\"slider__input\"\n type=\"range\"\n id={inputId}\n min={this.allowedMin(index)}\n max={this.allowedMax(index)}\n step={this.safeStep}\n value={value}\n disabled={this.isDisabled}\n aria-label={directLabel}\n aria-labelledby={labelledby}\n aria-describedby={this.ariaDescribedby}\n aria-valuetext={this.ariaValueText(value, index)}\n aria-orientation={this.orientation === 'vertical' ? 'vertical' : undefined}\n aria-readonly={this.readOnly ? 'true' : undefined}\n onInput={(event: Event) => this.handleNativeInput(event, index)}\n onChange={this.handleNativeChange}\n onKeyDown={this.handleKeyDown}\n onFocus={() => this.handleFocus(index)}\n onBlur={this.handleBlur}\n />\n <span class=\"slider__thumb-visual\" aria-hidden=\"true\"><span class=\"slider__thumb-wash\" /></span>\n </div>\n );\n }\n\n render() {\n const values = this.resolvedValues;\n const first = values[0];\n const last = values[values.length - 1];\n const firstInputId = this.inputId ?? `${this.generatedId}-input`;\n const dirty = !this.valuesEqual(this.publicValue, this.initialValue);\n const valueDisplay = values.map((value, index) => this.formattedValue(value, index)).join(` ${this.rangeSeparator} `);\n\n return (\n <Host\n aria-disabled={this.isDisabled ? 'true' : undefined}\n class={{\n slider: true,\n [`slider--${this.size}`]: true,\n [`slider--${this.orientation}`]: true,\n [`slider--thumb-${this.thumbAlignment}`]: true,\n 'slider--range': this.isRange,\n 'slider--readonly': this.readOnly,\n 'ds-control-inactive': this.isDisabled,\n }}\n data-orientation={this.orientation}\n data-disabled={this.isDisabled ? '' : undefined}\n data-readonly={this.readOnly ? '' : undefined}\n data-dragging={this.dragging ? '' : undefined}\n data-focused={this.focused ? '' : undefined}\n data-filled=\"\"\n data-dirty={dirty ? '' : undefined}\n data-touched={this.touched ? '' : undefined}\n data-valid={!this.isDisabled ? '' : undefined}\n onClick={this.handleHostClick}\n >\n {(this.label || this.showValue) && (\n <div class=\"slider__label-row\">\n {this.label && (\n <ds-text\n class=\"slider__label\"\n as=\"label\"\n variant=\"text-body-small\"\n color=\"primary\"\n emphasis\n for={firstInputId}\n textId={this.labelId}\n >\n {this.label}\n </ds-text>\n )}\n {this.showValue && (\n <ds-text\n class=\"slider__value\"\n as=\"span\"\n variant=\"text-body-small\"\n color=\"primary\"\n fontFeature=\"tabular-nums\"\n >\n {valueDisplay}\n </ds-text>\n )}\n </div>\n )}\n <div\n class=\"slider__control\"\n ref={element => { this.controlEl = element ?? null; }}\n style={{\n '--ds-slider-start-pct': String(this.isRange ? this.percent(first) : 0),\n '--ds-slider-end-pct': String(this.percent(last)),\n } as Record<string, string>}\n onPointerDown={this.handlePointerDown}\n onPointerMove={this.handlePointerMove}\n onPointerUp={(event: PointerEvent) => this.endPointerInteraction(event, false)}\n onPointerCancel={(event: PointerEvent) => this.endPointerInteraction(event, true)}\n >\n <div class=\"slider__rail\" aria-hidden=\"true\"><div class=\"slider__indicator\" /></div>\n {values.map((value, index) => this.renderThumb(value, index))}\n </div>\n </Host>\n );\n }\n}\n",
654
+ "content": "import {\n AttachInternals,\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\n\nexport type SliderValue = number | number[];\nexport type SliderSize = 'md' | 'sm' | 'xs';\nexport type SliderOrientation = 'horizontal' | 'vertical';\nexport type SliderThumbAlignment = 'edge' | 'center';\n\nlet sliderId = 0;\nlet sliderExternalLabelId = 0;\n\n@Component({\n tag: 'ds-slider',\n styleUrl: 'Slider.css',\n scoped: true,\n formAssociated: true,\n})\nexport class Slider {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n /** Current value. Assign a two-number array through the JavaScript property for a range slider. */\n @Prop({ mutable: true }) value: SliderValue = 0;\n @Prop() min: number = 0;\n @Prop() max: number = 100;\n @Prop() step: number = 1;\n /** Minimum number of steps kept between two range thumbs. */\n @Prop() minStepsBetweenValues: number = 0;\n /** Visible field label. Supply aria-label when the label is intentionally hidden. */\n @Prop() label: string | undefined;\n /** Show the formatted current value beside the visible label. */\n @Prop() showValue: boolean = true;\n @Prop() size: SliderSize = 'md';\n @Prop() orientation: SliderOrientation = 'horizontal';\n /** Align the thumb edge with full-width rail endpoints, or its center with inset rail endpoints. */\n @Prop() thumbAlignment: SliderThumbAlignment = 'edge';\n @Prop({ reflect: true }) name: string | undefined;\n @Prop({ reflect: true }) form: string | undefined;\n @Prop({ reflect: true }) disabled: boolean = false;\n /** Keep the value focusable and submittable while preventing changes. */\n @Prop({ reflect: true }) readOnly: boolean = false;\n @Prop() isInactive: boolean = false;\n /** Direct accessible name for a single-thumb slider without a visible label. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n /** Id references for a visible single-thumb slider label. */\n @Prop({ attribute: 'aria-labelledby' }) ariaLabelledby: string | undefined;\n /** Id references for supporting guidance or error text. */\n @Prop({ attribute: 'aria-describedby' }) ariaDescribedby: string | undefined;\n /** Accessible label for the lower thumb in a range slider. */\n @Prop() startLabel: string = 'Minimum value';\n /** Accessible label for the upper thumb in a range slider. */\n @Prop() endLabel: string = 'Maximum value';\n /** Human-readable value text for a single slider or both range thumbs. */\n @Prop() valueText: string | undefined;\n /** Per-thumb human-readable value text for a range slider. Assign through the JavaScript property. */\n @Prop() valueTexts: string[] = [];\n /** Locale used by Intl.NumberFormat for the visible value. */\n @Prop() locale: string | undefined;\n /** Intl.NumberFormat options. Assign objects through the JavaScript property. */\n @Prop() formatOptions: Intl.NumberFormatOptions | undefined;\n @Prop() valuePrefix: string = '';\n @Prop() valueSuffix: string = '';\n @Prop() rangeSeparator: string = '–';\n /** Id applied to the first native range input. */\n @Prop() inputId: string | undefined;\n\n /** Emitted continuously while the value changes. */\n @Event() dsChange!: EventEmitter<SliderValue>;\n /** Emitted when a pointer or keyboard value change is committed. */\n @Event() dsCommit!: EventEmitter<SliderValue>;\n\n @State() private formDisabled = false;\n @State() private dragging = false;\n @State() private focused = false;\n @State() private touched = false;\n @State() private activeThumbIndex = 0;\n @State() private externalLabelledby: string | undefined;\n\n private readonly generatedId = `ds-slider-${++sliderId}`;\n private readonly labelId = `${this.generatedId}-label`;\n private initialValue: SliderValue = 0;\n private lastCommittedValue: SliderValue = 0;\n private pointerId: number | null = null;\n private pointerStartValue: SliderValue = 0;\n private controlEl: HTMLDivElement | null = null;\n\n componentWillLoad() {\n this.initialValue = this.cloneValue(this.publicValue);\n this.lastCommittedValue = this.cloneValue(this.publicValue);\n this.syncFormValue();\n }\n\n componentDidLoad() {\n this.syncNativeLabels();\n }\n\n @Watch('value')\n @Watch('min')\n @Watch('max')\n @Watch('step')\n @Watch('minStepsBetweenValues')\n @Watch('name')\n @Watch('disabled')\n @Watch('isInactive')\n syncFormValue() {\n const values = this.resolvedValues;\n if (this.isDisabled || !this.name) {\n this.internals.setFormValue(null);\n return;\n }\n\n if (this.isRange) {\n const data = new FormData();\n values.forEach(value => data.append(this.name as string, String(value)));\n this.internals.setFormValue(data, JSON.stringify(values));\n } else {\n this.internals.setFormValue(String(values[0]), String(values[0]));\n }\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.value = this.cloneValue(this.initialValue);\n this.lastCommittedValue = this.cloneValue(this.initialValue);\n this.dragging = false;\n this.focused = false;\n this.touched = false;\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n if (typeof state !== 'string') return;\n if (this.isRange) {\n try {\n const restored = JSON.parse(state);\n if (Array.isArray(restored)) this.value = restored.map(Number).slice(0, 2);\n } catch {\n this.value = this.cloneValue(this.initialValue);\n }\n } else {\n const restored = Number(state);\n if (Number.isFinite(restored)) this.value = restored;\n }\n this.lastCommittedValue = this.cloneValue(this.publicValue);\n }\n\n @Method()\n async setFocus(index: number = 0) {\n const inputs = this.el.querySelectorAll<HTMLInputElement>('.slider__input');\n inputs[Math.max(0, Math.min(index, inputs.length - 1))]?.focus();\n }\n\n private get isDisabled(): boolean {\n return this.disabled || this.isInactive || this.formDisabled;\n }\n\n private get isRange(): boolean {\n return Array.isArray(this.value);\n }\n\n private get safeStep(): number {\n return Number.isFinite(this.step) && this.step > 0 ? this.step : 1;\n }\n\n private get safeMin(): number {\n return Number.isFinite(this.min) ? this.min : 0;\n }\n\n private get safeMax(): number {\n const minimum = this.safeMin;\n return Number.isFinite(this.max) && this.max > minimum ? this.max : minimum + this.safeStep;\n }\n\n private get minimumGap(): number {\n const steps = Number.isFinite(this.minStepsBetweenValues)\n ? Math.max(0, Math.floor(this.minStepsBetweenValues))\n : 0;\n return steps * this.safeStep;\n }\n\n private get resolvedValues(): number[] {\n const minimum = this.safeMin;\n const maximum = this.safeMax;\n if (!this.isRange) return [this.snap(Number(this.value))];\n\n const raw = this.value as number[];\n let lower = this.snap(Number(raw[0] ?? minimum));\n let upper = this.snap(Number(raw[1] ?? maximum));\n if (lower > upper) [lower, upper] = [upper, lower];\n\n const gap = Math.min(this.minimumGap, maximum - minimum);\n if (upper - lower < gap) {\n upper = Math.min(maximum, this.snap(lower + gap));\n lower = Math.max(minimum, this.snap(upper - gap));\n }\n return [lower, upper];\n }\n\n private get publicValue(): SliderValue {\n const values = this.resolvedValues;\n return this.isRange ? [...values] : values[0];\n }\n\n private cloneValue(value: SliderValue): SliderValue {\n return Array.isArray(value) ? [...value] : value;\n }\n\n private valuesEqual(a: SliderValue, b: SliderValue): boolean {\n if (Array.isArray(a) && Array.isArray(b)) {\n return a.length === b.length && a.every((value, index) => value === b[index]);\n }\n return !Array.isArray(a) && !Array.isArray(b) && a === b;\n }\n\n private decimalPlaces(value: number): number {\n const text = String(value).toLowerCase();\n if (text.includes('e-')) return Number(text.split('e-')[1]) || 0;\n return text.includes('.') ? text.split('.')[1].length : 0;\n }\n\n private snap(value: number): number {\n const minimum = this.safeMin;\n const maximum = this.safeMax;\n const candidate = Number.isFinite(value) ? value : minimum;\n const stepped = minimum + Math.round((candidate - minimum) / this.safeStep) * this.safeStep;\n const precision = Math.min(10, Math.max(\n this.decimalPlaces(minimum),\n this.decimalPlaces(maximum),\n this.decimalPlaces(this.safeStep),\n ));\n return Number(Math.min(maximum, Math.max(minimum, stepped)).toFixed(precision));\n }\n\n private percent(value: number): number {\n return ((value - this.safeMin) / (this.safeMax - this.safeMin)) * 100;\n }\n\n private allowedMin(index: number, values = this.resolvedValues): number {\n return this.isRange && index === 1\n ? Math.min(this.safeMax, values[0] + this.minimumGap)\n : this.safeMin;\n }\n\n private allowedMax(index: number, values = this.resolvedValues): number {\n return this.isRange && index === 0\n ? Math.max(this.safeMin, values[1] - this.minimumGap)\n : this.safeMax;\n }\n\n private setValueAt(index: number, next: number): boolean {\n const previous = this.publicValue;\n const values = this.resolvedValues;\n const constrained = Math.min(this.allowedMax(index, values), Math.max(this.allowedMin(index, values), this.snap(next)));\n values[index] = constrained;\n const nextValue: SliderValue = this.isRange ? [...values] : values[0];\n if (this.valuesEqual(previous, nextValue)) return false;\n\n this.value = nextValue;\n this.dsChange.emit(this.cloneValue(nextValue));\n return true;\n }\n\n private commitIfChanged() {\n const current = this.publicValue;\n if (this.valuesEqual(current, this.lastCommittedValue)) return;\n this.lastCommittedValue = this.cloneValue(current);\n this.dsCommit.emit(this.cloneValue(current));\n }\n\n private syncNativeLabels() {\n if (this.label || this.ariaLabel || this.ariaLabelledby || this.isRange) return;\n const ids = (Array.from(this.internals.labels) as HTMLLabelElement[]).map(label => {\n if (!label.id) label.id = `ds-slider-label-${++sliderExternalLabelId}`;\n return label.id;\n });\n if (ids.length) this.externalLabelledby = ids.join(' ');\n }\n\n private formattedValue(value: number, index: number): string {\n const authored = this.valueTexts[index] ?? this.valueText;\n if (authored) return authored;\n let number: string;\n try {\n number = new Intl.NumberFormat(this.locale, this.formatOptions).format(value);\n } catch {\n number = String(value);\n }\n return `${this.valuePrefix}${number}${this.valueSuffix}`;\n }\n\n private ariaValueText(value: number, index: number): string | undefined {\n if (this.valueTexts[index] || this.valueText || this.valuePrefix || this.valueSuffix || this.formatOptions) {\n return this.formattedValue(value, index);\n }\n return undefined;\n }\n\n private handleNativeInput = (event: Event, index: number) => {\n const input = event.target as HTMLInputElement;\n this.activeThumbIndex = index;\n if (this.readOnly || this.isDisabled) {\n input.value = String(this.resolvedValues[index]);\n return;\n }\n this.setValueAt(index, Number(input.value));\n };\n\n private handleNativeChange = () => {\n if (!this.readOnly && !this.isDisabled) this.commitIfChanged();\n };\n\n private handleKeyDown = (event: KeyboardEvent) => {\n if (!this.readOnly) return;\n if (['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Home', 'End', 'PageUp', 'PageDown'].includes(event.key)) {\n event.preventDefault();\n }\n };\n\n private handleFocus = (index: number) => {\n this.activeThumbIndex = index;\n this.focused = true;\n };\n\n private handleBlur = (event: FocusEvent) => {\n const next = event.relatedTarget;\n if (next instanceof Node && this.el.contains(next)) return;\n this.focused = false;\n this.touched = true;\n };\n\n private handleHostClick = (event: MouseEvent) => {\n if (event.target !== this.el || this.isDisabled) return;\n const inputs = this.el.querySelectorAll<HTMLInputElement>('.slider__input');\n inputs[Math.min(this.activeThumbIndex, inputs.length - 1)]?.focus();\n };\n\n private pointerPercent(event: PointerEvent): number {\n const control = this.controlEl;\n if (!control) return 0;\n const rect = control.getBoundingClientRect();\n const thumb = this.el.querySelector<HTMLElement>('.slider__thumb');\n const thumbSize = thumb\n ? this.orientation === 'horizontal' ? thumb.offsetWidth : thumb.offsetHeight\n : 0;\n // Both alignment modes keep the complete thumb inside the control. Edge\n // alignment lets the rail span the control while center alignment insets\n // the rail to the same thumb-center travel.\n const inset = thumbSize / 2;\n\n if (this.orientation === 'vertical') {\n const travel = Math.max(1, rect.height - inset * 2);\n return Math.min(1, Math.max(0, (rect.bottom - inset - event.clientY) / travel));\n }\n\n const travel = Math.max(1, rect.width - inset * 2);\n const percent = (event.clientX - rect.left - inset) / travel;\n return Math.min(1, Math.max(0, percent));\n }\n\n private valueFromPointer(event: PointerEvent): number {\n return this.snap(this.safeMin + this.pointerPercent(event) * (this.safeMax - this.safeMin));\n }\n\n private closestThumbIndex(value: number, target: EventTarget | null): number {\n const targetElement = target instanceof HTMLElement\n ? target.closest<HTMLElement>('.slider__thumb')\n : null;\n if (targetElement?.dataset['index']) return Number(targetElement.dataset['index']);\n if (!this.isRange) return 0;\n\n const values = this.resolvedValues;\n const lowerDistance = Math.abs(value - values[0]);\n const upperDistance = Math.abs(value - values[1]);\n if (lowerDistance === upperDistance) return value >= values[0] ? 1 : 0;\n return lowerDistance < upperDistance ? 0 : 1;\n }\n\n private handlePointerDown = (event: PointerEvent) => {\n if (event.button !== 0 || this.isDisabled || this.readOnly) return;\n event.preventDefault();\n const value = this.valueFromPointer(event);\n const index = this.closestThumbIndex(value, event.target);\n this.activeThumbIndex = index;\n this.pointerId = event.pointerId;\n this.pointerStartValue = this.cloneValue(this.publicValue);\n this.dragging = true;\n this.setValueAt(index, value);\n this.controlEl?.setPointerCapture(event.pointerId);\n requestAnimationFrame(() => this.el.querySelectorAll<HTMLInputElement>('.slider__input')[index]?.focus({ preventScroll: true }));\n };\n\n private handlePointerMove = (event: PointerEvent) => {\n if (this.pointerId !== event.pointerId || !this.dragging) return;\n event.preventDefault();\n this.setValueAt(this.activeThumbIndex, this.valueFromPointer(event));\n };\n\n private endPointerInteraction(event: PointerEvent, canceled: boolean) {\n if (this.pointerId !== event.pointerId) return;\n if (this.controlEl?.hasPointerCapture(event.pointerId)) this.controlEl.releasePointerCapture(event.pointerId);\n this.pointerId = null;\n this.dragging = false;\n this.touched = true;\n if (!canceled && !this.valuesEqual(this.pointerStartValue, this.publicValue)) this.commitIfChanged();\n }\n\n private renderThumb(value: number, index: number) {\n const inputId = index === 0\n ? this.inputId ?? `${this.generatedId}-input`\n : `${this.inputId ?? `${this.generatedId}-input`}-end`;\n const rangeLabel = index === 0 ? this.startLabel : this.endLabel;\n const labelledby = this.isRange\n ? undefined\n : this.ariaLabelledby ?? (this.label ? this.labelId : this.externalLabelledby);\n const directLabel = this.isRange ? rangeLabel : labelledby ? undefined : this.ariaLabel;\n\n return (\n <div\n class={{\n slider__thumb: true,\n 'slider__thumb--active': index === this.activeThumbIndex,\n }}\n data-index={String(index)}\n style={{ '--ds-slider-thumb-pct': String(this.percent(value)) } as Record<string, string>}\n >\n <input\n class=\"slider__input\"\n type=\"range\"\n id={inputId}\n min={this.allowedMin(index)}\n max={this.allowedMax(index)}\n step={this.safeStep}\n value={value}\n disabled={this.isDisabled}\n aria-label={directLabel}\n aria-labelledby={labelledby}\n aria-describedby={this.ariaDescribedby}\n aria-valuetext={this.ariaValueText(value, index)}\n aria-orientation={this.orientation === 'vertical' ? 'vertical' : undefined}\n aria-readonly={this.readOnly ? 'true' : undefined}\n onInput={(event: Event) => this.handleNativeInput(event, index)}\n onChange={this.handleNativeChange}\n onKeyDown={this.handleKeyDown}\n onFocus={() => this.handleFocus(index)}\n onBlur={this.handleBlur}\n />\n <span class=\"slider__thumb-visual\" aria-hidden=\"true\"><span class=\"slider__thumb-wash\" /></span>\n </div>\n );\n }\n\n render() {\n const values = this.resolvedValues;\n const first = values[0];\n const last = values[values.length - 1];\n const firstInputId = this.inputId ?? `${this.generatedId}-input`;\n const dirty = !this.valuesEqual(this.publicValue, this.initialValue);\n const valueDisplay = values.map((value, index) => this.formattedValue(value, index)).join(` ${this.rangeSeparator} `);\n\n return (\n <Host\n aria-disabled={this.isDisabled ? 'true' : undefined}\n class={{\n slider: true,\n 'ds-field-stack': true,\n [`slider--${this.size}`]: true,\n [`slider--${this.orientation}`]: true,\n [`slider--thumb-${this.thumbAlignment}`]: true,\n 'slider--range': this.isRange,\n 'slider--readonly': this.readOnly,\n 'ds-control-inactive': this.isDisabled,\n }}\n data-orientation={this.orientation}\n data-disabled={this.isDisabled ? '' : undefined}\n data-readonly={this.readOnly ? '' : undefined}\n data-dragging={this.dragging ? '' : undefined}\n data-focused={this.focused ? '' : undefined}\n data-filled=\"\"\n data-dirty={dirty ? '' : undefined}\n data-touched={this.touched ? '' : undefined}\n data-valid={!this.isDisabled ? '' : undefined}\n onClick={this.handleHostClick}\n >\n {(this.label || this.showValue) && (\n <div class=\"slider__label-row\">\n {this.label && (\n <ds-text\n class=\"slider__label\"\n as=\"label\"\n variant=\"text-body-small\"\n color=\"primary\"\n emphasis\n for={firstInputId}\n textId={this.labelId}\n >\n {this.label}\n </ds-text>\n )}\n {this.showValue && (\n <ds-text\n class=\"slider__value\"\n as=\"span\"\n variant=\"text-body-small\"\n color=\"primary\"\n fontFeature=\"tabular-nums\"\n >\n {valueDisplay}\n </ds-text>\n )}\n </div>\n )}\n <div\n class=\"slider__control\"\n ref={element => { this.controlEl = element ?? null; }}\n style={{\n '--ds-slider-start-pct': String(this.isRange ? this.percent(first) : 0),\n '--ds-slider-end-pct': String(this.percent(last)),\n } as Record<string, string>}\n onPointerDown={this.handlePointerDown}\n onPointerMove={this.handlePointerMove}\n onPointerUp={(event: PointerEvent) => this.endPointerInteraction(event, false)}\n onPointerCancel={(event: PointerEvent) => this.endPointerInteraction(event, true)}\n >\n <div class=\"slider__rail\" aria-hidden=\"true\"><div class=\"slider__indicator\" /></div>\n {values.map((value, index) => this.renderThumb(value, index))}\n </div>\n </Host>\n );\n }\n}\n",
654
655
  "type": "registry:ui"
655
656
  }
656
657
  ]
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
3
  "name": "switch",
4
4
  "title": "Switch",
5
- "description": "Compact on/off setting control for changes that take effect immediately, with three sizes and form-associated behavior.",
5
+ "description": "Compact on/off setting control for changes that take effect immediately, with four sizes and form-associated behavior.",
6
6
  "type": "registry:ui",
7
7
  "meta": {
8
8
  "library": "@ds-mo/ui",
@@ -13,7 +13,7 @@
13
13
  "intent": {
14
14
  "audience": "general",
15
15
  "status": "stable",
16
- "summary": "Compact on/off setting control for changes that take effect immediately, with three sizes and form-associated behavior.",
16
+ "summary": "Compact on/off setting control for changes that take effect immediately, with four sizes and form-associated behavior.",
17
17
  "useWhen": [
18
18
  "A binary setting takes effect immediately when the user turns it on or off.",
19
19
  "A settings, form, or menu row needs a compact on/off control.",
@@ -56,7 +56,7 @@
56
56
  "states": [
57
57
  "Activation updates checked immediately and emits dsChange with the new boolean value.",
58
58
  "The initial checked value is the form-reset baseline; unchecked switches submit nothing unless uncheckedValue is provided.",
59
- "Sizes are md 32×20 with a 12px thumb and 4px body inset, sm 24×16 with a 10px thumb and 3px inset, and xs 20×12 with an 8px thumb and 2px inset.",
59
+ "Sizes are lg 40×24 with a 16px thumb and 4px body inset, md 32×20 with a 12px thumb and 4px inset, sm 24×16 with a 10px thumb and 3px inset, and xs 20×12 with an 8px thumb and 2px inset.",
60
60
  "Unchecked uses a transparent track with a density-specific tertiary-foreground inset stroke and a solid tertiary-foreground thumb without a border. Checked uses a brand track and primary-background thumb with no strokes.",
61
61
  "Presentation mode removes switch semantics, focus, interaction, and form submission while retaining the checked visual."
62
62
  ],
@@ -165,7 +165,7 @@
165
165
  },
166
166
  "size": {
167
167
  "type": "SwitchSize",
168
- "resolvedType": "\"md\" | \"sm\" | \"xs\"",
168
+ "resolvedType": "\"lg\" | \"md\" | \"sm\" | \"xs\"",
169
169
  "attribute": "size",
170
170
  "default": "'md'",
171
171
  "required": false,
@@ -277,7 +277,7 @@
277
277
  },
278
278
  "size": {
279
279
  "type": "SwitchSize",
280
- "resolvedType": "\"md\" | \"sm\" | \"xs\"",
280
+ "resolvedType": "\"lg\" | \"md\" | \"sm\" | \"xs\"",
281
281
  "attribute": "size",
282
282
  "default": "'md'",
283
283
  "required": false,
@@ -349,12 +349,12 @@
349
349
  "files": [
350
350
  {
351
351
  "path": "src/wc/components/Switch/Switch.css",
352
- "content": "@import '../../utils/focus-ring.css';\n@import '../../utils/control-inactive.css';\n\n:host {\n --ds-switch-width: var(--dimension-size-400);\n --ds-switch-height: var(--dimension-size-250);\n --ds-switch-thumb-size: var(--dimension-size-150);\n --ds-switch-gap: var(--dimension-space-050);\n --ds-switch-stroke: var(--dimension-stroke-width-012);\n --ds-switch-inset: var(--ds-switch-gap);\n --ds-switch-thumb-hover: var(--color-interaction-hover);\n --ds-switch-thumb-pressed: var(--color-interaction-pressed);\n --ds-switch-travel: calc(\n var(--ds-switch-width) - var(--ds-switch-thumb-size) - var(--ds-switch-inset) -\n var(--ds-switch-inset)\n );\n position: relative;\n display: inline-flex;\n width: var(--ds-switch-width);\n height: var(--ds-switch-height);\n padding: 0;\n border: none;\n border-radius: var(--dimension-radius-half);\n box-sizing: border-box;\n flex-shrink: 0;\n overflow: visible;\n cursor: default;\n user-select: none;\n outline: none;\n background-color: transparent;\n box-shadow: inset 0 0 0 var(--ds-switch-stroke) var(--color-border-secondary);\n transition:\n background-color var(--effect-motion-short-3),\n box-shadow var(--effect-motion-short-3);\n}\n\n:host(.switch--sm) {\n --ds-switch-width: var(--dimension-size-300);\n --ds-switch-height: var(--dimension-size-200);\n --ds-switch-thumb-size: calc(var(--dimension-size-base) * 5 / 4);\n --ds-switch-gap: calc(var(--dimension-space-025) + var(--dimension-space-012));\n}\n\n:host(.switch--xs) {\n --ds-switch-width: var(--dimension-size-250);\n --ds-switch-height: var(--dimension-size-150);\n --ds-switch-thumb-size: var(--dimension-size-100);\n --ds-switch-gap: var(--dimension-space-025);\n}\n\n:host(.checked) {\n background-color: var(--color-background-bold-brand);\n box-shadow: none;\n}\n\n:host(.switch--readonly),\n:host(.switch--presentation) {\n cursor: default;\n}\n\n:host(.switch--presentation) {\n pointer-events: none;\n}\n\n/* Inactive: `.ds-control-inactive` (50% opacity). */\n\n.thumb {\n position: absolute;\n top: 50%;\n left: var(--ds-switch-inset);\n width: var(--ds-switch-thumb-size);\n height: var(--ds-switch-thumb-size);\n border-radius: var(--dimension-radius-half);\n background-color: var(--color-foreground-tertiary);\n box-shadow: none;\n transform: translateY(-50%);\n transition:\n background-color var(--effect-motion-short-3),\n transform var(--effect-motion-short-3);\n pointer-events: none;\n}\n\n.thumb::after {\n content: '';\n position: absolute;\n inset: 0;\n border-radius: inherit;\n background: transparent;\n pointer-events: none;\n}\n\n:host(:hover:not(:disabled):not(.ds-control-inactive):not(.switch--readonly):not(.switch--presentation))\n .thumb::after {\n background: var(--ds-switch-thumb-hover);\n}\n\n:host(:active:not(:disabled):not(.ds-control-inactive):not(.switch--readonly):not(.switch--presentation))\n .thumb::after {\n background: var(--ds-switch-thumb-pressed);\n}\n\n:host(.checked) .thumb {\n background-color: var(--color-background-primary);\n box-shadow: none;\n transform: translate3d(var(--ds-switch-travel), -50%, 0);\n}\n\n@media (prefers-reduced-motion: reduce) {\n :host {\n transition: none;\n }\n\n .thumb {\n transition: none;\n }\n}\n",
352
+ "content": "@import '../../utils/focus-ring.css';\n@import '../../utils/control-inactive.css';\n\n:host {\n --ds-switch-width: var(--dimension-size-400);\n --ds-switch-height: var(--dimension-size-250);\n --ds-switch-thumb-size: var(--dimension-size-150);\n --ds-switch-gap: var(--dimension-space-050);\n --ds-switch-stroke: var(--dimension-stroke-width-012);\n --ds-switch-inset: var(--ds-switch-gap);\n --ds-switch-thumb-hover: var(--color-interaction-hover);\n --ds-switch-thumb-pressed: var(--color-interaction-pressed);\n --ds-switch-travel: calc(\n var(--ds-switch-width) - var(--ds-switch-thumb-size) - var(--ds-switch-inset) -\n var(--ds-switch-inset)\n );\n position: relative;\n display: inline-flex;\n width: var(--ds-switch-width);\n height: var(--ds-switch-height);\n padding: 0;\n border: none;\n border-radius: var(--dimension-radius-half);\n box-sizing: border-box;\n flex-shrink: 0;\n overflow: visible;\n cursor: default;\n user-select: none;\n outline: none;\n background-color: transparent;\n box-shadow: inset 0 0 0 var(--ds-switch-stroke) var(--color-border-secondary);\n transition:\n background-color var(--effect-motion-short-3),\n box-shadow var(--effect-motion-short-3);\n}\n\n:host(.switch--lg) {\n --ds-switch-width: var(--dimension-size-500);\n --ds-switch-height: var(--dimension-size-300);\n --ds-switch-thumb-size: var(--dimension-iconography-sm);\n --ds-switch-gap: var(--dimension-space-050);\n}\n\n:host(.switch--sm) {\n --ds-switch-width: var(--dimension-size-300);\n --ds-switch-height: var(--dimension-size-200);\n --ds-switch-thumb-size: calc(var(--dimension-size-base) * 5 / 4);\n --ds-switch-gap: calc(var(--dimension-space-025) + var(--dimension-space-012));\n}\n\n:host(.switch--xs) {\n --ds-switch-width: var(--dimension-size-250);\n --ds-switch-height: var(--dimension-size-150);\n --ds-switch-thumb-size: var(--dimension-size-100);\n --ds-switch-gap: var(--dimension-space-025);\n}\n\n:host(.checked) {\n background-color: var(--color-background-bold-brand);\n box-shadow: none;\n}\n\n:host(.switch--readonly),\n:host(.switch--presentation) {\n cursor: default;\n}\n\n:host(.switch--presentation) {\n pointer-events: none;\n}\n\n/* Inactive: `.ds-control-inactive` (50% opacity). */\n\n.thumb {\n position: absolute;\n top: 50%;\n left: var(--ds-switch-inset);\n width: var(--ds-switch-thumb-size);\n height: var(--ds-switch-thumb-size);\n border-radius: var(--dimension-radius-half);\n background-color: var(--color-foreground-tertiary);\n box-shadow: none;\n transform: translateY(-50%);\n transition:\n background-color var(--effect-motion-short-3),\n transform var(--effect-motion-short-3);\n pointer-events: none;\n}\n\n.thumb::after {\n content: '';\n position: absolute;\n inset: 0;\n border-radius: inherit;\n background: transparent;\n pointer-events: none;\n}\n\n:host(:hover:not(:disabled):not(.ds-control-inactive):not(.switch--readonly):not(.switch--presentation))\n .thumb::after {\n background: var(--ds-switch-thumb-hover);\n}\n\n:host(:active:not(:disabled):not(.ds-control-inactive):not(.switch--readonly):not(.switch--presentation))\n .thumb::after {\n background: var(--ds-switch-thumb-pressed);\n}\n\n:host(.checked) .thumb {\n background-color: var(--color-background-primary);\n box-shadow: none;\n transform: translate3d(var(--ds-switch-travel), -50%, 0);\n}\n\n@media (prefers-reduced-motion: reduce) {\n :host {\n transition: none;\n }\n\n .thumb {\n transition: none;\n }\n}\n",
353
353
  "type": "registry:ui"
354
354
  },
355
355
  {
356
356
  "path": "src/wc/components/Switch/Switch.tsx",
357
- "content": "import { AttachInternals, Component, Element, Prop, State, Event, EventEmitter, Watch, h, Host } from '@stencil/core';\nimport { DEFAULT_REQUIRED_MESSAGE, setRequiredValidity } from '../../utils';\n\nexport type SwitchSize = 'md' | 'sm' | 'xs';\n\nlet generatedLabelId = 0;\n\n@Component({\n tag: 'ds-switch',\n styleUrl: 'Switch.css',\n shadow: true,\n formAssociated: true,\n})\nexport class Switch {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n /** Current on/off state. The initial value is restored when the owning form resets. */\n @Prop({ mutable: true }) checked: boolean = false;\n\n @Prop({ reflect: true }) name: string | undefined;\n @Prop() value: string = 'on';\n\n /** Optional value submitted when unchecked; omitted by default like a native checkbox. */\n @Prop() uncheckedValue: string | undefined;\n\n /** Associates the switch with a form by id when it is rendered outside that form. */\n @Prop({ reflect: true }) form: string | undefined;\n\n @Prop({ reflect: true }) disabled: boolean = false;\n @Prop({ reflect: true }) readOnly: boolean = false;\n @Prop({ reflect: true }) required: boolean = false;\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n @Prop() isInactive: boolean = false;\n\n /** Compact track size for placement inside controls, menus, and form rows. */\n @Prop() size: SwitchSize = 'md';\n\n /**\n * Removes standalone control semantics so a composite owner such as a\n * `menuitemcheckbox` can use the switch as an aria-hidden visual indicator.\n */\n @Prop() presentation: boolean = false;\n\n @Event() dsChange!: EventEmitter<boolean>;\n\n private initialChecked = false;\n @State() private formDisabled = false;\n @State() private focused = false;\n @State() private touched = false;\n\n componentWillLoad() {\n this.initialChecked = this.checked;\n this.syncFormValue();\n }\n\n componentDidLoad() {\n this.syncNativeLabels();\n }\n\n @Watch('checked')\n @Watch('value')\n @Watch('uncheckedValue')\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('presentation')\n @Watch('required')\n syncFormValue() {\n const inactive = this.presentation || this.isInactive || this.disabled || this.formDisabled;\n const submissionValue = this.checked ? this.value : this.uncheckedValue ?? null;\n const state = this.checked ? 'checked' : 'unchecked';\n this.internals.setFormValue(inactive ? null : submissionValue, state);\n\n const missing = this.required && !inactive && !this.checked;\n setRequiredValidity(this.internals, missing, this.requiredMessage);\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.checked = this.initialChecked;\n this.focused = false;\n this.touched = false;\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n this.checked = state === 'checked';\n }\n\n private syncNativeLabels() {\n if (this.presentation) return;\n if (this.el.hasAttribute('aria-label') || this.el.hasAttribute('aria-labelledby')) return;\n\n const labelIds = (Array.from(this.internals.labels) as HTMLLabelElement[]).map(label => {\n if (!label.id) label.id = `ds-switch-label-${++generatedLabelId}`;\n return label.id;\n });\n if (labelIds.length) this.el.setAttribute('aria-labelledby', labelIds.join(' '));\n }\n\n private handleClick = () => {\n if (this.presentation || this.readOnly || this.isInactive || this.disabled || this.formDisabled) return;\n this.checked = !this.checked;\n this.dsChange.emit(this.checked);\n };\n\n private handleKeyDown = (event: KeyboardEvent) => {\n if (this.presentation || this.readOnly || this.isInactive || this.disabled || this.formDisabled) return;\n if (event.key === ' ' || event.key === 'Enter') {\n event.preventDefault();\n this.handleClick();\n }\n };\n\n private handleFocus = () => {\n if (!this.presentation) this.focused = true;\n };\n\n private handleBlur = () => {\n if (this.presentation) return;\n this.focused = false;\n this.touched = true;\n };\n\n render() {\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n const invalid = this.required && !inactive && !this.checked;\n const dirty = this.checked !== this.initialChecked;\n // Switch owns its structural track and thumb strokes so nested interaction layers cannot cover them.\n return (\n <Host\n role={this.presentation ? undefined : 'switch'}\n aria-checked={this.presentation ? undefined : String(this.checked)}\n aria-disabled={!this.presentation && inactive ? 'true' : undefined}\n aria-readonly={!this.presentation && this.readOnly ? 'true' : undefined}\n aria-required={!this.presentation && this.required ? 'true' : undefined}\n aria-invalid={!this.presentation && invalid ? 'true' : undefined}\n aria-hidden={this.presentation ? 'true' : undefined}\n data-checked={this.checked ? '' : undefined}\n data-unchecked={!this.checked ? '' : undefined}\n data-disabled={inactive ? '' : undefined}\n data-readonly={this.readOnly ? '' : undefined}\n data-required={this.required ? '' : undefined}\n data-focused={!this.presentation && this.focused ? '' : undefined}\n data-filled={!this.presentation && this.checked ? '' : undefined}\n data-dirty={!this.presentation && dirty ? '' : undefined}\n data-touched={!this.presentation && this.touched ? '' : undefined}\n data-valid={!this.presentation && !inactive && !invalid ? '' : undefined}\n data-invalid={!this.presentation && invalid ? '' : undefined}\n tabIndex={this.presentation || inactive ? -1 : 0}\n class={{\n switch: true,\n checked: this.checked,\n 'switch--md': this.size === 'md',\n 'switch--sm': this.size === 'sm',\n 'switch--xs': this.size === 'xs',\n 'switch--readonly': this.readOnly,\n 'switch--presentation': this.presentation,\n 'ds-focus-ring': true,\n 'ds-control-inactive': inactive,\n }}\n onClick={this.handleClick}\n onKeyDown={this.handleKeyDown}\n onFocus={this.handleFocus}\n onBlur={this.handleBlur}\n >\n <span class=\"thumb\" />\n </Host>\n );\n }\n}\n",
357
+ "content": "import { AttachInternals, Component, Element, Prop, State, Event, EventEmitter, Watch, h, Host } from '@stencil/core';\nimport { DEFAULT_REQUIRED_MESSAGE, setRequiredValidity } from '../../utils';\n\nexport type SwitchSize = 'lg' | 'md' | 'sm' | 'xs';\n\nlet generatedLabelId = 0;\n\n@Component({\n tag: 'ds-switch',\n styleUrl: 'Switch.css',\n shadow: true,\n formAssociated: true,\n})\nexport class Switch {\n @Element() el!: HTMLElement;\n @AttachInternals() internals!: ElementInternals;\n\n /** Current on/off state. The initial value is restored when the owning form resets. */\n @Prop({ mutable: true }) checked: boolean = false;\n\n @Prop({ reflect: true }) name: string | undefined;\n @Prop() value: string = 'on';\n\n /** Optional value submitted when unchecked; omitted by default like a native checkbox. */\n @Prop() uncheckedValue: string | undefined;\n\n /** Associates the switch with a form by id when it is rendered outside that form. */\n @Prop({ reflect: true }) form: string | undefined;\n\n @Prop({ reflect: true }) disabled: boolean = false;\n @Prop({ reflect: true }) readOnly: boolean = false;\n @Prop({ reflect: true }) required: boolean = false;\n @Prop() requiredMessage: string = DEFAULT_REQUIRED_MESSAGE;\n @Prop() isInactive: boolean = false;\n\n /** Compact track size for placement inside controls, menus, and form rows. */\n @Prop() size: SwitchSize = 'md';\n\n /**\n * Removes standalone control semantics so a composite owner such as a\n * `menuitemcheckbox` can use the switch as an aria-hidden visual indicator.\n */\n @Prop() presentation: boolean = false;\n\n @Event() dsChange!: EventEmitter<boolean>;\n\n private initialChecked = false;\n @State() private formDisabled = false;\n @State() private focused = false;\n @State() private touched = false;\n\n componentWillLoad() {\n this.initialChecked = this.checked;\n this.syncFormValue();\n }\n\n componentDidLoad() {\n this.syncNativeLabels();\n }\n\n @Watch('checked')\n @Watch('value')\n @Watch('uncheckedValue')\n @Watch('disabled')\n @Watch('isInactive')\n @Watch('presentation')\n @Watch('required')\n syncFormValue() {\n const inactive = this.presentation || this.isInactive || this.disabled || this.formDisabled;\n const submissionValue = this.checked ? this.value : this.uncheckedValue ?? null;\n const state = this.checked ? 'checked' : 'unchecked';\n this.internals.setFormValue(inactive ? null : submissionValue, state);\n\n const missing = this.required && !inactive && !this.checked;\n setRequiredValidity(this.internals, missing, this.requiredMessage);\n }\n\n formDisabledCallback(disabled: boolean) {\n this.formDisabled = disabled;\n this.syncFormValue();\n }\n\n formResetCallback() {\n this.checked = this.initialChecked;\n this.focused = false;\n this.touched = false;\n }\n\n formStateRestoreCallback(state: string | File | FormData | null) {\n this.checked = state === 'checked';\n }\n\n private syncNativeLabels() {\n if (this.presentation) return;\n if (this.el.hasAttribute('aria-label') || this.el.hasAttribute('aria-labelledby')) return;\n\n const labelIds = (Array.from(this.internals.labels) as HTMLLabelElement[]).map(label => {\n if (!label.id) label.id = `ds-switch-label-${++generatedLabelId}`;\n return label.id;\n });\n if (labelIds.length) this.el.setAttribute('aria-labelledby', labelIds.join(' '));\n }\n\n private handleClick = () => {\n if (this.presentation || this.readOnly || this.isInactive || this.disabled || this.formDisabled) return;\n this.checked = !this.checked;\n this.dsChange.emit(this.checked);\n };\n\n private handleKeyDown = (event: KeyboardEvent) => {\n if (this.presentation || this.readOnly || this.isInactive || this.disabled || this.formDisabled) return;\n if (event.key === ' ' || event.key === 'Enter') {\n event.preventDefault();\n this.handleClick();\n }\n };\n\n private handleFocus = () => {\n if (!this.presentation) this.focused = true;\n };\n\n private handleBlur = () => {\n if (this.presentation) return;\n this.focused = false;\n this.touched = true;\n };\n\n render() {\n const inactive = this.isInactive || this.disabled || this.formDisabled;\n const invalid = this.required && !inactive && !this.checked;\n const dirty = this.checked !== this.initialChecked;\n // Switch owns its structural track and thumb strokes so nested interaction layers cannot cover them.\n return (\n <Host\n role={this.presentation ? undefined : 'switch'}\n aria-checked={this.presentation ? undefined : String(this.checked)}\n aria-disabled={!this.presentation && inactive ? 'true' : undefined}\n aria-readonly={!this.presentation && this.readOnly ? 'true' : undefined}\n aria-required={!this.presentation && this.required ? 'true' : undefined}\n aria-invalid={!this.presentation && invalid ? 'true' : undefined}\n aria-hidden={this.presentation ? 'true' : undefined}\n data-checked={this.checked ? '' : undefined}\n data-unchecked={!this.checked ? '' : undefined}\n data-disabled={inactive ? '' : undefined}\n data-readonly={this.readOnly ? '' : undefined}\n data-required={this.required ? '' : undefined}\n data-focused={!this.presentation && this.focused ? '' : undefined}\n data-filled={!this.presentation && this.checked ? '' : undefined}\n data-dirty={!this.presentation && dirty ? '' : undefined}\n data-touched={!this.presentation && this.touched ? '' : undefined}\n data-valid={!this.presentation && !inactive && !invalid ? '' : undefined}\n data-invalid={!this.presentation && invalid ? '' : undefined}\n tabIndex={this.presentation || inactive ? -1 : 0}\n class={{\n switch: true,\n checked: this.checked,\n 'switch--lg': this.size === 'lg',\n 'switch--md': this.size === 'md',\n 'switch--sm': this.size === 'sm',\n 'switch--xs': this.size === 'xs',\n 'switch--readonly': this.readOnly,\n 'switch--presentation': this.presentation,\n 'ds-focus-ring': true,\n 'ds-control-inactive': inactive,\n }}\n onClick={this.handleClick}\n onKeyDown={this.handleKeyDown}\n onFocus={this.handleFocus}\n onBlur={this.handleBlur}\n >\n <span class=\"thumb\" />\n </Host>\n );\n }\n}\n",
358
358
  "type": "registry:ui"
359
359
  }
360
360
  ]
@@ -3,8 +3,10 @@
3
3
  * the inset highlight in a combined `--effect-elevation-*` box shadow.
4
4
  *
5
5
  * Import `@ds-mo/ui/control-elevation.css`, apply `.ds-control-elevation`, and
6
- * select one level modifier. The owner still supplies layout, radius, surface,
7
- * and any backdrop blur. Keep wrapped controls' optional resting borders off.
6
+ * select one level modifier. Add `--press-scale` when the wrapper contains an
7
+ * approved press-scaling button so its surface and elevation move as one unit.
8
+ * The owner still supplies layout, radius, surface, and any backdrop blur.
9
+ * Keep wrapped controls' optional resting borders off.
8
10
  */
9
11
 
10
12
  .ds-control-elevation {
@@ -38,3 +40,26 @@
38
40
  --ds-control-elevation-shadow: var(--effect-shadow-elevated-floating);
39
41
  --ds-control-elevation-highlight: var(--effect-highlight-elevated-floating);
40
42
  }
43
+
44
+ /*
45
+ * Transfer an approved button's physical press feedback to the elevated
46
+ * wrapper. The inherited override keeps the nested native button at rest so
47
+ * the wrapper and control do not compound their scale.
48
+ */
49
+ .ds-control-elevation--press-scale {
50
+ --ds-control-press-active-scale: var(--dimension-scale-default);
51
+
52
+ transition: scale var(--effect-motion-short-2);
53
+ }
54
+
55
+ .ds-control-elevation--press-scale[data-ds-press-active] {
56
+ scale: var(--dimension-scale-subtle);
57
+ }
58
+
59
+ @media (prefers-reduced-motion: reduce) {
60
+ .ds-control-elevation--press-scale,
61
+ .ds-control-elevation--press-scale[data-ds-press-active] {
62
+ scale: none;
63
+ transition: none;
64
+ }
65
+ }
@@ -5,7 +5,7 @@ export type ButtonFilledIntent = 'neutral' | 'brand' | 'ai' | 'negative' | 'warn
5
5
  export type ButtonFilledContrast = 'bold' | 'strong' | 'medium' | 'faint';
6
6
  export type ButtonFilledBackground = ChoiceBackground;
7
7
  export type ButtonFilledVariant = 'icon' | 'label' | 'icon-label';
8
- export type ButtonFilledSize = 'md' | 'sm' | 'xs';
8
+ export type ButtonFilledSize = 'lg' | 'md' | 'sm' | 'xs';
9
9
  export type ButtonFilledWidth = ControlWidth;
10
10
  export declare class ButtonFilled {
11
11
  el: HTMLElement;
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from '../../stencil-public-runtime';
2
- export type CheckboxSize = 'md' | 'sm' | 'xs';
2
+ export type CheckboxSize = 'lg' | 'md' | 'sm' | 'xs';
3
3
  export declare class Checkbox {
4
4
  internals: ElementInternals;
5
5
  private labelId;
@@ -1,7 +1,7 @@
1
1
  import { EventEmitter } from '../../stencil-public-runtime';
2
2
  import { type ControlWidth } from '../../utils';
3
3
  export type InputType = 'text' | 'email' | 'tel' | 'url' | 'search' | 'password';
4
- export type InputSize = 'md' | 'sm' | 'xs';
4
+ export type InputSize = 'lg' | 'md' | 'sm' | 'xs';
5
5
  export type InputWidth = ControlWidth;
6
6
  export declare class Input {
7
7
  el: HTMLElement;
@@ -1,13 +1,16 @@
1
1
  import { EventEmitter } from '../../stencil-public-runtime';
2
- import { type ChoicePopupAnchorAlignment } from '../../utils';
2
+ import { type ChoicePopupAnchorAlignment, type ControlSize } from '../../utils';
3
3
  import { type MenuAlign, type MenuSide } from './menu-position';
4
4
  import type { MenuItemData, MenuSection } from './menu-types';
5
5
  export type MenuSelectionMode = 'none' | 'single';
6
+ export type MenuSize = ControlSize;
6
7
  import { type ShellGradientPreset } from '../../shell/shell-gradient-presets';
7
8
  export declare class Menu {
8
9
  el: HTMLElement;
9
10
  open: boolean;
10
11
  items: MenuItemData[];
12
+ /** Choice-row density. */
13
+ size: MenuSize;
11
14
  /** Give ordinary rows mutually-exclusive menu semantics using isSelected. */
12
15
  selectionMode: MenuSelectionMode;
13
16
  sections: MenuSection[];
@@ -17,7 +17,6 @@ export declare class MobileSheetNav {
17
17
  dsBrowseContextChange: EventEmitter<NavChromeStyle>;
18
18
  dsClose: EventEmitter<void>;
19
19
  private get groups();
20
- private get items();
21
20
  private get activeId();
22
21
  handleOpenChange(open: boolean): void;
23
22
  handleKeyDown(event: KeyboardEvent): void;
@@ -4,7 +4,7 @@ export interface RadioOption {
4
4
  value: string;
5
5
  isInactive?: boolean;
6
6
  }
7
- export type RadioSize = 'md' | 'sm' | 'xs';
7
+ export type RadioSize = 'lg' | 'md' | 'sm' | 'xs';
8
8
  export declare class Radio {
9
9
  el: HTMLElement;
10
10
  internals: ElementInternals;
@@ -4,7 +4,7 @@ import { type ChoiceBackground, type ChoiceOption, type ChoiceSection } from '..
4
4
  export type SelectOption = ChoiceOption;
5
5
  export type SelectSection = ChoiceSection;
6
6
  export type SelectBackground = ChoiceBackground;
7
- export type SelectSize = 'md' | 'sm' | 'xs';
7
+ export type SelectSize = 'lg' | 'md' | 'sm' | 'xs';
8
8
  export type SelectWidth = ControlWidth;
9
9
  export declare class Select {
10
10
  el: HTMLElement;
@@ -82,15 +82,9 @@ export declare class Select {
82
82
  private readonly generatedId;
83
83
  private readonly listboxId;
84
84
  private readonly errorId;
85
- private triggerEl;
86
- private popupEl;
87
- private searchEl;
88
85
  private initialValue;
89
- private typeahead;
90
- private typeaheadTimer;
91
- private outsideHandler;
92
- private repositionHandler;
93
- private positionRetryRaf;
86
+ private readonly controller;
87
+ private createController;
94
88
  componentWillLoad(): void;
95
89
  componentDidLoad(): void;
96
90
  disconnectedCallback(): void;
@@ -111,23 +105,12 @@ export declare class Select {
111
105
  private get selectedOption();
112
106
  private get hasSelection();
113
107
  private get activeOptionId();
114
- private scrollActiveOptionIntoView;
115
- private bindPopupListeners;
116
- private unbindPopupListeners;
117
- private cancelPositionRetry;
118
- /** Retry until the conditionally rendered popup is mounted and measurable. */
119
- private schedulePositionUpdate;
120
- /** @returns `true` when the current popup was found and positioned. */
121
- private updatePosition;
122
- private openPopup;
123
108
  private closePopup;
109
+ private openPopup;
124
110
  private selectOption;
125
111
  private clearSelection;
126
- private moveActive;
127
- private setActiveEdge;
128
- private handleTypeahead;
129
- private handleListKeyDown;
130
- private handleTriggerKeyDown;
112
+ private readonly handleListKeyDown;
113
+ private readonly handleTriggerKeyDown;
131
114
  private renderOption;
132
115
  render(): any;
133
116
  }
@@ -86,15 +86,9 @@ export declare class SelectMulti {
86
86
  private readonly generatedId;
87
87
  private readonly listboxId;
88
88
  private readonly errorId;
89
- private triggerEl;
90
- private popupEl;
91
- private searchEl;
92
89
  private initialValues;
93
- private typeahead;
94
- private typeaheadTimer;
95
- private outsideHandler;
96
- private repositionHandler;
97
- private positionRetryRaf;
90
+ private readonly controller;
91
+ private createController;
98
92
  componentWillLoad(): void;
99
93
  componentDidLoad(): void;
100
94
  disconnectedCallback(): void;
@@ -115,21 +109,12 @@ export declare class SelectMulti {
115
109
  private get resolvedValues();
116
110
  private get hasSelection();
117
111
  private get activeOptionId();
118
- private scrollActiveOptionIntoView;
119
- private bindPopupListeners;
120
- private unbindPopupListeners;
121
- private cancelPositionRetry;
122
- /** Retry until the conditionally rendered popup is mounted and measurable. */
123
- private schedulePositionUpdate;
124
- /** @returns `true` when the current popup was found and positioned. */
125
- private updatePosition;
126
- private openPopup;
127
112
  private closePopup;
113
+ private openPopup;
128
114
  private toggleOption;
129
115
  private clearSelection;
130
- private handleTypeahead;
131
- private handleListKeyDown;
132
- private handleTriggerKeyDown;
116
+ private readonly handleListKeyDown;
117
+ private readonly handleTriggerKeyDown;
133
118
  private renderOption;
134
119
  render(): any;
135
120
  }
@@ -18,6 +18,7 @@ export declare class ShellTools {
18
18
  toolShortcutsLabel: string;
19
19
  inboxLabel: string;
20
20
  inboxNavigationLabel: string;
21
+ presentationChanged(next: 'drawer' | 'fullscreen', previous: 'drawer' | 'fullscreen'): void;
21
22
  dsToolChange: EventEmitter<{
22
23
  id: PanelToolsToolId;
23
24
  selected: boolean;
@@ -1,5 +1,5 @@
1
1
  import { EventEmitter } from '../../stencil-public-runtime';
2
- export type SwitchSize = 'md' | 'sm' | 'xs';
2
+ export type SwitchSize = 'lg' | 'md' | 'sm' | 'xs';
3
3
  export declare class Switch {
4
4
  el: HTMLElement;
5
5
  internals: ElementInternals;
@@ -33,7 +33,7 @@ import { IconColor as IconColor1, IconSize } from "./components/Icon/Icon";
33
33
  import { InputSize, InputType, InputWidth } from "./components/Input/Input";
34
34
  import { LoaderColor, LoaderSize } from "./components/Loader/Loader";
35
35
  import { MenuItemData, MenuSection } from "./components/Menu/menu-types";
36
- import { MenuSelectionMode } from "./components/Menu/Menu";
36
+ import { MenuSelectionMode, MenuSize } from "./components/Menu/Menu";
37
37
  import { MenuAlign, MenuSide } from "./components/Menu/menu-position";
38
38
  import { ChoicePopupAnchorAlignment } from "./utils";
39
39
  import { ShellGradientPreset } from "./shell/shell-gradient-presets";
@@ -92,7 +92,7 @@ export { IconColor as IconColor1, IconSize } from "./components/Icon/Icon";
92
92
  export { InputSize, InputType, InputWidth } from "./components/Input/Input";
93
93
  export { LoaderColor, LoaderSize } from "./components/Loader/Loader";
94
94
  export { MenuItemData, MenuSection } from "./components/Menu/menu-types";
95
- export { MenuSelectionMode } from "./components/Menu/Menu";
95
+ export { MenuSelectionMode, MenuSize } from "./components/Menu/Menu";
96
96
  export { MenuAlign, MenuSide } from "./components/Menu/menu-position";
97
97
  export { ChoicePopupAnchorAlignment } from "./utils";
98
98
  export { ShellGradientPreset } from "./shell/shell-gradient-presets";
@@ -1276,6 +1276,11 @@ export namespace Components {
1276
1276
  * @default TOKEN_CSS_LENGTHS.space050
1277
1277
  */
1278
1278
  "sideOffset": number | string;
1279
+ /**
1280
+ * Choice-row density.
1281
+ * @default 'md'
1282
+ */
1283
+ "size": MenuSize;
1279
1284
  }
1280
1285
  interface DsMessage {
1281
1286
  /**
@@ -5350,6 +5355,11 @@ declare namespace LocalJSX {
5350
5355
  * @default TOKEN_CSS_LENGTHS.space050
5351
5356
  */
5352
5357
  "sideOffset"?: number | string;
5358
+ /**
5359
+ * Choice-row density.
5360
+ * @default 'md'
5361
+ */
5362
+ "size"?: MenuSize;
5353
5363
  }
5354
5364
  interface DsMessage {
5355
5365
  /**
@@ -7310,6 +7320,7 @@ declare namespace LocalJSX {
7310
7320
  }
7311
7321
  interface DsMenuAttributes {
7312
7322
  "open": boolean;
7323
+ "size": MenuSize;
7313
7324
  "selectionMode": MenuSelectionMode;
7314
7325
  "side": MenuSide;
7315
7326
  "align": MenuAlign;
@@ -1,6 +1,8 @@
1
1
  import { FunctionalComponent, VNode } from '../stencil-public-runtime';
2
2
  import type { ChoiceOption } from './choice-list';
3
+ import { type ControlSize } from './control-text';
3
4
  interface ChoiceSearchProps {
5
+ size?: ControlSize;
4
6
  value: string;
5
7
  placeholder: string;
6
8
  ariaLabel?: string;
@@ -15,12 +17,14 @@ interface ChoiceSearchProps {
15
17
  }
16
18
  export declare const ChoiceSearch: FunctionalComponent<ChoiceSearchProps>;
17
19
  interface ChoiceFooterProps {
20
+ size?: ControlSize;
18
21
  clearLabel: string;
19
22
  summary?: string;
20
23
  onClear: (event: MouseEvent) => void;
21
24
  }
22
25
  export declare const ChoiceFooter: FunctionalComponent<ChoiceFooterProps>;
23
26
  interface ChoiceOptionRowProps {
27
+ size?: ControlSize;
24
28
  id: string;
25
29
  option: ChoiceOption;
26
30
  selected: boolean;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Transfers an eligible button pointer press to its elevated wrapper.
3
+ *
4
+ * The wrapper cannot rely on `:active`: Chromium also activates ancestors of a
5
+ * disabled native button. Window-level release cleanup preserves the native
6
+ * release-outside behavior without changing pointer capture or click delivery.
7
+ */
8
+ export declare function beginElevatedControlPress(event: PointerEvent, eligible: boolean): void;
9
+ //# sourceMappingURL=control-press.d.ts.map
@@ -1,6 +1,8 @@
1
1
  import type { TextVariant } from '../components/Text/text-types';
2
2
  /** Shared public control sizes. Typography is derived; it is not a separate density API. */
3
- export type ControlSize = 'md' | 'sm' | 'xs';
3
+ export type ControlSize = 'lg' | 'md' | 'sm' | 'xs';
4
4
  /** Complete text variant selected internally by each control size. */
5
5
  export declare const CONTROL_TEXT_VARIANT: Record<ControlSize, TextVariant>;
6
+ /** Supporting copy paired with each control density in two-line choice rows. */
7
+ export declare const CONTROL_SUPPORTING_TEXT_VARIANT: Record<ControlSize, TextVariant>;
6
8
  //# sourceMappingURL=control-text.d.ts.map
@@ -12,7 +12,7 @@ export type { TokenDefaultKey } from './token-defaults';
12
12
  export { DEFAULT_REQUIRED_MESSAGE, setRequiredValidity } from './required-validity';
13
13
  export { controlWidthClass } from './control-width';
14
14
  export type { ControlWidth } from './control-width';
15
- export { CONTROL_TEXT_VARIANT } from './control-text';
15
+ export { CONTROL_SUPPORTING_TEXT_VARIANT, CONTROL_TEXT_VARIANT } from './control-text';
16
16
  export type { ControlSize } from './control-text';
17
17
  export { choicePopupMinWidth, resolveChoicePopupAlignOffset, } from './choice-popup-alignment';
18
18
  export type { ChoicePopupAnchorAlignment } from './choice-popup-alignment';