@adia-ai/web-components 0.8.40 → 0.8.41

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 (378) hide show
  1. package/CHANGELOG.md +50 -3
  2. package/MIGRATION.md +1042 -16
  3. package/README.md +25 -30
  4. package/USAGE.md +26 -0
  5. package/bin/audit-template-children.mjs +556 -0
  6. package/components/accordion/accordion-item.a2ui.json +4 -0
  7. package/components/accordion/accordion-item.yaml +2 -0
  8. package/components/accordion/accordion.a2ui.json +4 -0
  9. package/components/accordion/accordion.yaml +2 -0
  10. package/components/action-list/action-item.a2ui.json +24 -2
  11. package/components/action-list/action-item.yaml +28 -3
  12. package/components/action-list/action-list.a2ui.json +6 -1
  13. package/components/action-list/action-list.class.js +52 -11
  14. package/components/action-list/action-list.css +43 -3
  15. package/components/action-list/action-list.d.ts +4 -2
  16. package/components/action-list/action-list.yaml +5 -0
  17. package/components/adia-mark/adia-mark.a2ui.json +1 -0
  18. package/components/adia-wordmark/adia-wordmark.a2ui.json +1 -0
  19. package/components/agent-artifact/agent-artifact.a2ui.json +1 -0
  20. package/components/agent-feedback-bar/agent-feedback-bar.a2ui.json +6 -5
  21. package/components/agent-feedback-bar/agent-feedback-bar.yaml +6 -2
  22. package/components/agent-questions/agent-questions.a2ui.json +1 -0
  23. package/components/agent-reasoning/agent-reasoning.a2ui.json +6 -5
  24. package/components/agent-reasoning/agent-reasoning.yaml +6 -2
  25. package/components/agent-suggestions/agent-suggestions.a2ui.json +11 -2
  26. package/components/agent-suggestions/agent-suggestions.class.js +2 -1
  27. package/components/agent-suggestions/agent-suggestions.d.ts +4 -4
  28. package/components/agent-suggestions/agent-suggestions.yaml +4 -2
  29. package/components/agent-trace/agent-trace.a2ui.json +6 -5
  30. package/components/agent-trace/agent-trace.yaml +6 -2
  31. package/components/alert/alert.a2ui.json +1 -0
  32. package/components/alert/alert.css +1 -1
  33. package/components/anchor-bar/anchor-bar.a2ui.json +1 -0
  34. package/components/aside/aside.a2ui.json +1 -0
  35. package/components/avatar/avatar-group.a2ui.json +7 -1
  36. package/components/avatar/avatar-group.yaml +1 -0
  37. package/components/avatar/avatar.a2ui.json +12 -11
  38. package/components/avatar/avatar.d.ts +1 -1
  39. package/components/avatar/avatar.yaml +4 -4
  40. package/components/badge/badge.a2ui.json +1 -0
  41. package/components/block/block.a2ui.json +1 -0
  42. package/components/blockquote/blockquote.a2ui.json +1 -0
  43. package/components/breadcrumb/breadcrumb.a2ui.json +1 -0
  44. package/components/button/button.a2ui.json +1 -3
  45. package/components/button/button.yaml +1 -3
  46. package/components/calendar-grid/calendar-grid.a2ui.json +1 -0
  47. package/components/calendar-grid/calendar-grid.class.js +2 -1
  48. package/components/calendar-picker/calendar-picker.a2ui.json +143 -24
  49. package/components/calendar-picker/calendar-picker.class.js +490 -197
  50. package/components/calendar-picker/calendar-picker.css +120 -171
  51. package/components/calendar-picker/calendar-picker.yaml +225 -47
  52. package/components/canvas/canvas.a2ui.json +6 -5
  53. package/components/canvas/canvas.d.ts +37 -0
  54. package/components/canvas/canvas.js +72 -3
  55. package/components/canvas/canvas.yaml +6 -2
  56. package/components/card/card.a2ui.json +25 -2
  57. package/components/card/card.class.js +3 -0
  58. package/components/card/card.css +31 -1
  59. package/components/card/card.d.ts +2 -2
  60. package/components/card/card.examples.md +3 -3
  61. package/components/card/card.yaml +28 -4
  62. package/components/chart/chart.a2ui.json +9 -4
  63. package/components/chart/chart.class.js +6 -6
  64. package/components/chart/chart.css +6 -4
  65. package/components/chart/chart.d.ts +1 -1
  66. package/components/chart/chart.yaml +5 -3
  67. package/components/chart-legend/chart-legend.a2ui.json +1 -0
  68. package/components/chat-thread/chat-input.a2ui.json +1 -0
  69. package/components/chat-thread/chat-thread.a2ui.json +10 -9
  70. package/components/chat-thread/chat-thread.yaml +13 -20
  71. package/components/check/check.a2ui.json +10 -9
  72. package/components/check/check.yaml +4 -4
  73. package/components/code/code.a2ui.json +1 -0
  74. package/components/col/col.a2ui.json +1 -0
  75. package/components/color-input/color-input.a2ui.json +1 -0
  76. package/components/color-picker/color-picker.a2ui.json +1 -0
  77. package/components/combobox/combobox.a2ui.json +3 -2
  78. package/components/combobox/combobox.yaml +2 -2
  79. package/components/command/command.a2ui.json +12 -10
  80. package/components/command/command.class.js +3 -2
  81. package/components/command/command.yaml +17 -4
  82. package/components/context-menu/context-menu.a2ui.json +1 -0
  83. package/components/date-range-picker/date-range-picker.a2ui.json +2 -1
  84. package/components/date-range-picker/date-range-picker.yaml +1 -1
  85. package/components/datetime-picker/datetime-picker.a2ui.json +15 -108
  86. package/components/datetime-picker/datetime-picker.class.js +37 -690
  87. package/components/datetime-picker/datetime-picker.css +11 -183
  88. package/components/datetime-picker/datetime-picker.d.ts +4 -5
  89. package/components/datetime-picker/datetime-picker.yaml +44 -135
  90. package/components/demo-toggle/demo-toggle.a2ui.json +1 -0
  91. package/components/demo-toggle/demo-toggle.css +1 -1
  92. package/components/description-list/description-list.a2ui.json +1 -0
  93. package/components/display-field/display-field.a2ui.json +1 -0
  94. package/components/divider/divider.a2ui.json +1 -0
  95. package/components/drawer/drawer.a2ui.json +1 -3
  96. package/components/drawer/drawer.css +1 -1
  97. package/components/drawer/drawer.yaml +0 -2
  98. package/components/drilldown/drilldown.a2ui.json +22 -20
  99. package/components/drilldown/drilldown.class.js +4 -3
  100. package/components/drilldown/drilldown.css +6 -6
  101. package/components/drilldown/drilldown.yaml +40 -9
  102. package/components/embed/embed.a2ui.json +6 -5
  103. package/components/embed/embed.yaml +2 -2
  104. package/components/empty-state/empty-state.a2ui.json +1 -0
  105. package/components/feed/feed-item.a2ui.json +25 -5
  106. package/components/feed/feed-item.yaml +28 -7
  107. package/components/feed/feed.a2ui.json +9 -2
  108. package/components/feed/feed.class.js +19 -12
  109. package/components/feed/feed.css +8 -8
  110. package/components/feed/feed.yaml +14 -1
  111. package/components/field/field.a2ui.json +1 -0
  112. package/components/field/field.examples.md +1 -1
  113. package/components/fields/fields.a2ui.json +1 -0
  114. package/components/fields/fields.examples.md +1 -1
  115. package/components/footer/footer.a2ui.json +1 -0
  116. package/components/frame/frame.a2ui.json +1 -0
  117. package/components/grid/grid.a2ui.json +1 -0
  118. package/components/header/header.a2ui.json +3 -2
  119. package/components/header/header.yaml +3 -3
  120. package/components/heatmap/heatmap.a2ui.json +2 -1
  121. package/components/heatmap/heatmap.class.js +1 -1
  122. package/components/heatmap/heatmap.css +4 -4
  123. package/components/heatmap/heatmap.yaml +1 -1
  124. package/components/icon/icon.a2ui.json +1 -0
  125. package/components/image/image.a2ui.json +28 -14
  126. package/components/image/image.class.js +38 -23
  127. package/components/image/image.css +18 -8
  128. package/components/image/image.d.ts +2 -2
  129. package/components/image/image.examples.md +3 -3
  130. package/components/image/image.yaml +23 -7
  131. package/components/inline-edit/inline-edit.a2ui.json +1 -0
  132. package/components/inline-message/inline-message.a2ui.json +3 -1
  133. package/components/inline-message/inline-message.class.js +3 -1
  134. package/components/inline-message/inline-message.d.ts +2 -2
  135. package/components/inline-message/inline-message.yaml +2 -1
  136. package/components/input/input.a2ui.json +66 -16
  137. package/components/input/input.class.js +64 -7
  138. package/components/input/input.css +14 -14
  139. package/components/input/input.d.ts +9 -4
  140. package/components/input/input.yaml +83 -10
  141. package/components/inspector/inspector.a2ui.json +1 -0
  142. package/components/integration-card/integration-card.a2ui.json +1 -0
  143. package/components/kbd/kbd.a2ui.json +1 -0
  144. package/components/link/link.a2ui.json +2 -1
  145. package/components/link/link.yaml +1 -1
  146. package/components/list/list-item.a2ui.json +9 -0
  147. package/components/list/list-item.yaml +12 -0
  148. package/components/list/list.a2ui.json +9 -3
  149. package/components/list/list.class.js +14 -1
  150. package/components/list/list.css +12 -1
  151. package/components/list/list.d.ts +7 -5
  152. package/components/list/list.yaml +16 -6
  153. package/components/list-window/list-window.a2ui.json +1 -0
  154. package/components/list-window/list-window.class.js +2 -1
  155. package/components/loading-overlay/loading-overlay.a2ui.json +1 -0
  156. package/components/mark/mark.a2ui.json +2 -1
  157. package/components/mark/mark.yaml +1 -1
  158. package/components/menu/menu-divider.a2ui.json +1 -0
  159. package/components/menu/menu-item.a2ui.json +3 -1
  160. package/components/menu/menu-item.yaml +6 -0
  161. package/components/menu/menu-label.a2ui.json +1 -0
  162. package/components/menu/menu.a2ui.json +3 -2
  163. package/components/menu/menu.class.js +2 -2
  164. package/components/menu/menu.d.ts +2 -2
  165. package/components/menu/menu.yaml +3 -3
  166. package/components/modal/modal.a2ui.json +1 -0
  167. package/components/nav/nav.a2ui.json +1 -0
  168. package/components/nav/nav.class.js +14 -3
  169. package/components/nav-group/nav-group.a2ui.json +1 -0
  170. package/components/nav-group/nav-group.class.js +3 -1
  171. package/components/nav-item/nav-item.a2ui.json +1 -0
  172. package/components/noodles/noodles.a2ui.json +1 -0
  173. package/components/noodles/noodles.class.js +5 -1
  174. package/components/number-format/number-format.a2ui.json +1 -0
  175. package/components/option-card/option-card.a2ui.json +1 -0
  176. package/components/otp-input/otp-input.a2ui.json +3 -2
  177. package/components/otp-input/otp-input.yaml +9 -2
  178. package/components/page/page.a2ui.json +13 -0
  179. package/components/page/page.d.ts +1 -1
  180. package/components/page/page.yaml +1 -0
  181. package/components/pagination/pagination.a2ui.json +1 -0
  182. package/components/pagination/pagination.class.js +3 -2
  183. package/components/pane/pane.a2ui.json +6 -0
  184. package/components/pane/pane.class.js +3 -3
  185. package/components/pane/pane.css +2 -2
  186. package/components/pane/pane.yaml +3 -0
  187. package/components/password-strength/password-strength.a2ui.json +1 -0
  188. package/components/pipeline-status/pipeline-status.a2ui.json +1 -0
  189. package/components/popover/popover.a2ui.json +7 -6
  190. package/components/popover/popover.class.js +2 -2
  191. package/components/popover/popover.d.ts +2 -2
  192. package/components/popover/popover.yaml +4 -4
  193. package/components/preview/preview.a2ui.json +1 -0
  194. package/components/preview/preview.class.js +5 -1
  195. package/components/progress/progress.a2ui.json +47 -24
  196. package/components/progress/progress.class.js +66 -11
  197. package/components/progress/progress.css +44 -26
  198. package/components/progress/progress.d.ts +7 -5
  199. package/components/progress/progress.examples.md +2 -2
  200. package/components/progress/progress.yaml +235 -53
  201. package/components/progress-row/progress-row.a2ui.json +16 -12
  202. package/components/progress-row/progress-row.d.ts +2 -2
  203. package/components/progress-row/progress-row.examples.md +2 -14
  204. package/components/progress-row/progress-row.yaml +28 -36
  205. package/components/qr-code/qr-code.a2ui.json +3 -2
  206. package/components/qr-code/qr-code.class.js +4 -4
  207. package/components/qr-code/qr-code.css +1 -1
  208. package/components/qr-code/qr-code.yaml +5 -3
  209. package/components/radio/radio.a2ui.json +8 -7
  210. package/components/radio/radio.class.js +8 -0
  211. package/components/radio/radio.yaml +3 -3
  212. package/components/radio-group/radio-group.a2ui.json +5 -3
  213. package/components/radio-group/radio-group.class.js +11 -0
  214. package/components/radio-group/radio-group.yaml +7 -1
  215. package/components/range/range.a2ui.json +6 -0
  216. package/components/range/range.class.js +2 -2
  217. package/components/range/range.css +2 -2
  218. package/components/range/range.yaml +3 -0
  219. package/components/rating/rating.a2ui.json +1 -0
  220. package/components/relative-time/relative-time.a2ui.json +1 -0
  221. package/components/richtext/richtext.a2ui.json +2 -1
  222. package/components/richtext/richtext.yaml +1 -1
  223. package/components/row/row.a2ui.json +1 -0
  224. package/components/search/search.a2ui.json +1 -0
  225. package/components/search/search.class.js +4 -1
  226. package/components/section/section.a2ui.json +1 -0
  227. package/components/segment/segment.a2ui.json +21 -10
  228. package/components/segment/segment.css +15 -0
  229. package/components/segment/segment.d.ts +2 -1
  230. package/components/segment/segment.yaml +20 -13
  231. package/components/segmented/segmented.a2ui.json +36 -7
  232. package/components/segmented/segmented.class.js +113 -22
  233. package/components/segmented/segmented.css +3 -0
  234. package/components/segmented/segmented.d.ts +3 -1
  235. package/components/segmented/segmented.yaml +80 -8
  236. package/components/select/select.a2ui.json +18 -13
  237. package/components/select/select.class.js +61 -37
  238. package/components/select/select.css +15 -13
  239. package/components/select/select.yaml +18 -7
  240. package/components/skeleton/skeleton.a2ui.json +8 -7
  241. package/components/skeleton/skeleton.class.js +8 -7
  242. package/components/skeleton/skeleton.css +21 -6
  243. package/components/skeleton/skeleton.d.ts +2 -2
  244. package/components/skeleton/skeleton.examples.md +2 -2
  245. package/components/skeleton/skeleton.yaml +3 -2
  246. package/components/skip-nav/skip-nav.a2ui.json +1 -0
  247. package/components/slider/slider.a2ui.json +6 -0
  248. package/components/slider/slider.class.js +20 -3
  249. package/components/slider/slider.css +2 -2
  250. package/components/slider/slider.yaml +3 -0
  251. package/components/spinner/spinner.a2ui.json +1 -0
  252. package/components/stack/stack.a2ui.json +1 -0
  253. package/components/stat/stat.a2ui.json +1 -0
  254. package/components/stat/stat.js +2 -2
  255. package/components/step-progress/step-progress.a2ui.json +1 -0
  256. package/components/step-progress/step-progress.yaml +1 -1
  257. package/components/stepper/stepper-item.a2ui.json +7 -3
  258. package/components/stepper/stepper-item.yaml +15 -5
  259. package/components/stepper/stepper.a2ui.json +5 -1
  260. package/components/stepper/stepper.class.js +31 -16
  261. package/components/stepper/stepper.d.ts +1 -1
  262. package/components/stepper/stepper.yaml +4 -2
  263. package/components/stream/stream.a2ui.json +1 -0
  264. package/components/swatch/swatch.a2ui.json +1 -0
  265. package/components/swiper/swiper.a2ui.json +1 -0
  266. package/components/swiper/swiper.class.js +5 -1
  267. package/components/switch/switch.a2ui.json +12 -11
  268. package/components/switch/switch.css +1 -1
  269. package/components/switch/switch.yaml +6 -6
  270. package/components/table/table.a2ui.json +1 -0
  271. package/components/table/table.class.js +14 -12
  272. package/components/table/table.css +11 -9
  273. package/components/table-toolbar/table-toolbar.a2ui.json +1 -0
  274. package/components/table-toolbar/table-toolbar.class.js +38 -19
  275. package/components/table-toolbar/table-toolbar.css +1 -1
  276. package/components/tabs/tab.a2ui.json +9 -0
  277. package/components/tabs/tab.js +4 -0
  278. package/components/tabs/tab.yaml +15 -0
  279. package/components/tabs/tabs.a2ui.json +9 -4
  280. package/components/tabs/tabs.class.js +13 -1
  281. package/components/tabs/tabs.css +8 -0
  282. package/components/tabs/tabs.d.ts +2 -0
  283. package/components/tabs/tabs.yaml +9 -2
  284. package/components/tag/tag.a2ui.json +1 -0
  285. package/components/tags-input/tags-input.a2ui.json +22 -17
  286. package/components/tags-input/tags-input.class.js +13 -11
  287. package/components/tags-input/tags-input.css +10 -6
  288. package/components/tags-input/tags-input.d.ts +16 -7
  289. package/components/tags-input/tags-input.examples.md +2 -2
  290. package/components/tags-input/tags-input.yaml +25 -8
  291. package/components/text/text.a2ui.json +6 -5
  292. package/components/text/text.css +10 -5
  293. package/components/text/text.d.ts +2 -2
  294. package/components/text/text.yaml +9 -4
  295. package/components/textarea/textarea.a2ui.json +2 -1
  296. package/components/textarea/textarea.yaml +1 -1
  297. package/components/theme-provider/theme-provider.a2ui.json +2 -1
  298. package/components/theme-provider/theme-provider.d.ts +5 -3
  299. package/components/theme-provider/theme-provider.yaml +5 -3
  300. package/components/time-picker/time-picker.a2ui.json +1 -0
  301. package/components/time-picker/time-picker.class.js +4 -2
  302. package/components/timeline/timeline-item.a2ui.json +26 -3
  303. package/components/timeline/timeline-item.yaml +22 -7
  304. package/components/timeline/timeline.a2ui.json +4 -0
  305. package/components/timeline/timeline.class.js +29 -18
  306. package/components/timeline/timeline.d.ts +3 -3
  307. package/components/timeline/timeline.yaml +2 -0
  308. package/components/toast/toast.a2ui.json +10 -3
  309. package/components/toast/toast.yaml +20 -13
  310. package/components/toc/toc.a2ui.json +1 -0
  311. package/components/toggle-group/toggle-group.a2ui.json +7 -3
  312. package/components/toggle-group/toggle-group.class.js +40 -1
  313. package/components/toggle-group/toggle-group.css +6 -1
  314. package/components/toggle-group/toggle-group.d.ts +2 -2
  315. package/components/toggle-group/toggle-group.yaml +21 -12
  316. package/components/toggle-group/toggle-option.a2ui.json +7 -3
  317. package/components/toggle-group/toggle-option.yaml +13 -7
  318. package/components/toggle-scheme/toggle-scheme.a2ui.json +2 -1
  319. package/components/toggle-scheme/toggle-scheme.yaml +1 -1
  320. package/components/toolbar/toolbar-group.a2ui.json +1 -0
  321. package/components/toolbar/toolbar.a2ui.json +1 -0
  322. package/components/toolbar/toolbar.class.js +3 -0
  323. package/components/tooltip/tooltip.a2ui.json +1 -0
  324. package/components/tour/tour-step.a2ui.json +4 -0
  325. package/components/tour/tour-step.yaml +2 -0
  326. package/components/tour/tour.a2ui.json +4 -0
  327. package/components/tour/tour.yaml +2 -0
  328. package/components/tree/tree-item.a2ui.json +5 -0
  329. package/components/tree/tree-item.yaml +3 -0
  330. package/components/tree/tree.a2ui.json +4 -0
  331. package/components/tree/tree.class.js +2 -1
  332. package/components/tree/tree.yaml +2 -0
  333. package/components/upload/upload.a2ui.json +8 -1
  334. package/components/upload/upload.class.js +35 -5
  335. package/components/upload/upload.css +28 -0
  336. package/components/upload/upload.d.ts +16 -0
  337. package/components/upload/upload.yaml +27 -7
  338. package/components/visually-hidden/visually-hidden.a2ui.json +1 -0
  339. package/core/dom.d.ts +11 -0
  340. package/core/dom.js +34 -0
  341. package/core/form.js +7 -0
  342. package/core/index.d.ts +1 -0
  343. package/core/index.js +1 -0
  344. package/core/responsive.js +16 -4
  345. package/core/template.js +2 -1
  346. package/custom-elements.json +437 -489
  347. package/dist/host.min.css +1 -1
  348. package/dist/host.sheet.js +1 -1
  349. package/dist/theme-provider.min.js +5 -5
  350. package/dist/web-components.min.css +1 -1
  351. package/dist/web-components.min.js +91 -104
  352. package/dist/web-components.sheet.js +1 -1
  353. package/package.json +4 -2
  354. package/patterns/conversion-funnel/conversion-funnel.examples.html +15 -15
  355. package/patterns/inline-dialog/inline-dialog.examples.html +2 -2
  356. package/patterns/inline-dialog/inline-dialog.html +4 -0
  357. package/patterns/timezone-selector/timezone-selector.examples.html +1 -1
  358. package/patterns/timezone-selector/timezone-selector.html +2 -2
  359. package/styles/api/text.css +7 -0
  360. package/traits/_catalog.json +129 -41
  361. package/traits/anchor-positioning/anchor-positioning.js +1 -0
  362. package/traits/announcer/announcer.js +1 -0
  363. package/traits/arrow-grid-nav/arrow-grid-nav.js +1 -0
  364. package/traits/attention-pulse/attention-pulse.js +1 -0
  365. package/traits/count-up/count-up.js +1 -0
  366. package/traits/define.js +78 -0
  367. package/traits/focus-restore/focus-restore.js +2 -1
  368. package/traits/haptic-feedback/haptic-feedback.js +1 -0
  369. package/traits/input-mask/input-mask.js +1 -0
  370. package/traits/intersection-observer/intersection-observer.js +1 -0
  371. package/traits/layout-animation/layout-animation.js +1 -0
  372. package/traits/noise-texture/noise-texture.js +1 -0
  373. package/traits/resize-observer/resize-observer.js +1 -0
  374. package/traits/scroll-progress/scroll-progress.js +1 -0
  375. package/traits/snap-to-grid/snap-to-grid.js +1 -0
  376. package/traits/sound-feedback/sound-feedback.js +1 -0
  377. package/traits/spring-animate/spring-animate.js +1 -0
  378. package/traits/validation/validation.js +1 -0
@@ -18,8 +18,19 @@
18
18
  * <segment-ui value="tab3" text="Tab 3"></segment-ui>
19
19
  * </segmented-ui>
20
20
  *
21
- * Mutually exclusive toggle group with animated indicator pill.
22
- * Indicator uses CSS transform (GPU-accelerated).
21
+ * Mutually exclusive (single-select) toggle group with animated indicator
22
+ * pill. Indicator uses CSS transform (GPU-accelerated).
23
+ *
24
+ * <segmented-ui multiple value="bold,italic">
25
+ * <segment-ui value="bold" text="B"></segment-ui>
26
+ * <segment-ui value="italic" text="I"></segment-ui>
27
+ * </segmented-ui>
28
+ *
29
+ * [multiple] (gh#1369, #1363 C1): multi-select mode — `value` is a
30
+ * comma-separated set, no sliding indicator (each selected segment carries
31
+ * its own background instead), Space/Enter toggles instead of arrow-move.
32
+ * Absorbs <toggle-group-ui>/<toggle-option-ui>'s role, which are deprecated
33
+ * in favor of this axis.
23
34
  */
24
35
 
25
36
  import { UIFormElement } from '../../core/form.js';
@@ -28,6 +39,13 @@ import '../segment/segment.js';
28
39
  export class UISegmented extends UIFormElement {
29
40
  static properties = {
30
41
  ...UIFormElement.properties,
42
+ // gh#1369 (#1363 C1): multi-select axis, positive polarity matching
43
+ // <select-ui multiple> — not toggle-group's inverted `single`. Under
44
+ // [multiple], `value` becomes a comma-separated set (same encoding
45
+ // toggle-group-ui and select-ui[multiple] already use); the sliding
46
+ // indicator is suppressed and each selected segment carries its own
47
+ // background instead (segment.css).
48
+ multiple: { type: Boolean, default: false, reflect: true },
31
49
  value: { type: String, default: '', reflect: true },
32
50
  // gh#693: absent (default false) preserves the original self-select
33
51
  // behavior below. A path-bound consumer whose bound value can be a
@@ -55,7 +73,7 @@ export class UISegmented extends UIFormElement {
55
73
 
56
74
  connected() {
57
75
  super.connected();
58
- this.setAttribute('role', 'radiogroup');
76
+ this.setAttribute('role', this.multiple ? 'group' : 'radiogroup');
59
77
 
60
78
  if (!this.#bound) {
61
79
  this.#bound = true;
@@ -65,8 +83,10 @@ export class UISegmented extends UIFormElement {
65
83
 
66
84
  // Default to first segment if no value — unless allow-empty opts out
67
85
  // (a falsy path-bound value must render as truly empty, not silently
68
- // pick one, gh#693).
69
- if (!this.value && !this.allowEmpty) {
86
+ // pick one, gh#693). Multi-select never self-selects: an empty
87
+ // [multiple] value legitimately means "nothing chosen yet", the same
88
+ // default toggle-group-ui always had.
89
+ if (!this.multiple && !this.value && !this.allowEmpty) {
70
90
  const first = this.querySelector('segment-ui:not([disabled])');
71
91
  if (first) this.value = first.value || first.getAttribute('value') || '';
72
92
  }
@@ -112,7 +132,19 @@ export class UISegmented extends UIFormElement {
112
132
  return this.#segments.filter(s => !s.disabled);
113
133
  }
114
134
 
135
+ // [multiple] value encoding — comma-separated, matching toggle-group-ui
136
+ // and select-ui[multiple] (gh#1369).
137
+ get #valueSet() {
138
+ return new Set(this.value ? this.value.split(',').map(v => v.trim()).filter(Boolean) : []);
139
+ }
140
+
115
141
  render() {
142
+ // gh#1369 review: connected() only sets the host role ONCE — if
143
+ // `multiple` flips after connect, that one-shot value goes stale while
144
+ // children's roles (below) already update reactively every render.
145
+ // Keep the host role synchronized here too, every render.
146
+ this.setAttribute('role', this.multiple ? 'group' : 'radiogroup');
147
+
116
148
  // FEEDBACK-23: flag a non-<segment-ui> direct child once per element.
117
149
  // Runs before the empty-guard so an all-bare-<segment> group still warns.
118
150
  if (!UISegmented.#warnedNonSegment.has(this)) {
@@ -137,11 +169,14 @@ export class UISegmented extends UIFormElement {
137
169
  const segs = this.#segments;
138
170
  if (!segs.length) return;
139
171
 
140
- // Sync selection state
172
+ // Sync selection state — [multiple] checks Set membership (comma-
173
+ // separated value), single-select checks direct string equality.
174
+ const selectedSet = this.multiple ? this.#valueSet : null;
141
175
  let anyActive = false;
142
176
  for (const seg of segs) {
143
177
  const segVal = seg.value || seg.getAttribute('value') || '';
144
- const isActive = segVal === this.value;
178
+ const isActive = this.multiple ? selectedSet.has(segVal) : segVal === this.value;
179
+ seg.setAttribute('role', this.multiple ? 'checkbox' : 'radio');
145
180
  if (isActive) {
146
181
  seg.setAttribute('selected', '');
147
182
  anyActive = true;
@@ -152,6 +187,8 @@ export class UISegmented extends UIFormElement {
152
187
  }
153
188
  // allow-empty's "nothing selected" state must still be keyboard
154
189
  // reachable — mirrors list-ui's roving-tabindex fallback (list.class.js).
190
+ // [multiple] can legitimately have nothing selected at all times, same
191
+ // fallback applies.
155
192
  if (!anyActive) {
156
193
  const firstEnabled = segs.find((s) => !s.disabled);
157
194
  firstEnabled?.setAttribute('tabindex', '0');
@@ -159,10 +196,33 @@ export class UISegmented extends UIFormElement {
159
196
 
160
197
  if (this.label) this.setAttribute('aria-label', this.label);
161
198
 
199
+ // Sliding indicator is a single-select-only affordance — exactly one
200
+ // pill can slide to exactly one position. [multiple] relies on each
201
+ // segment's own [selected] background instead (segment.css).
202
+ // #updateIndicator itself guards on `this.multiple` (gh#1369 review) —
203
+ // call it unconditionally so this is the one call site, same as every
204
+ // other caller (ResizeObserver, document.fonts.ready).
162
205
  this.#updateIndicator(segs);
163
206
  }
164
207
 
165
208
  #updateIndicator(segs) {
209
+ // gh#1369 review: render() only skips this call in [multiple] mode —
210
+ // the ResizeObserver (connected()) and document.fonts.ready callbacks
211
+ // call #updateIndicator directly and asynchronously, bypassing that
212
+ // guard entirely. Without a guard HERE, a multiple-mode instance with
213
+ // exactly one segment selected (this.value has no comma, so it can
214
+ // exactly match a segment's own value) can grow a spurious sliding
215
+ // indicator after a late resize or font-load event. Guard the method
216
+ // itself so every call site — sync or async — is covered.
217
+ if (this.multiple) {
218
+ for (const stale of this.querySelectorAll(':scope > [data-indicator]')) {
219
+ stale.remove();
220
+ }
221
+ this.#indicator = null;
222
+ this.removeAttribute('data-indicator-ready');
223
+ return;
224
+ }
225
+
166
226
  const selectedIdx = segs.findIndex(s => (s.value || s.getAttribute('value') || '') === this.value);
167
227
 
168
228
  if (selectedIdx < 0) {
@@ -205,7 +265,18 @@ export class UISegmented extends UIFormElement {
205
265
 
206
266
  #select(seg) {
207
267
  if (seg.disabled) return;
208
- this.value = seg.value;
268
+ const segVal = seg.value || seg.getAttribute('value') || '';
269
+
270
+ if (this.multiple) {
271
+ if (!segVal) return;
272
+ const selected = this.#valueSet;
273
+ if (selected.has(segVal)) selected.delete(segVal);
274
+ else selected.add(segVal);
275
+ this.value = [...selected].join(',');
276
+ } else {
277
+ this.value = segVal;
278
+ }
279
+
209
280
  this.syncValue();
210
281
  this.dispatchEvent(new CustomEvent('change', { bubbles: true, detail: { value: this.value } }));
211
282
  }
@@ -215,26 +286,46 @@ export class UISegmented extends UIFormElement {
215
286
  if (seg && this.contains(seg)) this.#select(seg);
216
287
  };
217
288
 
289
+ // Shared roving-focus navigation — Home/End/Arrow keys move focus among
290
+ // enabled segments. Selection semantics diverge below: single-select is
291
+ // a radiogroup (arrow-move selects, native radio behavior); [multiple]
292
+ // is a checkbox toolbar (arrow-move only focuses, Space/Enter toggles).
293
+ #nextFocusTarget(current, key) {
294
+ const segs = this.#enabledSegments;
295
+ const idx = segs.indexOf(current);
296
+ if (idx < 0) return null; // current isn't an enabled segment — nothing to do
297
+ switch (key) {
298
+ case 'ArrowRight': case 'ArrowDown':
299
+ return segs[idx < segs.length - 1 ? idx + 1 : 0];
300
+ case 'ArrowLeft': case 'ArrowUp':
301
+ return segs[idx > 0 ? idx - 1 : segs.length - 1];
302
+ case 'Home': return segs[0];
303
+ case 'End': return segs[segs.length - 1];
304
+ default: return null; // key not handled
305
+ }
306
+ }
307
+
218
308
  #handleKeydown = (e) => {
219
309
  const target = e.target;
220
310
  if (target.tagName !== 'SEGMENT-UI') return;
221
- const segs = this.#enabledSegments;
222
- const idx = segs.indexOf(target);
223
- if (idx < 0) return;
224
311
 
225
- let next;
226
- switch (e.key) {
227
- case 'ArrowRight': case 'ArrowDown':
228
- next = idx < segs.length - 1 ? idx + 1 : 0; break;
229
- case 'ArrowLeft': case 'ArrowUp':
230
- next = idx > 0 ? idx - 1 : segs.length - 1; break;
231
- case 'Home': next = 0; break;
232
- case 'End': next = segs.length - 1; break;
233
- default: return;
312
+ if (this.multiple) {
313
+ if (e.key === 'Enter' || e.key === ' ') {
314
+ e.preventDefault();
315
+ this.#select(target);
316
+ return;
317
+ }
318
+ const next = this.#nextFocusTarget(target, e.key);
319
+ if (!next) return;
320
+ e.preventDefault();
321
+ next.focus();
322
+ return;
234
323
  }
235
324
 
325
+ const next = this.#nextFocusTarget(target, e.key);
326
+ if (!next) return;
236
327
  e.preventDefault();
237
- segs[next].focus();
238
- this.#select(segs[next]);
328
+ next.focus();
329
+ this.#select(next);
239
330
  };
240
331
  }
@@ -68,6 +68,9 @@
68
68
  display: none;
69
69
  }
70
70
 
71
+ /* [multiple] never sets data-indicator-ready (segmented.class.js render())
72
+ — the indicator span is never created/prepended in multi-select mode,
73
+ so it stays display:none via the rule above. */
71
74
  :scope[data-indicator-ready] > [data-indicator] {
72
75
  display: block;
73
76
  position: absolute;
@@ -12,8 +12,10 @@ export interface SegmentedChangeEventDetail<V extends string = string> {
12
12
  export type SegmentedChangeEvent<V extends string = string> = CustomEvent<SegmentedChangeEventDetail<V>>;
13
13
 
14
14
  export class UISegmented extends UIFormElement {
15
- /** Selected segment's value. */
15
+ /** Selected segment's value. Under `multiple`, a comma-separated set. */
16
16
  value: string;
17
+ /** Enables multi-select — comma-separated `value`, no sliding indicator, Space/Enter toggles (gh#1369). */
18
+ multiple: boolean;
17
19
 
18
20
  addEventListener<K extends keyof HTMLElementEventMap>(
19
21
  type: K,
@@ -5,14 +5,19 @@ status: stable
5
5
  component: Segmented
6
6
  category: navigation
7
7
  version: 1
8
- description: Single-select toggle group with an animated sliding indicator. Children must be segment-ui elements.
8
+ description: Toggle group with an animated sliding indicator (single-select) or independently-toggleable segments (multi-select via [multiple]). Children must be segment-ui elements.
9
9
  props:
10
10
  value:
11
- description: Value of the currently selected segment.
11
+ description: Value of the currently selected segment. Under [multiple], a comma-separated set of selected values (same encoding as select-ui[multiple]).
12
12
  type: string
13
13
  default: ''
14
+ multiple:
15
+ description: "Enables multi-select — value becomes a comma-separated set, the sliding indicator is suppressed (each selected segment carries its own background instead), and Space/Enter toggles a focused segment instead of arrow-move selecting it. Positive polarity, matching select-ui[multiple]. Absorbs the role of the deprecated <toggle-group-ui> + <toggle-option-ui> pair (gh#1369, #1363 C1). Unlike <toggle-group-ui single>, re-clicking an already-selected segment does NOT clear the selection — segmented-ui's single-select is standard radiogroup semantics (always exactly one selected once a value is set)."
16
+ type: boolean
17
+ default: false
18
+ reflect: true
14
19
  allowEmpty:
15
- description: Suppresses the connect-time self-select of the first enabled segment when value is falsy. Without it, an empty value silently selects the first segment; with it, a falsy value renders every segment unselected until a real value arrives — for path-bound consumers whose bound value can be a genuine empty state.
20
+ description: Suppresses the connect-time self-select of the first enabled segment when value is falsy. Without it, an empty value silently selects the first segment; with it, a falsy value renders every segment unselected until a real value arrives — for path-bound consumers whose bound value can be a genuine empty state. No effect under [multiple] — multi-select never self-selects (empty value already means "nothing chosen").
16
21
  type: boolean
17
22
  default: false
18
23
  attribute: allow-empty
@@ -22,20 +27,27 @@ events:
22
27
  slots:
23
28
  default:
24
29
  description: Child segment-ui elements that form the toggle group. Children MUST be segment-ui — bare segment tags render text but are silently ignored for the sliding indicator and role/aria-checked state.
30
+ parts:
25
31
  indicator:
26
- description: Auto-created sliding indicator element prepended on first render.
32
+ description: >-
33
+ Auto-created sliding indicator element prepended on first render —
34
+ template-owned anatomy part, not a light-DOM insertion point
35
+ (gh#1454). Lazily created once via an internal-field guard, never
36
+ adopted from a consumer-authored element.
27
37
  states:
28
38
  - name: idle
29
39
  description: Default, ready for interaction.
30
40
  traits: []
31
41
  tokens: {}
32
42
  a2ui:
43
+ allowedChildren:
44
+ - Segment
33
45
  rules:
34
- - rule: 'Single-select segmented control. Hosts <segment-ui> children; exactly one selected at a time.'
46
+ - rule: 'Single-select by default. Hosts <segment-ui> children; exactly one selected at a time.'
35
47
  reason: 'Single-select cluster contract.'
36
- - rule: 'For multi-select use <toggle-group-ui> + <toggle-option-ui> instead.'
37
- reason: 'Selection model boundary.'
38
- - rule: 'Use for view-mode switches (grid/list, light/dark) or short filter sets (3-5 options); for longer sets use <tabs-ui> or <select-ui>.'
48
+ - rule: 'For multi-select (filter chips, multi-flag toggles, day-of-week pickers), set [multiple] instead of using a separate component — value becomes a comma-separated set of selected segment values. <toggle-group-ui>/<toggle-option-ui> are deprecated; use <segmented-ui multiple> + <segment-ui> instead (gh#1369, #1363 C1).'
49
+ reason: 'Selection model axis — multiple absorbs the deprecated multi-select pair.'
50
+ - rule: 'Use for view-mode switches (grid/list, light/dark) or short filter sets (3-5 options); for longer sets use <tabs-ui> or <select-ui multiple>.'
39
51
  reason: 'Capacity guidance.'
40
52
  anti_patterns: []
41
53
  examples:
@@ -86,6 +98,54 @@ examples:
86
98
  "text": "Monthly"
87
99
  }
88
100
  ]
101
+ - name: multi-select-filter-chips
102
+ description: Multi-select segmented control (gh#1369) for filter chips — [multiple] set, value is a comma-separated set of active segments.
103
+ a2ui: >-
104
+ [
105
+ {
106
+ "id": "root",
107
+ "component": "Card",
108
+ "children": [
109
+ "sec"
110
+ ]
111
+ },
112
+ {
113
+ "id": "sec",
114
+ "component": "Section",
115
+ "children": [
116
+ "comp"
117
+ ]
118
+ },
119
+ {
120
+ "id": "comp",
121
+ "component": "Segmented",
122
+ "multiple": true,
123
+ "value": "bold,italic",
124
+ "children": [
125
+ "s1",
126
+ "s2",
127
+ "s3"
128
+ ]
129
+ },
130
+ {
131
+ "id": "s1",
132
+ "component": "Segment",
133
+ "value": "bold",
134
+ "text": "Bold"
135
+ },
136
+ {
137
+ "id": "s2",
138
+ "component": "Segment",
139
+ "value": "italic",
140
+ "text": "Italic"
141
+ },
142
+ {
143
+ "id": "s3",
144
+ "component": "Segment",
145
+ "value": "underline",
146
+ "text": "Underline"
147
+ }
148
+ ]
89
149
  keywords:
90
150
  - segmented
91
151
  - options
@@ -95,6 +155,9 @@ keywords:
95
155
  - tab
96
156
  - tabs
97
157
  - button-group
158
+ - filter
159
+ - multi-select
160
+ - filter-chips
98
161
  synonyms:
99
162
  button-group:
100
163
  - toolbar
@@ -125,11 +188,20 @@ synonyms:
125
188
  - segmented
126
189
  - radio
127
190
  - settings
191
+ filter:
192
+ - segmented
193
+ - toggle-group
194
+ - filter-chips
195
+ multi-select:
196
+ - segmented
197
+ - toggle-group
198
+ - select
128
199
  tags:
129
200
  - SegmentedControl
130
201
  - Segmented
131
202
  related:
132
203
  - Segment
133
204
  - ToggleGroup
205
+ - ToggleOption
134
206
  - Tabs
135
207
  - Radio
@@ -83,7 +83,10 @@
83
83
  },
84
84
  "maxlength": {
85
85
  "description": "Maximum character length for validation",
86
- "type": "number",
86
+ "type": [
87
+ "number",
88
+ "null"
89
+ ],
87
90
  "default": null
88
91
  },
89
92
  "min": {
@@ -93,7 +96,10 @@
93
96
  },
94
97
  "minlength": {
95
98
  "description": "Minimum character length for validation",
96
- "type": "number",
99
+ "type": [
100
+ "number",
101
+ "null"
102
+ ],
97
103
  "default": null
98
104
  },
99
105
  "multiple": {
@@ -158,7 +164,7 @@
158
164
  "summary-label": {
159
165
  "description": "Multi-select only (gh#442). Replaces the chip trigger with one\ntext line: a single selection shows its own label; N > 1 shows\n\"<summary-label> (N)\" — e.g. \"Organizations (2)\"; none shows the\nplaceholder. Popover checkbox rows, Backspace-removes-last, the\ncomma-separated form value, [select-all], and [clearable] are\nunchanged. Absent (default) = tag-ui chip trigger. Prefer this\nform for narrow triggers (filter bars) where wrapping chips\nwould balloon the control.\n",
160
166
  "type": "string",
161
- "default": null
167
+ "default": ""
162
168
  },
163
169
  "value": {
164
170
  "description": "Currently selected option value. With declarative <option> children, a child carrying the native `selected` attribute sets the initial value when this attribute is absent (native <select> parity).",
@@ -226,6 +232,14 @@
226
232
  "picker"
227
233
  ],
228
234
  "name": "UISelect",
235
+ "parts": {
236
+ "hint": {
237
+ "description": "Override slot for hint markup richer than the plain [hint] attribute string (inline links, code spans). Renders beneath the trigger at body-subtle typography. Mutually exclusive with [hint] per this description, but template-owned in practice (gh#1454): the trigger stamp is an unconditional `this.innerHTML = …` with no capture-before-wipe for `hint` (unlike `leading`/`trailing` on input-ui), so any pre-existing [slot=\"hint\"] content is discarded, not adopted, on first render."
238
+ },
239
+ "listbox": {
240
+ "description": "Popover container for options (role=listbox) — template-owned anatomy part, not a light-DOM insertion point (gh#1454). Its contents are always wiped (`innerHTML = ''`) and rebuilt wholesale from `.options`/ parsed `<option>` children on every options render."
241
+ }
242
+ },
229
243
  "related": [
230
244
  "image",
231
245
  "button",
@@ -234,17 +248,8 @@
234
248
  "upload"
235
249
  ],
236
250
  "slots": {
237
- "hint": {
238
- "description": "Override slot for hint markup richer than the plain [hint] attribute string (inline links, code spans). Renders beneath the trigger at body-subtle typography. Mutually exclusive with [hint]."
239
- },
240
- "label": {
241
- "description": "Label element above the trigger"
242
- },
243
- "listbox": {
244
- "description": "Popover container for options (role=listbox)"
245
- },
246
251
  "trigger": {
247
- "description": "Button that opens the listbox"
252
+ "description": "Button that opens the listbox. Stamped only if no consumer-authored [slot=\"trigger\"] is already present at first render (adopt-or-stamp) — a genuine light-DOM insertion point."
248
253
  }
249
254
  },
250
255
  "states": [
@@ -399,8 +399,7 @@ export class UISelect extends UIFormElement {
399
399
  this.#parseOptions();
400
400
  if (this.#options.length) {
401
401
  this.#renderOptions();
402
- const display = this.querySelector('[slot="display"]');
403
- if (display) display.textContent = this.#displayText();
402
+ this.#syncDisplayText();
404
403
  }
405
404
  });
406
405
  }
@@ -410,12 +409,13 @@ export class UISelect extends UIFormElement {
410
409
  render() {
411
410
  // SPEC-040 — in multi-select mode the trigger needs to host chips +
412
411
  // optional clear-x + optional "+N more" pill alongside the usual
413
- // leading / display / caret. We toggle host attribute [data-multi-chips]
414
- // so CSS can switch layout from single-line text to flex-wrap chip row.
412
+ // leading / display / caret. CSS switches layout from single-line
413
+ // text to flex-wrap chip row off the compound selector
414
+ // [multiple]:not([summary-label]) — both already-declared, reflected
415
+ // props, so no private data-* mirror is needed here (gh#1332
416
+ // Category A; dropped [data-multi-chips]).
415
417
  // gh#442 — [summary-label] keeps the single-line text layout: the
416
418
  // summary renders through [slot="display"], no chip row.
417
- if (this.multiple && !this.summaryLabel) this.setAttribute('data-multi-chips', '');
418
- else this.removeAttribute('data-multi-chips');
419
419
 
420
420
  // A11y (FB-88): name a placeholder-only select from its placeholder, like
421
421
  // input-ui, so it isn't unnamed for assistive tech (WCAG 4.1.2).
@@ -495,34 +495,7 @@ export class UISelect extends UIFormElement {
495
495
  // Re-attach listbox after trigger is stamped
496
496
  if (lb) this.appendChild(lb);
497
497
  } else {
498
- const display = this.querySelector('[slot="display"]');
499
- if (display && !this.multiple) {
500
- // Single-select: keep the canonical text-rendering path.
501
- if (display.hasAttribute('contenteditable')) {
502
- // Only update text when not actively editing (no active query)
503
- if (!this.#query) {
504
- const text = this.#displayText() === this.placeholder ? '' : this.#displayText();
505
- display.textContent = text;
506
- display.toggleAttribute('data-empty', !text);
507
- }
508
- } else {
509
- display.textContent = this.#displayText();
510
- }
511
- } else if (display && this.multiple) {
512
- // Multi-select: display element holds the placeholder ONLY when
513
- // no chips are present (otherwise the chip row IS the display).
514
- const hasChips = this.#values().length > 0;
515
- if (display.hasAttribute('contenteditable')) {
516
- // search input — keep placeholder; never inject the value text.
517
- display.setAttribute('data-placeholder', hasChips ? '' : (this.placeholder || ''));
518
- if (!this.#query) {
519
- display.textContent = '';
520
- display.toggleAttribute('data-empty', true);
521
- }
522
- } else {
523
- display.textContent = hasChips ? '' : (this.placeholder || '');
524
- }
525
- }
498
+ this.#syncDisplayText();
526
499
  }
527
500
 
528
501
  // Reflect the selected option's mark/avatar/icon in the trigger leading.
@@ -597,6 +570,57 @@ export class UISelect extends UIFormElement {
597
570
  }
598
571
  }
599
572
 
573
+ /**
574
+ * Reconcile the trigger's text with its [data-empty] flag — ALWAYS together.
575
+ *
576
+ * ADIA2-8927. For a searchable trigger the placeholder paints from
577
+ * `[slot="display"][contenteditable][data-empty]::before` (select.css), so
578
+ * text and flag are two halves of ONE state, not two independent writes:
579
+ * set the text and leave the flag and the label renders on top of the
580
+ * placeholder in the same box. `#displayText()` also falls back to
581
+ * `placeholder` when nothing is selected, and that string must never land in
582
+ * the editable surface as real content — the pseudo owns it, so writing it
583
+ * paints the placeholder twice, over itself.
584
+ *
585
+ * This lived inline in render()'s else-branch while `set options` and the
586
+ * FB-98 observer kept their own text-only writes, which is exactly how both
587
+ * drifted: a programmatically populated searchable select showed the overlap
588
+ * until the first click re-entered render() and repaired it. One caller-
589
+ * agnostic method is what keeps a third write site from repeating it.
590
+ */
591
+ #syncDisplayText() {
592
+ const display = this.querySelector('[slot="display"]');
593
+ if (!display) return; // trigger not stamped yet
594
+
595
+ if (!this.multiple) {
596
+ // Single-select: keep the canonical text-rendering path.
597
+ if (display.hasAttribute('contenteditable')) {
598
+ // Only update text when not actively editing (no active query)
599
+ if (!this.#query) {
600
+ const text = this.#displayText() === this.placeholder ? '' : this.#displayText();
601
+ display.textContent = text;
602
+ display.toggleAttribute('data-empty', !text);
603
+ }
604
+ } else {
605
+ display.textContent = this.#displayText();
606
+ }
607
+ } else {
608
+ // Multi-select: display element holds the placeholder ONLY when
609
+ // no chips are present (otherwise the chip row IS the display).
610
+ const hasChips = this.#values().length > 0;
611
+ if (display.hasAttribute('contenteditable')) {
612
+ // search input — keep placeholder; never inject the value text.
613
+ display.setAttribute('data-placeholder', hasChips ? '' : (this.placeholder || ''));
614
+ if (!this.#query) {
615
+ display.textContent = '';
616
+ display.toggleAttribute('data-empty', true);
617
+ }
618
+ } else {
619
+ display.textContent = hasChips ? '' : (this.placeholder || '');
620
+ }
621
+ }
622
+ }
623
+
600
624
  /** Parse <option> and <optgroup> children into internal model. */
601
625
  // FB-98: collect declarative <option>/<optgroup> children — direct OR nested
602
626
  // inside the template engine's display:contents wrapper spans (which carry
@@ -689,8 +713,7 @@ export class UISelect extends UIFormElement {
689
713
  }
690
714
  }
691
715
  this.#renderOptions();
692
- const display = this.querySelector('[slot="display"]');
693
- if (display) display.textContent = this.#displayText();
716
+ this.#syncDisplayText();
694
717
  // Pre-existing gap surfaced while adding mark (gh#339): #renderOptions()
695
718
  // only rebuilds the LISTBOX rows — the trigger's leading visual
696
719
  // (icon/avatar/mark) was only ever reconciled from render()'s call to
@@ -975,7 +998,8 @@ export class UISelect extends UIFormElement {
975
998
  return `${this.summaryLabel} (${values.length})`;
976
999
  }
977
1000
  // SPEC-040 — chips render the selection; display element holds the
978
- // placeholder when empty and is otherwise empty (CSS sees [data-multi-chips]).
1001
+ // placeholder when empty and is otherwise empty (CSS sees
1002
+ // [multiple]:not([summary-label])).
979
1003
  return this.value ? '' : this.placeholder;
980
1004
  }
981
1005
  const flat = this.#options.flatMap(o => o.options || [o]);
@@ -215,11 +215,13 @@
215
215
  }
216
216
 
217
217
  /* ── SPEC-040 — Multi-select chip trigger ──
218
- [data-multi-chips] is set on the host by render() when [multiple] is
219
- active. Layout switches from inline-text trigger to wrap-flex chip row.
220
- Tag-ui chips inside [data-chips] inherit the host's tag-ui token set;
221
- the +more pill is a CSS-only button styled to match. */
222
- :scope[data-multi-chips] [slot="trigger"] {
218
+ [multiple]:not([summary-label]) both already-declared, reflected
219
+ props; no private stamp needed (dropped the redundant [data-multi-chips]
220
+ mirror, gh#1332 Category A). Layout switches from inline-text trigger
221
+ to wrap-flex chip row. Tag-ui chips inside [data-chips] inherit the
222
+ host's tag-ui token set; the +more pill is a CSS-only button styled
223
+ to match. */
224
+ :scope[multiple]:not([summary-label]) [slot="trigger"] {
223
225
  flex-wrap: wrap;
224
226
  align-items: center;
225
227
  /* Pack chips left with a small gap — override the base trigger's
@@ -232,17 +234,17 @@
232
234
  it to grow as chips overflow. */
233
235
  padding-block: calc(var(--select-py, var(--a-space-1)));
234
236
  }
235
- :scope[data-multi-chips] [data-chips] {
237
+ :scope[multiple]:not([summary-label]) [data-chips] {
236
238
  display: contents; /* let chips participate in the trigger's flex line */
237
239
  }
238
- :scope[data-multi-chips] [data-chips] tag-ui {
240
+ :scope[multiple]:not([summary-label]) [data-chips] tag-ui {
239
241
  /* Chip styling tracks the active variant via tag-ui's own tokens. */
240
242
  --tag-size: var(--a-ui-tiny);
241
243
  }
242
244
  /* "+N more" overflow pill — a stamped <button-ui variant="ghost" size="sm">
243
245
  (gh issue 276: no native <button> in stamped DOM); pill shape + chip-row
244
246
  height via button token re-points, chrome stays button-ui's own. */
245
- :scope[data-multi-chips] [data-chips] [data-more] {
247
+ :scope[multiple]:not([summary-label]) [data-chips] [data-more] {
246
248
  --button-radius: var(--a-radius-full);
247
249
  --button-height: calc(var(--a-size) - var(--a-space-3));
248
250
  --button-fg: var(--select-fg-muted);
@@ -252,7 +254,7 @@
252
254
 
253
255
  /* Clear-all `x` affordance — stamped <button-ui variant="ghost" size="sm"
254
256
  icon="x"> (gh issue 276). Visible only when [clearable] AND chips. */
255
- :scope[data-multi-chips] [data-clear-all] {
257
+ :scope[multiple]:not([summary-label]) [data-clear-all] {
256
258
  --button-radius: var(--a-radius-full);
257
259
  --button-height: calc(var(--a-size) - var(--a-space-3));
258
260
  --button-fg: var(--select-fg-muted);
@@ -263,16 +265,16 @@
263
265
  }
264
266
  /* button-ui's own :scope display wins over the UA [hidden] rule —
265
267
  keep the explicit guard. */
266
- :scope[data-multi-chips] [data-clear-all][hidden] { display: none; }
268
+ :scope[multiple]:not([summary-label]) [data-clear-all][hidden] { display: none; }
267
269
 
268
270
  /* In multi-select with chips present, the trigger display element is
269
271
  the rest-of-row slot — make it flex-grow but not push chips off. */
270
- :scope[data-multi-chips] [slot="display"] {
272
+ :scope[multiple]:not([summary-label]) [slot="display"] {
271
273
  flex: 1;
272
274
  min-width: 4ch;
273
275
  }
274
276
  /* Hide the display when empty in multi-mode (chips fill the slot). */
275
- :scope[data-multi-chips] [slot="display"]:empty {
277
+ :scope[multiple]:not([summary-label]) [slot="display"]:empty {
276
278
  flex: 0 0 0;
277
279
  padding: 0;
278
280
  }
@@ -432,7 +434,7 @@ select-ui [slot="hint"] {
432
434
  margin-top: var(--select-hint-mt, var(--a-space-1));
433
435
  font-size: var(--select-hint-size, var(--a-ui-xs));
434
436
  color: var(--select-hint-fg, var(--select-fg-muted));
435
- line-height: var(--select-hint-lh, 1.4);
437
+ line-height: var(--select-hint-lh, var(--a-font-leading-normal));
436
438
  }
437
439
 
438
440
  /* ── SPEC-040 — Multi-select popover affordances ──