@ds-mo/ui 14.34.0 → 14.35.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 (285) hide show
  1. package/dist/.package-ready.json +1 -1
  2. package/dist/agent-patterns.json +1 -1
  3. package/dist/agent.json +310 -23
  4. package/dist/angular/ds-filter-menu.d.ts +1 -1
  5. package/dist/angular/ds-filter-menu.js +3 -3
  6. package/dist/angular/ds-menu.d.ts +1 -1
  7. package/dist/angular/ds-menu.js +3 -3
  8. package/dist/angular/ds-select.d.ts +1 -1
  9. package/dist/angular/ds-select.js +3 -3
  10. package/dist/angular/ds-tab-group.d.ts +1 -1
  11. package/dist/angular/ds-tab-group.js +3 -3
  12. package/dist/angular/ds-table-filter.d.ts +1 -1
  13. package/dist/angular/ds-table-filter.js +3 -3
  14. package/dist/angular/ds-table-group.d.ts +1 -1
  15. package/dist/angular/ds-table-group.js +3 -3
  16. package/dist/angular/ds-table-preferences.d.ts +39 -0
  17. package/dist/angular/ds-table-preferences.js +69 -0
  18. package/dist/angular/ds-table-saved-views.d.ts +1 -1
  19. package/dist/angular/ds-table-saved-views.js +3 -3
  20. package/dist/angular/ds-table-search.d.ts +1 -1
  21. package/dist/angular/ds-table-search.js +3 -3
  22. package/dist/angular/ds-table-sort.d.ts +1 -1
  23. package/dist/angular/ds-table-sort.js +3 -3
  24. package/dist/angular/ds-table-toolbar.d.ts +1 -1
  25. package/dist/angular/ds-table-toolbar.js +3 -3
  26. package/dist/angular/ds-table.d.ts +3 -1
  27. package/dist/angular/ds-table.js +8 -5
  28. package/dist/angular/index.d.ts +1 -1
  29. package/dist/angular/index.js +1 -0
  30. package/dist/angular/proxies.d.ts +1 -0
  31. package/dist/angular/proxies.js +1 -0
  32. package/dist/components/ds-agent-activity.js +1 -1
  33. package/dist/components/ds-agent-questionnaire.js +1 -1
  34. package/dist/components/ds-agent-response.js +1 -1
  35. package/dist/components/ds-agent-source-list.js +1 -1
  36. package/dist/components/ds-agent-tool-call.js +1 -1
  37. package/dist/components/ds-attachment-list.js +1 -1
  38. package/dist/components/ds-badge.js +1 -1
  39. package/dist/components/ds-banner.js +1 -1
  40. package/dist/components/ds-bar-action.js +1 -1
  41. package/dist/components/ds-bar-nav.js +1 -1
  42. package/dist/components/ds-bar-page-title.js +1 -1
  43. package/dist/components/ds-bar-title.js +1 -1
  44. package/dist/components/ds-bar-workflow.js +1 -1
  45. package/dist/components/ds-breadcrumb.js +1 -1
  46. package/dist/components/ds-button-filled.js +1 -1
  47. package/dist/components/ds-button-unfilled.js +1 -1
  48. package/dist/components/ds-card-action-center.js +1 -1
  49. package/dist/components/ds-card-chart.js +1 -1
  50. package/dist/components/ds-card-navigation.js +1 -1
  51. package/dist/components/ds-card-overview.js +1 -1
  52. package/dist/components/ds-card-setting.js +1 -1
  53. package/dist/components/ds-card-settings-scope.js +1 -1
  54. package/dist/components/ds-chart-legend.js +1 -1
  55. package/dist/components/ds-chart.js +1 -1
  56. package/dist/components/ds-checkbox.js +1 -1
  57. package/dist/components/ds-chip.js +1 -1
  58. package/dist/components/ds-code-block.js +1 -1
  59. package/dist/components/ds-conversation-list-item.js +1 -1
  60. package/dist/components/ds-conversation-list-section.js +1 -1
  61. package/dist/components/ds-empty-state.js +1 -1
  62. package/dist/components/ds-field.js +1 -1
  63. package/dist/components/ds-filter-menu.js +1 -1
  64. package/dist/components/ds-input.js +1 -1
  65. package/dist/components/ds-markdown.js +1 -1
  66. package/dist/components/ds-menu.js +1 -1
  67. package/dist/components/ds-message-actions.js +1 -1
  68. package/dist/components/ds-message-bubble.js +1 -1
  69. package/dist/components/ds-message-bubble.js.map +1 -1
  70. package/dist/components/ds-message-composer.js +1 -1
  71. package/dist/components/ds-message-scroller.js +1 -1
  72. package/dist/components/ds-message.js +1 -1
  73. package/dist/components/ds-mobile-bar-nav.js +1 -1
  74. package/dist/components/ds-mobile-header.js +1 -1
  75. package/dist/components/ds-mobile-section-switcher.js +1 -1
  76. package/dist/components/ds-mobile-sheet-nav.js +1 -1
  77. package/dist/components/ds-modal.js +1 -1
  78. package/dist/components/ds-pagination.js +1 -1
  79. package/dist/components/ds-panel-nav.js +1 -1
  80. package/dist/components/ds-panel-sub-nav.js +1 -1
  81. package/dist/components/ds-panel-tool-header.js +1 -1
  82. package/dist/components/ds-panel-tool-search.js +1 -1
  83. package/dist/components/ds-panel-tools.js +1 -1
  84. package/dist/components/ds-paper-texture.js +1 -1
  85. package/dist/components/ds-radio.js +1 -1
  86. package/dist/components/ds-score.js +1 -1
  87. package/dist/components/ds-scroll-overlay.js +1 -1
  88. package/dist/components/ds-select.js +1 -1
  89. package/dist/components/ds-shell-app.js +1 -1
  90. package/dist/components/ds-shell-app.js.map +1 -1
  91. package/dist/components/ds-shell-page.js +1 -1
  92. package/dist/components/ds-shell-tools.js +1 -1
  93. package/dist/components/ds-skeleton.js +1 -1
  94. package/dist/components/ds-slider.js +1 -1
  95. package/dist/components/ds-swatch-picker.js +1 -1
  96. package/dist/components/ds-switch.js +1 -1
  97. package/dist/components/ds-tab-group.js +1 -1
  98. package/dist/components/ds-table-filter.js +1 -1
  99. package/dist/components/ds-table-filter.js.map +1 -1
  100. package/dist/components/ds-table-group.js +1 -1
  101. package/dist/components/ds-table-group.js.map +1 -1
  102. package/dist/components/ds-table-preferences.d.ts +11 -0
  103. package/dist/components/ds-table-preferences.js +2 -0
  104. package/dist/components/ds-table-preferences.js.map +1 -0
  105. package/dist/components/ds-table-saved-views.js +1 -1
  106. package/dist/components/ds-table-saved-views.js.map +1 -1
  107. package/dist/components/ds-table-search.js +1 -1
  108. package/dist/components/ds-table-search.js.map +1 -1
  109. package/dist/components/ds-table-sort.js +1 -1
  110. package/dist/components/ds-table-sort.js.map +1 -1
  111. package/dist/components/ds-table-toolbar.js +1 -1
  112. package/dist/components/ds-table-toolbar.js.map +1 -1
  113. package/dist/components/ds-table.js +1 -1
  114. package/dist/components/ds-table.js.map +1 -1
  115. package/dist/components/ds-tag.js +1 -1
  116. package/dist/components/ds-text.js +1 -1
  117. package/dist/components/ds-textarea.js +1 -1
  118. package/dist/components/ds-toast.js +1 -1
  119. package/dist/components/ds-tooltip-chart.js +1 -1
  120. package/dist/components/ds-tooltip.js +1 -1
  121. package/dist/components/ds-typing-indicator.js +1 -1
  122. package/dist/components/{p-ByoywoBH.js → p-B-9fdIWv.js} +2 -2
  123. package/dist/components/{p-CnK36Be0.js → p-BINSI_KF.js} +2 -2
  124. package/dist/components/{p-CK2S28gU.js → p-BPhbwXOv.js} +2 -2
  125. package/dist/components/{p-CMq-jKEI.js → p-BPlWEidl.js} +2 -2
  126. package/dist/components/{p-DyyfWSLB.js → p-BQaO7cg_.js} +2 -2
  127. package/dist/components/{p-Br0dx2uP.js → p-BQsVI_vO.js} +2 -2
  128. package/dist/components/{p-B_nPct4G.js → p-BbfBvMRR.js} +2 -2
  129. package/dist/components/p-BcdHgPi5.js +2 -0
  130. package/dist/components/p-BcdHgPi5.js.map +1 -0
  131. package/dist/components/{p-D_QtZZR8.js → p-BmFeeR4q.js} +2 -2
  132. package/dist/components/{p-CXAa9TUM.js → p-BnWxbhFP.js} +2 -2
  133. package/dist/components/{p-CvplapTD.js → p-BnzsBNUY.js} +2 -2
  134. package/dist/components/{p-BlWNMytv.js → p-C1_3CVma.js} +2 -2
  135. package/dist/components/p-C1_3CVma.js.map +1 -0
  136. package/dist/components/{p-DjCnGxSd.js → p-C7CZ9pnP.js} +2 -2
  137. package/dist/components/{p-DBZwI7HY.js → p-C8ek-1SP.js} +2 -2
  138. package/dist/components/{p-BEttT0UB.js → p-CAxDdYjh.js} +2 -2
  139. package/dist/components/{p-BqGdNrVl.js → p-CE540WZ7.js} +2 -2
  140. package/dist/components/{p-B5QVra1K.js → p-CI7VfYC3.js} +2 -2
  141. package/dist/components/p-CKarsuR2.js +2 -0
  142. package/dist/components/p-CKarsuR2.js.map +1 -0
  143. package/dist/components/{p-AkSwrAKR.js → p-CLGtPE0K.js} +2 -2
  144. package/dist/components/{p-C51ytXM6.js → p-CSnsz3Eb.js} +2 -2
  145. package/dist/components/{p-Dx7WcWW1.js → p-CZOiTaNa.js} +2 -2
  146. package/dist/components/{p-xxuHxP3u.js → p-Cmj04E5Q.js} +2 -2
  147. package/dist/components/p-Cmj04E5Q.js.map +1 -0
  148. package/dist/components/{p-zZuAAiiI.js → p-Cq_Qpe_C.js} +2 -2
  149. package/dist/components/{p-CweS0FfJ.js → p-CqlBlv55.js} +2 -2
  150. package/dist/components/p-CwqFJMvP.js +2 -0
  151. package/dist/components/{p-DEyvEIYL.js.map → p-CwqFJMvP.js.map} +1 -1
  152. package/dist/components/{p-C8XJgV1n.js → p-Cwz9TPVS.js} +2 -2
  153. package/dist/components/{p-C1QHxVvP.js → p-D44g2Kw1.js} +2 -2
  154. package/dist/components/{p-CKo2nySr.js → p-DC69DgAR.js} +2 -2
  155. package/dist/components/{p-CQqZgTYk.js → p-DEGNrPLe.js} +2 -2
  156. package/dist/components/{p-OxdAzO4v.js → p-DFjIBS5B.js} +2 -2
  157. package/dist/components/{p-BwsS_Hyr.js → p-DGzn4k4h.js} +2 -2
  158. package/dist/components/{p-Dj296RmE.js → p-DQQFN2sw.js} +2 -2
  159. package/dist/components/{p-CIoeHcCk.js → p-DTDLlinw.js} +2 -2
  160. package/dist/components/p-D_u8WhlU.js +2 -0
  161. package/dist/components/{p-CHBuzAYH.js.map → p-D_u8WhlU.js.map} +1 -1
  162. package/dist/components/{p-CqeMumqy.js → p-DdspY23v.js} +2 -2
  163. package/dist/components/{p-DzbEJN1A.js → p-Dk-8Ju0Q.js} +2 -2
  164. package/dist/components/{p-Da9VpZX2.js → p-DtLQIMsa.js} +2 -2
  165. package/dist/components/{p-Cw_npFwN.js → p-FPZbS19l.js} +2 -2
  166. package/dist/components/{p-BMERUCe1.js → p-KBrC9IG4.js} +2 -2
  167. package/dist/components/p-N5SHSVo2.js +2 -0
  168. package/dist/components/{p-CHUdAW1Q.js.map → p-N5SHSVo2.js.map} +1 -1
  169. package/dist/components/p-QjkufCcn.js +2 -0
  170. package/dist/components/p-QjkufCcn.js.map +1 -0
  171. package/dist/components/p-UAIxh67Q.js +2 -0
  172. package/dist/components/p-UAIxh67Q.js.map +1 -0
  173. package/dist/components/{p-CIYJhyKt.js → p-beCz9OnD.js} +2 -2
  174. package/dist/components/{p-LZ8F08Fj.js → p-beRFLHbB.js} +2 -2
  175. package/dist/components/p-beRFLHbB.js.map +1 -0
  176. package/dist/components/{p-4DNyCYKM.js → p-mpk_Mm8_.js} +2 -2
  177. package/dist/components/{p-DrjY8Mpy.js → p-n68x-ZKp.js} +2 -2
  178. package/dist/components/{p-CNDTHTf6.js → p-omPm8_vx.js} +2 -2
  179. package/dist/components/{p-lv4YQEsa.js → p-umAf0fBn.js} +2 -2
  180. package/dist/components/{p-ya8A_SMw.js → p-xHGqrz_p.js} +2 -2
  181. package/dist/components/{p-ya8A_SMw.js.map → p-xHGqrz_p.js.map} +1 -1
  182. package/dist/components/{p-CBQzTgvI.js → p-y06IxxoN.js} +2 -2
  183. package/dist/components/{p-CoE7yza3.js → p-yZRXPHDe.js} +2 -2
  184. package/dist/docs/components.json +2242 -720
  185. package/dist/lib/components/ShellApp/shell-app-types.d.ts +3 -1
  186. package/dist/mcp-data/registry/filter-menu.json +102 -15
  187. package/dist/mcp-data/registry/menu.json +22 -3
  188. package/dist/mcp-data/registry/registry.json +312 -22
  189. package/dist/mcp-data/registry/select.json +43 -8
  190. package/dist/mcp-data/registry/shell-app.json +4 -3
  191. package/dist/mcp-data/registry/tab-group.json +20 -3
  192. package/dist/mcp-data/registry/table-filter.json +26 -6
  193. package/dist/mcp-data/registry/table-group.json +59 -7
  194. package/dist/mcp-data/registry/table-preferences.json +511 -0
  195. package/dist/mcp-data/registry/table-saved-views.json +45 -7
  196. package/dist/mcp-data/registry/table-search.json +24 -6
  197. package/dist/mcp-data/registry/table-sort.json +22 -4
  198. package/dist/mcp-data/registry/table-toolbar.json +26 -6
  199. package/dist/mcp-data/registry/table.json +70 -4
  200. package/dist/react/components.d.ts +1 -0
  201. package/dist/react/components.js +1 -0
  202. package/dist/react/ds-table-preferences.d.ts +21 -0
  203. package/dist/react/ds-table-preferences.js +23 -0
  204. package/dist/react/ds-table.d.ts +1 -0
  205. package/dist/react/ds-table.js +1 -0
  206. package/dist/types/components/FilterMenu/FilterMenu.d.ts +16 -0
  207. package/dist/types/components/Menu/Menu.d.ts +2 -0
  208. package/dist/types/components/Select/Select.d.ts +5 -1
  209. package/dist/types/components/ShellApp/shell-app-types.d.ts +3 -1
  210. package/dist/types/components/TabGroup/TabGroup.d.ts +1 -0
  211. package/dist/types/components/Table/Table.d.ts +8 -0
  212. package/dist/types/components/TableFilter/TableFilter.d.ts +2 -0
  213. package/dist/types/components/TableGroup/TableGroup.d.ts +5 -0
  214. package/dist/types/components/TablePreferences/TablePreferences.d.ts +56 -0
  215. package/dist/types/components/TableSavedViews/TableSavedViews.d.ts +4 -0
  216. package/dist/types/components/TableSearch/TableSearch.d.ts +2 -0
  217. package/dist/types/components/TableSort/TableSort.d.ts +2 -0
  218. package/dist/types/components/TableToolbar/TableToolbar.d.ts +2 -0
  219. package/dist/types/components.d.ts +420 -18
  220. package/dist/vue/components.d.ts +1 -0
  221. package/dist/vue/components.js +1 -0
  222. package/dist/vue/ds-filter-menu.js +5 -0
  223. package/dist/vue/ds-menu.js +1 -0
  224. package/dist/vue/ds-select.js +2 -0
  225. package/dist/vue/ds-tab-group.js +1 -0
  226. package/dist/vue/ds-table-filter.js +1 -0
  227. package/dist/vue/ds-table-group.js +3 -0
  228. package/dist/vue/ds-table-preferences.d.ts +3 -0
  229. package/dist/vue/ds-table-preferences.js +45 -0
  230. package/dist/vue/ds-table-saved-views.js +2 -0
  231. package/dist/vue/ds-table-search.js +1 -0
  232. package/dist/vue/ds-table-sort.js +1 -0
  233. package/dist/vue/ds-table-toolbar.js +2 -1
  234. package/dist/vue/ds-table.js +5 -0
  235. package/package.json +1 -1
  236. package/dist/components/p-BlWNMytv.js.map +0 -1
  237. package/dist/components/p-CHBuzAYH.js +0 -2
  238. package/dist/components/p-CHUdAW1Q.js +0 -2
  239. package/dist/components/p-CUL5_8JM.js +0 -2
  240. package/dist/components/p-CUL5_8JM.js.map +0 -1
  241. package/dist/components/p-DEyvEIYL.js +0 -2
  242. package/dist/components/p-DVWOUQfT.js +0 -2
  243. package/dist/components/p-DVWOUQfT.js.map +0 -1
  244. package/dist/components/p-LZ8F08Fj.js.map +0 -1
  245. package/dist/components/p-xxuHxP3u.js.map +0 -1
  246. /package/dist/components/{p-ByoywoBH.js.map → p-B-9fdIWv.js.map} +0 -0
  247. /package/dist/components/{p-CnK36Be0.js.map → p-BINSI_KF.js.map} +0 -0
  248. /package/dist/components/{p-CK2S28gU.js.map → p-BPhbwXOv.js.map} +0 -0
  249. /package/dist/components/{p-CMq-jKEI.js.map → p-BPlWEidl.js.map} +0 -0
  250. /package/dist/components/{p-DyyfWSLB.js.map → p-BQaO7cg_.js.map} +0 -0
  251. /package/dist/components/{p-Br0dx2uP.js.map → p-BQsVI_vO.js.map} +0 -0
  252. /package/dist/components/{p-B_nPct4G.js.map → p-BbfBvMRR.js.map} +0 -0
  253. /package/dist/components/{p-D_QtZZR8.js.map → p-BmFeeR4q.js.map} +0 -0
  254. /package/dist/components/{p-CXAa9TUM.js.map → p-BnWxbhFP.js.map} +0 -0
  255. /package/dist/components/{p-CvplapTD.js.map → p-BnzsBNUY.js.map} +0 -0
  256. /package/dist/components/{p-DjCnGxSd.js.map → p-C7CZ9pnP.js.map} +0 -0
  257. /package/dist/components/{p-DBZwI7HY.js.map → p-C8ek-1SP.js.map} +0 -0
  258. /package/dist/components/{p-BEttT0UB.js.map → p-CAxDdYjh.js.map} +0 -0
  259. /package/dist/components/{p-BqGdNrVl.js.map → p-CE540WZ7.js.map} +0 -0
  260. /package/dist/components/{p-B5QVra1K.js.map → p-CI7VfYC3.js.map} +0 -0
  261. /package/dist/components/{p-AkSwrAKR.js.map → p-CLGtPE0K.js.map} +0 -0
  262. /package/dist/components/{p-C51ytXM6.js.map → p-CSnsz3Eb.js.map} +0 -0
  263. /package/dist/components/{p-Dx7WcWW1.js.map → p-CZOiTaNa.js.map} +0 -0
  264. /package/dist/components/{p-zZuAAiiI.js.map → p-Cq_Qpe_C.js.map} +0 -0
  265. /package/dist/components/{p-CweS0FfJ.js.map → p-CqlBlv55.js.map} +0 -0
  266. /package/dist/components/{p-C8XJgV1n.js.map → p-Cwz9TPVS.js.map} +0 -0
  267. /package/dist/components/{p-C1QHxVvP.js.map → p-D44g2Kw1.js.map} +0 -0
  268. /package/dist/components/{p-CKo2nySr.js.map → p-DC69DgAR.js.map} +0 -0
  269. /package/dist/components/{p-CQqZgTYk.js.map → p-DEGNrPLe.js.map} +0 -0
  270. /package/dist/components/{p-OxdAzO4v.js.map → p-DFjIBS5B.js.map} +0 -0
  271. /package/dist/components/{p-BwsS_Hyr.js.map → p-DGzn4k4h.js.map} +0 -0
  272. /package/dist/components/{p-Dj296RmE.js.map → p-DQQFN2sw.js.map} +0 -0
  273. /package/dist/components/{p-CIoeHcCk.js.map → p-DTDLlinw.js.map} +0 -0
  274. /package/dist/components/{p-CqeMumqy.js.map → p-DdspY23v.js.map} +0 -0
  275. /package/dist/components/{p-DzbEJN1A.js.map → p-Dk-8Ju0Q.js.map} +0 -0
  276. /package/dist/components/{p-Da9VpZX2.js.map → p-DtLQIMsa.js.map} +0 -0
  277. /package/dist/components/{p-Cw_npFwN.js.map → p-FPZbS19l.js.map} +0 -0
  278. /package/dist/components/{p-BMERUCe1.js.map → p-KBrC9IG4.js.map} +0 -0
  279. /package/dist/components/{p-CIYJhyKt.js.map → p-beCz9OnD.js.map} +0 -0
  280. /package/dist/components/{p-4DNyCYKM.js.map → p-mpk_Mm8_.js.map} +0 -0
  281. /package/dist/components/{p-DrjY8Mpy.js.map → p-n68x-ZKp.js.map} +0 -0
  282. /package/dist/components/{p-CNDTHTf6.js.map → p-omPm8_vx.js.map} +0 -0
  283. /package/dist/components/{p-lv4YQEsa.js.map → p-umAf0fBn.js.map} +0 -0
  284. /package/dist/components/{p-CBQzTgvI.js.map → p-y06IxxoN.js.map} +0 -0
  285. /package/dist/components/{p-CoE7yza3.js.map → p-yZRXPHDe.js.map} +0 -0
@@ -41,11 +41,15 @@
41
41
  "views, value, and dirty are controlled. Dirty state is presented only for a selected custom view; the default template never shows the trigger dot or save/discard actions even if dirty is supplied. Selection, create, save, discard, rename, and remove events communicate intent and never mutate application records.",
42
42
  "The component owns only transient interaction state: the contextual menu, naming dialog, draft name, duplicate-name validation, and focus restoration.",
43
43
  "The default view is a non-editable template. Application-owned custom views appear in a separate section with Rename and Remove actions. New view opens the naming dialog. When an editable selected view is dirty, that view row shows Save and negative Discard supporting actions while the footer remains dedicated to New view. The application decides which table state to capture, restore, and persist.",
44
- "The built-in picker and contextual option menu each retain their own surface-open treatment. Opening a view menu does not select that view."
44
+ "The built-in picker and contextual option menu each retain their own surface-open treatment. Opening a view menu does not select that view.",
45
+ "Borderless toolbar compositions omit resting control borders while retaining focus and interaction feedback.",
46
+ "Expanded toolbar triggers use an icon and regular-weight text without a trailing chevron.",
47
+ "Selected views and applied filters do not promote trigger icon or label colors. Trigger colors remain neutral across value changes."
45
48
  ],
46
49
  "responsiveBehavior": [
47
50
  "The control remains a single toolbar item and relies on Select, Menu, and Modal collision and viewport behavior. Its trigger hugs short view names and is capped at the compact menu-width token so long names truncate while the dirty dot stays visible. Its choice popup keeps that same token as the minimum width.",
48
- "Place it in the start region of ds-table-toolbar so the middle region can keep left-side and right-side controls on one spanning cluster."
51
+ "Place it in the start region of ds-table-toolbar so the middle region can keep left-side and right-side controls on one spanning cluster.",
52
+ "The ViewMenu prefix identifies saved views. Compact compositions and narrow table captions show only the icon while retaining the accessible name and tooltip."
49
53
  ],
50
54
  "frameworkCaveats": {
51
55
  "customElements": [
@@ -75,6 +79,15 @@
75
79
  },
76
80
  "api": {
77
81
  "props": {
82
+ "compact": {
83
+ "type": "boolean",
84
+ "resolvedType": "boolean",
85
+ "attribute": "compact",
86
+ "default": "false",
87
+ "required": false,
88
+ "mutable": false,
89
+ "description": "Use an icon-only trigger in an explicitly compact toolbar."
90
+ },
78
91
  "createLabel": {
79
92
  "type": "string",
80
93
  "resolvedType": "string",
@@ -111,6 +124,15 @@
111
124
  "mutable": false,
112
125
  "description": "Whether the current table state differs from the selected custom view. Ignored for the default view."
113
126
  },
127
+ "hasBorder": {
128
+ "type": "boolean",
129
+ "resolvedType": "boolean",
130
+ "attribute": "has-border",
131
+ "default": "true",
132
+ "required": false,
133
+ "mutable": false,
134
+ "description": "Show the trigger border."
135
+ },
114
136
  "label": {
115
137
  "type": "string",
116
138
  "resolvedType": "string",
@@ -143,8 +165,7 @@
143
165
  "resolvedType": "TableSavedView[]",
144
166
  "default": "[]",
145
167
  "required": false,
146
- "mutable": false,
147
- "description": "Application-owned custom saved views. Assign arrays through the JavaScript property."
168
+ "mutable": false
148
169
  }
149
170
  },
150
171
  "events": [
@@ -201,6 +222,15 @@
201
222
  "slots": []
202
223
  },
203
224
  "props": {
225
+ "compact": {
226
+ "type": "boolean",
227
+ "resolvedType": "boolean",
228
+ "attribute": "compact",
229
+ "default": "false",
230
+ "required": false,
231
+ "mutable": false,
232
+ "description": "Use an icon-only trigger in an explicitly compact toolbar."
233
+ },
204
234
  "createLabel": {
205
235
  "type": "string",
206
236
  "resolvedType": "string",
@@ -237,6 +267,15 @@
237
267
  "mutable": false,
238
268
  "description": "Whether the current table state differs from the selected custom view. Ignored for the default view."
239
269
  },
270
+ "hasBorder": {
271
+ "type": "boolean",
272
+ "resolvedType": "boolean",
273
+ "attribute": "has-border",
274
+ "default": "true",
275
+ "required": false,
276
+ "mutable": false,
277
+ "description": "Show the trigger border."
278
+ },
240
279
  "label": {
241
280
  "type": "string",
242
281
  "resolvedType": "string",
@@ -269,8 +308,7 @@
269
308
  "resolvedType": "TableSavedView[]",
270
309
  "default": "[]",
271
310
  "required": false,
272
- "mutable": false,
273
- "description": "Application-owned custom saved views. Assign arrays through the JavaScript property."
311
+ "mutable": false
274
312
  }
275
313
  },
276
314
  "events": [
@@ -381,7 +419,7 @@
381
419
  },
382
420
  {
383
421
  "path": "src/wc/components/TableSavedViews/TableSavedViews.tsx",
384
- "content": "import { Component, Element, Event, EventEmitter, h, Host, Prop, State } from '@stencil/core';\nimport type { MenuItemData } from '../Menu/menu-types';\nimport type {\n SelectOptionActionDetail,\n SelectOptionSubtextActionDetail,\n SelectValue,\n} from '../Select/Select';\nimport type {\n TableSavedView,\n TableSavedViewChangeDetail,\n TableSavedViewCreateDetail,\n TableSavedViewDiscardDetail,\n TableSavedViewRemoveDetail,\n TableSavedViewRenameDetail,\n TableSavedViewSaveDetail,\n} from './table-saved-views-types';\n\ntype SavedViewDialogMode = 'create' | 'rename';\n\nlet tableSavedViewsId = 0;\n\n@Component({\n tag: 'ds-table-saved-views',\n styleUrl: 'TableSavedViews.css',\n scoped: true,\n})\nexport class TableSavedViews {\n @Element() el!: HTMLElement;\n\n /** Application-owned custom saved views. Assign arrays through the JavaScript property. */\n @Prop() views: TableSavedView[] = [];\n /** ID of the controlled active view, including the default view ID. */\n @Prop() value: string = '__default__';\n /** Whether the current table state differs from the selected custom view. Ignored for the default view. */\n @Prop() dirty: boolean = false;\n /** ID used for the built-in default view. */\n @Prop() defaultViewId: string = '__default__';\n /** Label used for the built-in default view. */\n @Prop() defaultViewLabel: string = 'Default';\n /** Accessible name for the saved-views select. */\n @Prop() label: string = 'Saved views';\n /** Trigger label shown while the default view is selected. */\n @Prop() triggerLabel: string = 'Views';\n /** Footer action label that opens the create-view dialog. */\n @Prop() createLabel: string = 'New view';\n\n /** Emitted when a view selection is requested. */\n @Event() dsViewChange!: EventEmitter<TableSavedViewChangeDetail>;\n /** Emitted after a valid create-view name is submitted. */\n @Event() dsViewCreate!: EventEmitter<TableSavedViewCreateDetail>;\n /** Emitted after a valid renamed view name is submitted. */\n @Event() dsViewRename!: EventEmitter<TableSavedViewRenameDetail>;\n /** Emitted when a custom view removal is requested. */\n @Event() dsViewRemove!: EventEmitter<TableSavedViewRemoveDetail>;\n /** Emitted when changes to the active custom view should replace its stored payload. */\n @Event() dsViewSave!: EventEmitter<TableSavedViewSaveDetail>;\n /** Emitted when the active custom view's stored payload should be restored. */\n @Event() dsViewDiscard!: EventEmitter<TableSavedViewDiscardDetail>;\n\n @State() private actionViewId: string | null = null;\n @State() private actionAnchorId: string | undefined;\n @State() private actionMenuOpen = false;\n @State() private actionMenuInitialFocusVisible = false;\n @State() private dialogOpen = false;\n @State() private dialogMode: SavedViewDialogMode = 'create';\n @State() private dialogViewId: string | null = null;\n @State() private nameDraft = '';\n @State() private nameError = '';\n\n private readonly componentId = `ds-table-saved-views-${++tableSavedViewsId}`;\n private readonly selectId = `${this.componentId}-select`;\n private readonly actionMenuId = `${this.componentId}-actions`;\n private readonly fieldId = `${this.componentId}-name-field`;\n private readonly inputId = `${this.componentId}-name-input`;\n private readonly actionItems: MenuItemData[] = [\n { label: 'Rename', value: 'rename' },\n { label: 'Remove', value: 'remove', isDestructive: true },\n ];\n\n private get selectedCustomView(): TableSavedView | undefined {\n return this.views.find(view => view.id === this.value);\n }\n\n private get dialogView(): TableSavedView | undefined {\n return this.views.find(view => view.id === this.dialogViewId);\n }\n\n private get sections() {\n const custom = this.views.map(view => ({\n label: view.label,\n value: view.id,\n subtextActions:\n this.dirty && view.id === this.selectedCustomView?.id\n ? [\n { label: 'Save', value: 'save' },\n { label: 'Discard', value: 'discard', tone: 'negative' as const },\n ]\n : undefined,\n action: {\n label: `Options for ${view.label}`,\n controls: this.actionMenuId,\n expanded: this.actionViewId === view.id && this.actionMenuOpen,\n surfaceOpen: this.actionViewId === view.id,\n },\n }));\n const templates = {\n header: 'Templates',\n options: [{ label: this.defaultViewLabel, value: this.defaultViewId }],\n divider: custom.length > 0,\n };\n return custom.length > 0 ? [templates, { header: 'Custom', options: custom }] : [templates];\n }\n\n private handleViewChange(value: SelectValue) {\n if (typeof value === 'string') this.dsViewChange.emit({ viewId: value });\n }\n\n private handleSubtextAction(detail: SelectOptionSubtextActionDetail) {\n const view = this.selectedCustomView;\n if (!view || !this.dirty || detail.value !== view.id) return;\n if (detail.actionValue === 'save') this.dsViewSave.emit({ viewId: view.id });\n if (detail.actionValue === 'discard') this.dsViewDiscard.emit({ viewId: view.id });\n }\n\n private handleOptionAction(detail: SelectOptionActionDetail) {\n if (!this.views.some(view => view.id === detail.value)) return;\n this.actionMenuInitialFocusVisible = detail.originalEvent.detail === 0;\n if (this.actionViewId === detail.value && this.actionMenuOpen) {\n this.actionMenuOpen = false;\n return;\n }\n this.actionViewId = detail.value;\n this.actionAnchorId = detail.anchorId;\n this.actionMenuOpen = true;\n }\n\n private handleActionSelect(event: CustomEvent<MenuItemData>) {\n const viewId = this.actionViewId;\n this.actionMenuOpen = false;\n if (!viewId) return;\n if (event.detail.value === 'rename') this.openDialog('rename', viewId);\n if (event.detail.value === 'remove') this.dsViewRemove.emit({ viewId });\n }\n\n private openDialog(mode: SavedViewDialogMode, viewId: string | null = null) {\n const view = mode === 'rename' ? this.views.find(item => item.id === viewId) : undefined;\n if (mode === 'rename' && !view) return;\n this.dialogMode = mode;\n this.dialogViewId = view?.id ?? null;\n this.nameDraft = view?.label ?? '';\n this.nameError = '';\n this.dialogOpen = true;\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n const input = this.el.querySelector<HTMLElement & { setFocus?: () => Promise<void> }>(\n `#${this.inputId}`\n );\n void input?.setFocus?.();\n });\n });\n }\n\n private validateName(): string {\n const name = this.nameDraft.trim();\n if (!name) return 'View name is required.';\n const duplicate = this.views.some(\n view =>\n view.id !== this.dialogViewId &&\n view.label.trim().toLocaleLowerCase() === name.toLocaleLowerCase()\n );\n if (\n duplicate ||\n this.defaultViewLabel.trim().toLocaleLowerCase() === name.toLocaleLowerCase()\n ) {\n return 'A view with this name already exists.';\n }\n return '';\n }\n\n private submitDialog() {\n const error = this.validateName();\n if (error) {\n this.nameError = error;\n return;\n }\n const name = this.nameDraft.trim();\n if (this.dialogMode === 'rename' && this.dialogView) {\n this.dsViewRename.emit({ viewId: this.dialogView.id, name });\n } else {\n this.dsViewCreate.emit({ name });\n }\n this.dialogOpen = false;\n }\n\n private finishDialogClose() {\n if (this.dialogOpen) return;\n this.dialogViewId = null;\n this.nameDraft = '';\n this.nameError = '';\n const select = this.el.querySelector<HTMLElement & { setFocus?: () => Promise<void> }>(\n `#${this.selectId}`\n );\n void select?.setFocus?.();\n }\n\n render() {\n return (\n <Host>\n <ds-select\n id={this.selectId}\n size=\"md\"\n width=\"fill\"\n placeholder={this.label}\n aria-label={this.label}\n sections={this.sections}\n value={this.value}\n triggerLabel={this.selectedCustomView ? undefined : this.triggerLabel}\n triggerLabelPlaceholder={!this.selectedCustomView}\n dot={this.dirty && Boolean(this.selectedCustomView)}\n footerActionLabel={this.createLabel}\n activeFill={false}\n allowClear={false}\n onDsChange={(event: CustomEvent<SelectValue>) => this.handleViewChange(event.detail)}\n onDsFooterAction={() => this.openDialog('create')}\n onDsOptionSubtextAction={(event: CustomEvent<SelectOptionSubtextActionDetail>) =>\n this.handleSubtextAction(event.detail)\n }\n onDsOptionAction={(event: CustomEvent<SelectOptionActionDetail>) =>\n this.handleOptionAction(event.detail)\n }\n />\n\n <ds-menu\n id={this.actionMenuId}\n anchorId={this.actionAnchorId}\n menuLabel=\"Saved view options\"\n side=\"bottom\"\n align=\"end\"\n menuWidth=\"max-content\"\n minWidth=\"0\"\n items={this.actionItems}\n open={this.actionMenuOpen}\n initialFocusVisible={this.actionMenuInitialFocusVisible}\n onDsClose={() => (this.actionMenuOpen = false)}\n onDsAfterClose={() => {\n if (this.actionMenuOpen) return;\n this.actionViewId = null;\n this.actionAnchorId = undefined;\n }}\n onDsSelect={(event: CustomEvent<MenuItemData>) => this.handleActionSelect(event)}\n />\n\n <ds-modal\n open={this.dialogOpen}\n heading={this.dialogMode === 'rename' ? 'Rename view' : 'Save as new view'}\n modalWidth=\"sm\"\n onDsClose={() => (this.dialogOpen = false)}\n onDsAfterClose={() => this.finishDialogClose()}\n >\n <ds-field\n label=\"Name\"\n fieldId={this.fieldId}\n error={this.nameError.length > 0}\n errorMessage={this.nameError}\n >\n <ds-input\n id={this.inputId}\n size=\"md\"\n placeholder=\"Enter a unique name\"\n value={this.nameDraft}\n autoFocus={true}\n onDsChange={(event: CustomEvent<string>) => {\n this.nameDraft = event.detail;\n if (this.nameError) this.nameError = '';\n }}\n onKeyDown={(event: KeyboardEvent) => {\n if (event.key === 'Enter') this.submitDialog();\n }}\n />\n </ds-field>\n\n <div slot=\"footer\" class=\"table-saved-views__dialog-actions\">\n <ds-button-filled\n label={this.dialogMode === 'rename' ? 'Rename' : 'Save'}\n size=\"md\"\n onDsClick={() => this.submitDialog()}\n />\n <ds-button-unfilled\n label=\"Cancel\"\n size=\"md\"\n onDsClick={() => (this.dialogOpen = false)}\n />\n </div>\n </ds-modal>\n </Host>\n );\n }\n}\n",
422
+ "content": "import { Component, Element, Event, EventEmitter, h, Host, Prop, State } from '@stencil/core';\nimport type { MenuItemData } from '../Menu/menu-types';\nimport type {\n SelectOptionActionDetail,\n SelectOptionSubtextActionDetail,\n SelectValue,\n} from '../Select/Select';\nimport type {\n TableSavedView,\n TableSavedViewChangeDetail,\n TableSavedViewCreateDetail,\n TableSavedViewDiscardDetail,\n TableSavedViewRemoveDetail,\n TableSavedViewRenameDetail,\n TableSavedViewSaveDetail,\n} from './table-saved-views-types';\n\ntype SavedViewDialogMode = 'create' | 'rename';\n\nlet tableSavedViewsId = 0;\n\n@Component({\n tag: 'ds-table-saved-views',\n styleUrl: 'TableSavedViews.css',\n scoped: true,\n})\nexport class TableSavedViews {\n @Element() el!: HTMLElement;\n\n /** Application-owned custom saved views. Assign arrays through the JavaScript property. */\n /** Show the trigger border. */\n @Prop() hasBorder: boolean = true;\n @Prop() views: TableSavedView[] = [];\n /** ID of the controlled active view, including the default view ID. */\n @Prop() value: string = '__default__';\n /** Whether the current table state differs from the selected custom view. Ignored for the default view. */\n @Prop() dirty: boolean = false;\n /** ID used for the built-in default view. */\n @Prop() defaultViewId: string = '__default__';\n /** Label used for the built-in default view. */\n @Prop() defaultViewLabel: string = 'Default';\n /** Accessible name for the saved-views select. */\n @Prop() label: string = 'Saved views';\n /** Trigger label shown while the default view is selected. */\n @Prop() triggerLabel: string = 'Views';\n /** Use an icon-only trigger in an explicitly compact toolbar. */\n @Prop() compact: boolean = false;\n /** Footer action label that opens the create-view dialog. */\n @Prop() createLabel: string = 'New view';\n\n /** Emitted when a view selection is requested. */\n @Event() dsViewChange!: EventEmitter<TableSavedViewChangeDetail>;\n /** Emitted after a valid create-view name is submitted. */\n @Event() dsViewCreate!: EventEmitter<TableSavedViewCreateDetail>;\n /** Emitted after a valid renamed view name is submitted. */\n @Event() dsViewRename!: EventEmitter<TableSavedViewRenameDetail>;\n /** Emitted when a custom view removal is requested. */\n @Event() dsViewRemove!: EventEmitter<TableSavedViewRemoveDetail>;\n /** Emitted when changes to the active custom view should replace its stored payload. */\n @Event() dsViewSave!: EventEmitter<TableSavedViewSaveDetail>;\n /** Emitted when the active custom view's stored payload should be restored. */\n @Event() dsViewDiscard!: EventEmitter<TableSavedViewDiscardDetail>;\n\n @State() private actionViewId: string | null = null;\n @State() private actionAnchorId: string | undefined;\n @State() private actionMenuOpen = false;\n @State() private actionMenuInitialFocusVisible = false;\n @State() private dialogOpen = false;\n @State() private dialogMode: SavedViewDialogMode = 'create';\n @State() private dialogViewId: string | null = null;\n @State() private nameDraft = '';\n @State() private nameError = '';\n\n private readonly componentId = `ds-table-saved-views-${++tableSavedViewsId}`;\n private readonly selectId = `${this.componentId}-select`;\n private readonly actionMenuId = `${this.componentId}-actions`;\n private readonly fieldId = `${this.componentId}-name-field`;\n private readonly inputId = `${this.componentId}-name-input`;\n private readonly actionItems: MenuItemData[] = [\n { label: 'Rename', value: 'rename' },\n { label: 'Remove', value: 'remove', isDestructive: true },\n ];\n\n private get selectedCustomView(): TableSavedView | undefined {\n return this.views.find(view => view.id === this.value);\n }\n\n private get dialogView(): TableSavedView | undefined {\n return this.views.find(view => view.id === this.dialogViewId);\n }\n\n private get sections() {\n const custom = this.views.map(view => ({\n label: view.label,\n value: view.id,\n subtextActions:\n this.dirty && view.id === this.selectedCustomView?.id\n ? [\n { label: 'Save', value: 'save' },\n { label: 'Discard', value: 'discard', tone: 'negative' as const },\n ]\n : undefined,\n action: {\n label: `Options for ${view.label}`,\n controls: this.actionMenuId,\n expanded: this.actionViewId === view.id && this.actionMenuOpen,\n surfaceOpen: this.actionViewId === view.id,\n },\n }));\n const templates = {\n header: 'Templates',\n options: [{ label: this.defaultViewLabel, value: this.defaultViewId }],\n divider: custom.length > 0,\n };\n return custom.length > 0 ? [templates, { header: 'Custom', options: custom }] : [templates];\n }\n\n private handleViewChange(value: SelectValue) {\n if (typeof value === 'string') this.dsViewChange.emit({ viewId: value });\n }\n\n private handleSubtextAction(detail: SelectOptionSubtextActionDetail) {\n const view = this.selectedCustomView;\n if (!view || !this.dirty || detail.value !== view.id) return;\n if (detail.actionValue === 'save') this.dsViewSave.emit({ viewId: view.id });\n if (detail.actionValue === 'discard') this.dsViewDiscard.emit({ viewId: view.id });\n }\n\n private handleOptionAction(detail: SelectOptionActionDetail) {\n if (!this.views.some(view => view.id === detail.value)) return;\n this.actionMenuInitialFocusVisible = detail.originalEvent.detail === 0;\n if (this.actionViewId === detail.value && this.actionMenuOpen) {\n this.actionMenuOpen = false;\n return;\n }\n this.actionViewId = detail.value;\n this.actionAnchorId = detail.anchorId;\n this.actionMenuOpen = true;\n }\n\n private handleActionSelect(event: CustomEvent<MenuItemData>) {\n const viewId = this.actionViewId;\n this.actionMenuOpen = false;\n if (!viewId) return;\n if (event.detail.value === 'rename') this.openDialog('rename', viewId);\n if (event.detail.value === 'remove') this.dsViewRemove.emit({ viewId });\n }\n\n private openDialog(mode: SavedViewDialogMode, viewId: string | null = null) {\n const view = mode === 'rename' ? this.views.find(item => item.id === viewId) : undefined;\n if (mode === 'rename' && !view) return;\n this.dialogMode = mode;\n this.dialogViewId = view?.id ?? null;\n this.nameDraft = view?.label ?? '';\n this.nameError = '';\n this.dialogOpen = true;\n requestAnimationFrame(() => {\n requestAnimationFrame(() => {\n const input = this.el.querySelector<HTMLElement & { setFocus?: () => Promise<void> }>(\n `#${this.inputId}`\n );\n void input?.setFocus?.();\n });\n });\n }\n\n private validateName(): string {\n const name = this.nameDraft.trim();\n if (!name) return 'View name is required.';\n const duplicate = this.views.some(\n view =>\n view.id !== this.dialogViewId &&\n view.label.trim().toLocaleLowerCase() === name.toLocaleLowerCase()\n );\n if (\n duplicate ||\n this.defaultViewLabel.trim().toLocaleLowerCase() === name.toLocaleLowerCase()\n ) {\n return 'A view with this name already exists.';\n }\n return '';\n }\n\n private submitDialog() {\n const error = this.validateName();\n if (error) {\n this.nameError = error;\n return;\n }\n const name = this.nameDraft.trim();\n if (this.dialogMode === 'rename' && this.dialogView) {\n this.dsViewRename.emit({ viewId: this.dialogView.id, name });\n } else {\n this.dsViewCreate.emit({ name });\n }\n this.dialogOpen = false;\n }\n\n private finishDialogClose() {\n if (this.dialogOpen) return;\n this.dialogViewId = null;\n this.nameDraft = '';\n this.nameError = '';\n const select = this.el.querySelector<HTMLElement & { setFocus?: () => Promise<void> }>(\n `#${this.selectId}`\n );\n void select?.setFocus?.();\n }\n\n render() {\n return (\n <Host>\n <ds-select\n neutralTrigger\n hasBorder={this.hasBorder}\n id={this.selectId}\n size=\"md\"\n icon=\"ViewMenu\"\n indicator=\"none\"\n collapseLabel\n iconOnly={this.compact}\n width=\"fill\"\n placeholder={this.label}\n aria-label={this.label}\n sections={this.sections}\n value={this.value}\n triggerLabel={this.selectedCustomView ? undefined : this.triggerLabel}\n triggerLabelPlaceholder={!this.selectedCustomView}\n dot={this.dirty && Boolean(this.selectedCustomView)}\n footerActionLabel={this.createLabel}\n activeFill={false}\n allowClear={false}\n onDsChange={(event: CustomEvent<SelectValue>) => this.handleViewChange(event.detail)}\n onDsFooterAction={() => this.openDialog('create')}\n onDsOptionSubtextAction={(event: CustomEvent<SelectOptionSubtextActionDetail>) =>\n this.handleSubtextAction(event.detail)\n }\n onDsOptionAction={(event: CustomEvent<SelectOptionActionDetail>) =>\n this.handleOptionAction(event.detail)\n }\n />\n\n <ds-menu\n id={this.actionMenuId}\n anchorId={this.actionAnchorId}\n menuLabel=\"Saved view options\"\n side=\"bottom\"\n align=\"end\"\n menuWidth=\"max-content\"\n minWidth=\"0\"\n items={this.actionItems}\n open={this.actionMenuOpen}\n initialFocusVisible={this.actionMenuInitialFocusVisible}\n onDsClose={() => (this.actionMenuOpen = false)}\n onDsAfterClose={() => {\n if (this.actionMenuOpen) return;\n this.actionViewId = null;\n this.actionAnchorId = undefined;\n }}\n onDsSelect={(event: CustomEvent<MenuItemData>) => this.handleActionSelect(event)}\n />\n\n <ds-modal\n open={this.dialogOpen}\n heading={this.dialogMode === 'rename' ? 'Rename view' : 'Save as new view'}\n modalWidth=\"sm\"\n onDsClose={() => (this.dialogOpen = false)}\n onDsAfterClose={() => this.finishDialogClose()}\n >\n <ds-field\n label=\"Name\"\n fieldId={this.fieldId}\n error={this.nameError.length > 0}\n errorMessage={this.nameError}\n >\n <ds-input\n id={this.inputId}\n size=\"md\"\n placeholder=\"Enter a unique name\"\n value={this.nameDraft}\n autoFocus={true}\n onDsChange={(event: CustomEvent<string>) => {\n this.nameDraft = event.detail;\n if (this.nameError) this.nameError = '';\n }}\n onKeyDown={(event: KeyboardEvent) => {\n if (event.key === 'Enter') this.submitDialog();\n }}\n />\n </ds-field>\n\n <div slot=\"footer\" class=\"table-saved-views__dialog-actions\">\n <ds-button-filled\n label={this.dialogMode === 'rename' ? 'Rename' : 'Save'}\n size=\"md\"\n onDsClick={() => this.submitDialog()}\n />\n <ds-button-unfilled\n label=\"Cancel\"\n size=\"md\"\n onDsClick={() => (this.dialogOpen = false)}\n />\n </div>\n </ds-modal>\n </Host>\n );\n }\n}\n",
385
423
  "type": "registry:ui"
386
424
  },
387
425
  {
@@ -35,7 +35,7 @@
35
35
  "Typing slash opens the field listbox without inserting slash into the table query. Text typed while it is open filters data points by complete label, Up and Down change the active match, Enter selects it, Escape closes it, and Left and Right retain native text-cursor behavior because focus never leaves the input. Backspace edits the field-picker query first and closes the listbox when that query is empty.",
36
36
  "The query disables browser autocomplete history, automatic capitalization, autocorrection, and spellchecking so browser-owned suggestions never obscure the product-owned field menu or table results.",
37
37
  "Selected field Chips have individually labeled remove actions and are also described by the search input. Polite announcements report additions, removals, and clearing without duplicating the visible query.",
38
- "The slash suffix is a labeled double-inset md button excluded from sequential keyboard navigation; the search input exposes the same action through aria-keyshortcuts and its description.",
38
+ "The search input exposes the slash command through aria-keyshortcuts and its description, without a visible suffix button.",
39
39
  "The clear action removes both free text and every selected field. It restores focus to the search input; each Chip's own remove action remains scoped to that field."
40
40
  ],
41
41
  "states": [
@@ -45,12 +45,13 @@
45
45
  "Backspace edits a non-empty field-picker query, then closes the open slash listbox when that query is empty. When the table query is empty and the listbox is already closed, the next Backspace removes the last selected field. Ordinary Backspace edits table query text when the listbox is closed.",
46
46
  "Duplicate, blank, and unknown field identities are ignored for presentation. Selected fields preserve the consumer's order.",
47
47
  "Inactive search leaves the tab order, closes the field listbox, and does not emit query or field changes.",
48
- "The field-picker action holds the shared surface-open wash while its listbox is visible; chosen search fields remain independent."
48
+ "The field-picker action holds the shared surface-open wash while its listbox is visible; chosen search fields remain independent.",
49
+ "Borderless toolbar compositions omit resting control borders while retaining focus and interaction feedback."
49
50
  ],
50
51
  "responsiveBehavior": [
51
52
  "The control fills the available inline size supplied by the table toolbar search slot. Selected Chips and the editable query share one horizontal editor lane so the md control does not grow vertically.",
52
53
  "The field popup enters the top layer, aligns its first choice cell with the search control, flips or clamps to the viewport or nearest overlay boundary, and scrolls internally when its catalog is taller than available space.",
53
- "Long selected field labels truncate inside their Chips while the input remains available at the inline end of the editor lane. Both suffix actions resolve to the same 24px height as selected Chips. Clear follows the standard md Input recipe—a non-inset sm icon button—while slash is a double-inset md label button. Slash remains rightmost; when query text exposes Clear, a vertical divider separates the two actions."
54
+ "Clear appears when query text is present and uses the standard sm icon button treatment."
54
55
  ],
55
56
  "frameworkCaveats": {
56
57
  "customElements": [
@@ -113,6 +114,15 @@
113
114
  "required": false,
114
115
  "mutable": false
115
116
  },
117
+ "hasBorder": {
118
+ "type": "boolean",
119
+ "resolvedType": "boolean",
120
+ "attribute": "has-border",
121
+ "default": "true",
122
+ "required": false,
123
+ "mutable": false,
124
+ "description": "Show the resting field border; keyboard focus remains visible."
125
+ },
116
126
  "isInactive": {
117
127
  "type": "boolean",
118
128
  "resolvedType": "boolean",
@@ -211,6 +221,15 @@
211
221
  "required": false,
212
222
  "mutable": false
213
223
  },
224
+ "hasBorder": {
225
+ "type": "boolean",
226
+ "resolvedType": "boolean",
227
+ "attribute": "has-border",
228
+ "default": "true",
229
+ "required": false,
230
+ "mutable": false,
231
+ "description": "Show the resting field border; keyboard focus remains visible."
232
+ },
214
233
  "isInactive": {
215
234
  "type": "boolean",
216
235
  "resolvedType": "boolean",
@@ -318,7 +337,6 @@
318
337
  "registryDependencies": [
319
338
  "button-unfilled",
320
339
  "chip",
321
- "divider",
322
340
  "empty-state",
323
341
  "icon",
324
342
  "text"
@@ -326,12 +344,12 @@
326
344
  "files": [
327
345
  {
328
346
  "path": "src/wc/components/TableSearch/TableSearch.css",
329
- "content": "@import '../../utils/radius.css';\n@import '../../utils/choice-list.css';\n@import '../../utils/control-density-inset.css';\n@import '../../utils/typography.css';\n@import '../../utils/visually-hidden.css';\n\n:host(.table-search-host) {\n display: inline-flex;\n box-sizing: border-box;\n inline-size: 100%;\n min-inline-size: 0;\n --ds-table-search-foreground: var(--color-foreground-primary);\n --ds-table-search-adornment: var(--color-foreground-secondary);\n --ds-table-search-placeholder: var(--color-foreground-secondary);\n --ds-table-search-placeholder-active: var(--color-foreground-quaternary);\n --ds-table-search-border: var(--color-border-secondary);\n}\n\n.table-search__control {\n inline-size: 100%;\n min-inline-size: 0;\n padding-inline-end: var(--dimension-space-050);\n overflow: hidden;\n border: 0;\n background: transparent;\n color: var(--ds-table-search-foreground);\n --ds-interaction-border-width: var(--dimension-stroke-width-012);\n --ds-interaction-border-color: var(--ds-table-search-border);\n}\n\n.table-search__control:focus-within {\n --ds-interaction-border-width: var(--dimension-stroke-width-018);\n --ds-interaction-border-color: var(--color-border-bold-brand);\n}\n\n:host(.ds-control-inactive) .table-search__control::after {\n background: transparent;\n}\n\n.table-search__icon,\n.table-search__actions {\n position: relative;\n z-index: 2;\n flex: none;\n}\n\n.table-search__icon {\n color: var(--ds-table-search-adornment);\n}\n\n.table-search__editor {\n position: relative;\n z-index: 2;\n display: flex;\n flex: 1 1 auto;\n align-items: center;\n min-inline-size: 0;\n block-size: 100%;\n gap: var(--dimension-space-050);\n overflow-x: auto;\n overflow-y: hidden;\n scrollbar-width: none;\n}\n\n.table-search__editor::-webkit-scrollbar {\n display: none;\n}\n\n.table-search__chip {\n flex: none;\n max-inline-size: var(--dimension-panel-width-2xs);\n}\n\n.table-search__input {\n appearance: none;\n box-sizing: border-box;\n flex: 1 0 var(--dimension-size-600);\n min-inline-size: var(--dimension-size-600);\n block-size: 100%;\n margin: 0;\n padding: 0;\n border: 0;\n outline: 0;\n background: transparent;\n color: var(--ds-table-search-foreground);\n text-overflow: ellipsis;\n}\n\n.table-search__input::-webkit-search-cancel-button,\n.table-search__input::-webkit-search-decoration {\n display: none;\n}\n\n.table-search__input::placeholder {\n color: var(--ds-table-search-placeholder);\n}\n\n.table-search__input:focus::placeholder {\n color: var(--ds-table-search-placeholder-active);\n}\n\n.table-search__actions {\n display: flex;\n align-items: center;\n gap: var(--dimension-space-050);\n}\n\n.table-search__clear,\n.table-search__slash,\n.table-search__action-divider {\n flex: none;\n}\n\n.table-search__slash {\n color: var(--ds-table-search-adornment);\n}\n\n.table-search__popup {\n --ds-choice-popup-min-inline-size: var(--dimension-menu-width-xs);\n}\n\n.table-search__list {\n padding: var(--dimension-space-050);\n}\n\n.table-search__option {\n flex: none;\n inline-size: 100%;\n}\n\n@media (forced-colors: active) {\n .table-search__control:focus-within::after {\n outline-color: var(--ds-forced-color-selected);\n }\n}\n",
347
+ "content": "@import '../../utils/radius.css';\n@import '../../utils/choice-list.css';\n@import '../../utils/control-density-inset.css';\n@import '../../utils/typography.css';\n@import '../../utils/visually-hidden.css';\n\n:host(.table-search-host) {\n display: inline-flex;\n box-sizing: border-box;\n inline-size: 100%;\n min-inline-size: 0;\n --ds-table-search-foreground: var(--color-foreground-primary);\n --ds-table-search-adornment: var(--color-foreground-secondary);\n --ds-table-search-placeholder: var(--color-foreground-secondary);\n --ds-table-search-placeholder-active: var(--color-foreground-quaternary);\n --ds-table-search-border: var(--color-border-secondary);\n}\n\n.table-search__control {\n inline-size: 100%;\n min-inline-size: 0;\n padding-inline-end: var(--dimension-space-050);\n overflow: hidden;\n border: 0;\n background: transparent;\n color: var(--ds-table-search-foreground);\n --ds-interaction-border-width: var(--dimension-stroke-width-012);\n --ds-interaction-border-color: var(--ds-table-search-border);\n}\n\n.table-search__control:focus-within {\n --ds-interaction-border-width: var(--dimension-stroke-width-018);\n --ds-interaction-border-color: var(--color-border-bold-brand);\n}\n\n:host(.ds-control-inactive) .table-search__control::after {\n background: transparent;\n}\n\n.table-search__icon,\n.table-search__clear {\n position: relative;\n z-index: 2;\n flex: none;\n}\n\n.table-search__icon {\n color: var(--ds-table-search-adornment);\n}\n\n.table-search__editor {\n position: relative;\n z-index: 2;\n display: flex;\n flex: 1 1 auto;\n align-items: center;\n min-inline-size: 0;\n block-size: 100%;\n gap: var(--dimension-space-050);\n overflow-x: auto;\n overflow-y: hidden;\n scrollbar-width: none;\n}\n\n.table-search__editor::-webkit-scrollbar {\n display: none;\n}\n\n.table-search__chip {\n flex: none;\n max-inline-size: var(--dimension-panel-width-2xs);\n}\n\n.table-search__input {\n appearance: none;\n box-sizing: border-box;\n flex: 1 0 var(--dimension-size-600);\n min-inline-size: var(--dimension-size-600);\n block-size: 100%;\n margin: 0;\n padding: 0;\n border: 0;\n outline: 0;\n background: transparent;\n color: var(--ds-table-search-foreground);\n text-overflow: ellipsis;\n}\n\n.table-search__input::-webkit-search-cancel-button,\n.table-search__input::-webkit-search-decoration {\n display: none;\n}\n\n.table-search__input::placeholder {\n color: var(--ds-table-search-placeholder);\n}\n\n.table-search__input:focus::placeholder {\n color: var(--ds-table-search-placeholder-active);\n}\n\n.table-search__popup {\n --ds-choice-popup-min-inline-size: var(--dimension-menu-width-xs);\n}\n\n.table-search__list {\n padding: var(--dimension-space-050);\n}\n\n.table-search__option {\n flex: none;\n inline-size: 100%;\n}\n\n@media (forced-colors: active) {\n .table-search__control:focus-within::after {\n outline-color: var(--ds-forced-color-selected);\n }\n}\n\n:host(.table-search-host--borderless) .table-search__control:not(:focus-within) {\n --ds-interaction-border-width: 0px;\n}\n",
330
348
  "type": "registry:ui"
331
349
  },
332
350
  {
333
351
  "path": "src/wc/components/TableSearch/TableSearch.tsx",
334
- "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport {\n choicePopupMinWidth,\n resolveChoicePopupAlignOffset,\n resolveCssLengthPx,\n TOKEN_DEFAULTS,\n} from '../../utils';\nimport { AnchoredPositionController } from '../../utils/anchored-position-controller';\nimport { AnchoredOverlayInteractionController } from '../../utils/anchored-overlay-interaction-controller';\nimport { resolveAnchoredOverlayBoundaryRect } from '../../utils/anchored-overlay-boundary';\nimport {\n availableTableSearchFields,\n filterTableSearchFields,\n nextTableSearchActiveIndex,\n selectedTableSearchFields,\n tableSearchFields,\n} from './table-search-model';\nimport type { TableColumn } from '../Table/table-types';\nimport type { TableSearchField, TableSearchFieldsChangeDetail } from './table-search-types';\n\nlet tableSearchSequence = 0;\n\n@Component({\n tag: 'ds-table-search',\n styleUrl: 'TableSearch.css',\n scoped: true,\n})\nexport class TableSearch {\n @Element() private el!: HTMLElement;\n\n /** Controlled free-text query. */\n @Prop() value: string = '';\n /** Table column catalog used to derive searchable data points and their complete labels. */\n @Prop() columns: TableColumn[] = [];\n /** Controlled ordered field scopes rendered as Tags. */\n @Prop() selectedFieldIds: string[] = [];\n @Prop() placeholder: string = 'Search';\n @Prop({ attribute: 'aria-label' }) ariaLabel: string = 'Search table';\n @Prop() clearLabel: string = 'Clear search';\n @Prop() fieldMenuLabel: string = 'Choose search fields';\n @Prop() isInactive: boolean = false;\n\n @Event({ bubbles: false }) dsChange!: EventEmitter<string>;\n @Event({ bubbles: false }) dsFieldsChange!: EventEmitter<TableSearchFieldsChangeDetail>;\n @Event({ bubbles: false }) dsClear!: EventEmitter<void>;\n\n @State() private menuOpen = false;\n @State() private menuQuery = '';\n @State() private activeIndex = 0;\n @State() private focusRingVisible = false;\n @State() private position = { x: 0, y: 0 };\n @State() private positionReady = false;\n @State() private availableHeight: number | null = null;\n @State() private announcement = '';\n\n private readonly componentId = `ds-table-search-${++tableSearchSequence}`;\n private readonly inputId = `${this.componentId}-input`;\n private readonly listboxId = `${this.componentId}-listbox`;\n private readonly descriptionId = `${this.componentId}-description`;\n private inputEl: HTMLInputElement | null = null;\n private controlEl: HTMLElement | null = null;\n private popupEl: HTMLElement | null = null;\n\n private readonly positionController = new AnchoredPositionController({\n getAnchor: () => this.controlEl,\n getPopup: () => this.popupEl,\n getOwnerDocument: () => this.el.ownerDocument,\n measure: (anchor, popup) => {\n if (!this.menuOpen || !popup.isConnected) return null;\n const inset = resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n popup.style.minWidth = `max(var(--ds-choice-popup-min-inline-size, ${TOKEN_DEFAULTS.menuWidthXs}), ${choicePopupMinWidth(anchor.offsetWidth, inset)}px)`;\n const anchorRect = anchor.getBoundingClientRect();\n return {\n anchorRect,\n popupWidth: popup.offsetWidth,\n popupHeight: popup.offsetHeight,\n side: 'bottom',\n align: 'start',\n sideOffsetPx: inset,\n alignOffsetPx: resolveChoicePopupAlignOffset({\n align: 'start',\n alignOffsetPx: 0,\n sectionInsetPx: inset,\n }),\n viewportPadPx: inset,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n collisionRect: resolveAnchoredOverlayBoundaryRect(anchor),\n };\n },\n apply: ({ x, y, availableHeight }) => {\n this.position = { x, y };\n this.availableHeight = availableHeight;\n },\n onReady: () => {\n this.positionReady = true;\n },\n topLayer: true,\n liveUpdate: 'frame',\n observeResize: true,\n observeAnchorMotion: true,\n });\n\n private readonly interactionController = new AnchoredOverlayInteractionController({\n getAnchor: () => this.controlEl,\n getPopup: () => this.popupEl,\n getOwnerDocument: () => this.el.ownerDocument,\n onOutsideActivation: () => this.closeMenu(),\n });\n\n disconnectedCallback(): void {\n this.closeMenu();\n }\n\n @Watch('columns')\n @Watch('selectedFieldIds')\n syncAvailableFields(): void {\n const count = this.availableFields.length;\n if (count === 0) {\n this.closeMenu();\n return;\n }\n if (this.activeIndex >= count) this.activeIndex = count - 1;\n }\n\n @Watch('isInactive')\n syncInactive(isInactive: boolean): void {\n if (isInactive) this.closeMenu();\n }\n\n @Method()\n async setFocus(): Promise<void> {\n this.inputEl?.focus();\n }\n\n private get selectedFields(): TableSearchField[] {\n return selectedTableSearchFields(this.searchFields, this.selectedFieldIds);\n }\n\n private get availableFields(): TableSearchField[] {\n return availableTableSearchFields(this.searchFields, this.selectedFieldIds);\n }\n\n private get visibleFields(): TableSearchField[] {\n return filterTableSearchFields(this.availableFields, this.menuQuery);\n }\n\n private get searchFields(): TableSearchField[] {\n return tableSearchFields(this.columns);\n }\n\n private get activeOptionId(): string | undefined {\n return this.menuOpen && this.activeIndex >= 0\n ? `${this.componentId}-option-${this.activeIndex}`\n : undefined;\n }\n\n private openMenu(fromKeyboard: boolean): void {\n if (this.isInactive || this.menuOpen || this.availableFields.length === 0) return;\n this.menuQuery = '';\n // The native value can be ahead of the controlled prop when slash follows input\n // in the same frame, so clear it before the queued render can skip an equal value.\n if (this.inputEl) this.inputEl.value = '';\n this.activeIndex = 0;\n this.focusRingVisible = fromKeyboard;\n this.positionReady = false;\n this.menuOpen = true;\n requestAnimationFrame(() => {\n if (!this.menuOpen) return;\n this.interactionController.connect();\n this.positionController.observe();\n this.positionController.schedule();\n });\n }\n\n private closeMenu(): void {\n if (!this.menuOpen && !this.popupEl) return;\n if (this.popupEl?.matches(':popover-open')) this.popupEl.hidePopover();\n this.positionController.unobserve();\n this.interactionController.disconnect();\n this.popupEl = null;\n this.positionReady = false;\n this.availableHeight = null;\n this.menuOpen = false;\n this.menuQuery = '';\n }\n\n private handleInput(event: Event): void {\n const nextValue = (event.target as HTMLInputElement).value;\n if (this.menuOpen) {\n this.menuQuery = nextValue;\n this.activeIndex = filterTableSearchFields(this.availableFields, nextValue).length ? 0 : -1;\n this.focusRingVisible = false;\n this.positionController.schedule();\n return;\n }\n\n this.dsChange.emit(nextValue);\n }\n\n private selectField(field: TableSearchField): void {\n this.dsFieldsChange.emit({ selectedFieldIds: [...this.selectedFieldIds, field.id] });\n this.announcement = `${field.label} search field added.`;\n this.closeMenu();\n requestAnimationFrame(() => this.inputEl?.focus({ preventScroll: true }));\n }\n\n private removeLastField(): void {\n const selected = this.selectedFields;\n const removed = selected[selected.length - 1];\n if (!removed) return;\n this.dsFieldsChange.emit({\n selectedFieldIds: this.selectedFieldIds.filter(fieldId => fieldId !== removed.id),\n });\n this.announcement = `${removed.label} search field removed.`;\n }\n\n private removeField(field: TableSearchField): void {\n this.dsFieldsChange.emit({\n selectedFieldIds: this.selectedFieldIds.filter(fieldId => fieldId !== field.id),\n });\n this.announcement = `${field.label} search field removed.`;\n requestAnimationFrame(() => this.inputEl?.focus({ preventScroll: true }));\n }\n\n private clearSearch(): void {\n if (this.value) this.dsChange.emit('');\n if (this.selectedFieldIds.length) this.dsFieldsChange.emit({ selectedFieldIds: [] });\n this.dsClear.emit();\n this.announcement = 'Search cleared.';\n this.closeMenu();\n requestAnimationFrame(() => this.inputEl?.focus({ preventScroll: true }));\n }\n\n private handleKeyDown(event: KeyboardEvent): void {\n if (event.key === '/' && !this.menuOpen && !event.altKey && !event.ctrlKey && !event.metaKey) {\n event.preventDefault();\n this.openMenu(true);\n return;\n }\n\n if (event.key === 'Backspace' && this.menuOpen && this.menuQuery.length === 0) {\n event.preventDefault();\n this.closeMenu();\n return;\n }\n\n if (event.key === 'Backspace' && this.value.length === 0) {\n if (this.selectedFields.length) {\n event.preventDefault();\n this.removeLastField();\n }\n return;\n }\n\n if (!this.menuOpen) return;\n\n if (event.key === 'Escape') {\n event.preventDefault();\n this.closeMenu();\n return;\n }\n\n if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n event.preventDefault();\n this.focusRingVisible = true;\n this.activeIndex = nextTableSearchActiveIndex(\n this.activeIndex,\n this.visibleFields.length,\n event.key === 'ArrowDown' ? 1 : -1\n );\n this.el\n .querySelector<HTMLElement>(`#${CSS.escape(this.activeOptionId ?? '')}`)\n ?.scrollIntoView({ block: 'nearest' });\n return;\n }\n\n if (event.key === 'Enter') {\n const field = this.visibleFields[this.activeIndex];\n if (!field) return;\n event.preventDefault();\n this.selectField(field);\n return;\n }\n\n if (event.key === 'Tab') this.closeMenu();\n }\n\n render() {\n const selected = this.selectedFields;\n const available = this.availableFields;\n const visible = this.visibleFields;\n const inputValue = this.menuOpen ? this.menuQuery : this.value;\n const hasValue = this.value.length > 0 || selected.length > 0;\n const description = selected.length\n ? `Search limited to ${selected.map(field => field.label).join(' or ')}.`\n : 'Search includes every available table field. Type slash to choose fields.';\n const popupStyle: Record<string, string> = {\n position: 'fixed',\n left: '0',\n top: '0',\n transform: `translate(${Math.round(this.position.x)}px, ${Math.round(this.position.y)}px)`,\n zIndex: 'var(--dimension-z-index-floating)',\n visibility: this.positionReady ? 'visible' : 'hidden',\n };\n if (this.availableHeight !== null) {\n popupStyle['max-height'] = `${Math.floor(this.availableHeight)}px`;\n }\n\n return (\n <Host\n class={{\n 'table-search-host': true,\n 'ds-control--md': true,\n 'ds-control-inactive': this.isInactive,\n }}\n data-filled={hasValue ? '' : undefined}\n data-expanded={this.menuOpen ? '' : undefined}\n >\n <div\n ref={element => {\n this.controlEl = element ?? null;\n }}\n class=\"table-search__control ds-control-frame ds-control--md ds-interaction-fill\"\n >\n <span\n class=\"table-search__icon ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-icon name=\"MagnifyingGlass\" size=\"md\" color=\"inherit\" />\n </span>\n <div class=\"table-search__editor ds-interaction-fill__content\">\n {selected.map(field => (\n <ds-chip\n class=\"table-search__chip\"\n key={field.id}\n label={field.label}\n size=\"md\"\n state=\"default\"\n isInset\n insetDepth=\"double\"\n isInactive={this.isInactive}\n onDsRemove={() => this.removeField(field)}\n />\n ))}\n <input\n ref={element => {\n this.inputEl = element ?? null;\n }}\n id={this.inputId}\n class=\"table-search__input ds-text--body-medium ds-text--regular\"\n type=\"search\"\n role=\"combobox\"\n value={inputValue}\n placeholder={this.placeholder}\n disabled={this.isInactive}\n aria-label={this.ariaLabel}\n aria-describedby={this.descriptionId}\n aria-haspopup=\"listbox\"\n aria-expanded={String(this.menuOpen)}\n aria-controls={this.menuOpen ? this.listboxId : undefined}\n aria-activedescendant={this.activeOptionId}\n aria-keyshortcuts=\"/\"\n autoComplete=\"off\"\n autoCapitalize=\"none\"\n autoCorrect=\"off\"\n spellcheck={false}\n onInput={event => this.handleInput(event)}\n onKeyDown={event => this.handleKeyDown(event)}\n />\n </div>\n {this.searchFields.length > 0 && (\n <div class=\"table-search__actions ds-interaction-fill__content\">\n {inputValue.length > 0 && !this.isInactive && (\n <ds-button-unfilled\n class=\"table-search__clear\"\n variant=\"icon\"\n size=\"sm\"\n icon=\"CrossCircle\"\n hasBorder={false}\n rounded\n ariaLabel={this.clearLabel}\n onDsClick={() => this.clearSearch()}\n />\n )}\n {inputValue.length > 0 && !this.isInactive && (\n <ds-divider\n class=\"table-search__action-divider\"\n orientation=\"vertical\"\n length=\"var(--dimension-size-200)\"\n />\n )}\n <ds-button-unfilled\n class=\"table-search__slash\"\n variant=\"label\"\n size=\"md\"\n label=\"/\"\n labelEmphasis={false}\n isInset\n insetDepth=\"double\"\n hasBorder={false}\n ariaLabel={this.fieldMenuLabel}\n focusTabIndex={-1}\n haspopup=\"listbox\"\n controls={this.menuOpen ? this.listboxId : undefined}\n expanded={this.menuOpen}\n surfaceOpen={this.menuOpen}\n isInactive={this.isInactive || available.length === 0}\n onMouseDown={(event: MouseEvent) => event.preventDefault()}\n onDsClick={() => {\n this.inputEl?.focus({ preventScroll: true });\n this.openMenu(false);\n }}\n />\n </div>\n )}\n </div>\n <span id={this.descriptionId} class=\"ds-visually-hidden\">\n {description}\n </span>\n <span class=\"ds-visually-hidden\" aria-live=\"polite\">\n {this.announcement}\n </span>\n {this.menuOpen && (\n <div\n popover=\"manual\"\n ref={element => {\n this.popupEl = element ?? null;\n }}\n class=\"table-search__popup ds-choice-popup\"\n style={popupStyle}\n >\n <div\n id={this.listboxId}\n class=\"table-search__list ds-choice-list\"\n role=\"listbox\"\n aria-label={this.fieldMenuLabel}\n >\n {visible.map((field, index) => {\n const active = index === this.activeIndex;\n return (\n /* eslint-disable-next-line local/prefer-direct-ds-text -- The option owns listbox semantics and pointer interaction while ds-text remains its typography child. */\n <div\n id={`${this.componentId}-option-${index}`}\n key={field.id}\n class={{\n 'table-search__option': true,\n 'ds-choice-item': true,\n 'ds-control-frame': true,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-focus-ring--visible': active && this.focusRingVisible,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--selected': active,\n }}\n role=\"option\"\n aria-selected={String(active)}\n onMouseDown={event => event.preventDefault()}\n onMouseMove={() => {\n this.focusRingVisible = false;\n this.activeIndex = index;\n }}\n onClick={() => this.selectField(field)}\n >\n <ds-text\n class=\"ds-choice-item__label ds-control-label-box ds-interaction-fill__content\"\n as=\"span\"\n variant=\"text-body-medium\"\n color={active ? 'primary' : 'secondary'}\n >\n {field.label}\n </ds-text>\n </div>\n );\n })}\n {visible.length === 0 && (\n <div class=\"table-search__empty ds-choice-empty ds-empty-region\">\n <ds-empty-state body=\"No matching data points\" />\n </div>\n )}\n </div>\n </div>\n )}\n </Host>\n );\n }\n}\n",
352
+ "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport {\n choicePopupMinWidth,\n resolveChoicePopupAlignOffset,\n resolveCssLengthPx,\n TOKEN_DEFAULTS,\n} from '../../utils';\nimport { AnchoredPositionController } from '../../utils/anchored-position-controller';\nimport { AnchoredOverlayInteractionController } from '../../utils/anchored-overlay-interaction-controller';\nimport { resolveAnchoredOverlayBoundaryRect } from '../../utils/anchored-overlay-boundary';\nimport {\n availableTableSearchFields,\n filterTableSearchFields,\n nextTableSearchActiveIndex,\n selectedTableSearchFields,\n tableSearchFields,\n} from './table-search-model';\nimport type { TableColumn } from '../Table/table-types';\nimport type { TableSearchField, TableSearchFieldsChangeDetail } from './table-search-types';\n\nlet tableSearchSequence = 0;\n\n@Component({\n tag: 'ds-table-search',\n styleUrl: 'TableSearch.css',\n scoped: true,\n})\nexport class TableSearch {\n @Element() private el!: HTMLElement;\n\n /** Show the resting field border; keyboard focus remains visible. */\n @Prop({ reflect: true }) hasBorder: boolean = true;\n\n /** Controlled free-text query. */\n @Prop() value: string = '';\n /** Table column catalog used to derive searchable data points and their complete labels. */\n @Prop() columns: TableColumn[] = [];\n /** Controlled ordered field scopes rendered as Tags. */\n @Prop() selectedFieldIds: string[] = [];\n @Prop() placeholder: string = 'Search';\n @Prop({ attribute: 'aria-label' }) ariaLabel: string = 'Search table';\n @Prop() clearLabel: string = 'Clear search';\n @Prop() fieldMenuLabel: string = 'Choose search fields';\n @Prop() isInactive: boolean = false;\n\n @Event({ bubbles: false }) dsChange!: EventEmitter<string>;\n @Event({ bubbles: false }) dsFieldsChange!: EventEmitter<TableSearchFieldsChangeDetail>;\n @Event({ bubbles: false }) dsClear!: EventEmitter<void>;\n\n @State() private menuOpen = false;\n @State() private menuQuery = '';\n @State() private activeIndex = 0;\n @State() private focusRingVisible = false;\n @State() private position = { x: 0, y: 0 };\n @State() private positionReady = false;\n @State() private availableHeight: number | null = null;\n @State() private announcement = '';\n\n private readonly componentId = `ds-table-search-${++tableSearchSequence}`;\n private readonly inputId = `${this.componentId}-input`;\n private readonly listboxId = `${this.componentId}-listbox`;\n private readonly descriptionId = `${this.componentId}-description`;\n private inputEl: HTMLInputElement | null = null;\n private controlEl: HTMLElement | null = null;\n private popupEl: HTMLElement | null = null;\n\n private readonly positionController = new AnchoredPositionController({\n getAnchor: () => this.controlEl,\n getPopup: () => this.popupEl,\n getOwnerDocument: () => this.el.ownerDocument,\n measure: (anchor, popup) => {\n if (!this.menuOpen || !popup.isConnected) return null;\n const inset = resolveCssLengthPx(TOKEN_DEFAULTS.space050, TOKEN_DEFAULTS.space050);\n popup.style.minWidth = `max(var(--ds-choice-popup-min-inline-size, ${TOKEN_DEFAULTS.menuWidthXs}), ${choicePopupMinWidth(anchor.offsetWidth, inset)}px)`;\n const anchorRect = anchor.getBoundingClientRect();\n return {\n anchorRect,\n popupWidth: popup.offsetWidth,\n popupHeight: popup.offsetHeight,\n side: 'bottom',\n align: 'start',\n sideOffsetPx: inset,\n alignOffsetPx: resolveChoicePopupAlignOffset({\n align: 'start',\n alignOffsetPx: 0,\n sectionInsetPx: inset,\n }),\n viewportPadPx: inset,\n viewportWidth: window.innerWidth,\n viewportHeight: window.innerHeight,\n collisionRect: resolveAnchoredOverlayBoundaryRect(anchor),\n };\n },\n apply: ({ x, y, availableHeight }) => {\n this.position = { x, y };\n this.availableHeight = availableHeight;\n },\n onReady: () => {\n this.positionReady = true;\n },\n topLayer: true,\n liveUpdate: 'frame',\n observeResize: true,\n observeAnchorMotion: true,\n });\n\n private readonly interactionController = new AnchoredOverlayInteractionController({\n getAnchor: () => this.controlEl,\n getPopup: () => this.popupEl,\n getOwnerDocument: () => this.el.ownerDocument,\n onOutsideActivation: () => this.closeMenu(),\n });\n\n disconnectedCallback(): void {\n this.closeMenu();\n }\n\n @Watch('columns')\n @Watch('selectedFieldIds')\n syncAvailableFields(): void {\n const count = this.availableFields.length;\n if (count === 0) {\n this.closeMenu();\n return;\n }\n if (this.activeIndex >= count) this.activeIndex = count - 1;\n }\n\n @Watch('isInactive')\n syncInactive(isInactive: boolean): void {\n if (isInactive) this.closeMenu();\n }\n\n @Method()\n async setFocus(): Promise<void> {\n this.inputEl?.focus();\n }\n\n private get selectedFields(): TableSearchField[] {\n return selectedTableSearchFields(this.searchFields, this.selectedFieldIds);\n }\n\n private get availableFields(): TableSearchField[] {\n return availableTableSearchFields(this.searchFields, this.selectedFieldIds);\n }\n\n private get visibleFields(): TableSearchField[] {\n return filterTableSearchFields(this.availableFields, this.menuQuery);\n }\n\n private get searchFields(): TableSearchField[] {\n return tableSearchFields(this.columns);\n }\n\n private get activeOptionId(): string | undefined {\n return this.menuOpen && this.activeIndex >= 0\n ? `${this.componentId}-option-${this.activeIndex}`\n : undefined;\n }\n\n private openMenu(fromKeyboard: boolean): void {\n if (this.isInactive || this.menuOpen || this.availableFields.length === 0) return;\n this.menuQuery = '';\n // The native value can be ahead of the controlled prop when slash follows input\n // in the same frame, so clear it before the queued render can skip an equal value.\n if (this.inputEl) this.inputEl.value = '';\n this.activeIndex = 0;\n this.focusRingVisible = fromKeyboard;\n this.positionReady = false;\n this.menuOpen = true;\n requestAnimationFrame(() => {\n if (!this.menuOpen) return;\n this.interactionController.connect();\n this.positionController.observe();\n this.positionController.schedule();\n });\n }\n\n private closeMenu(): void {\n if (!this.menuOpen && !this.popupEl) return;\n if (this.popupEl?.matches(':popover-open')) this.popupEl.hidePopover();\n this.positionController.unobserve();\n this.interactionController.disconnect();\n this.popupEl = null;\n this.positionReady = false;\n this.availableHeight = null;\n this.menuOpen = false;\n this.menuQuery = '';\n }\n\n private handleInput(event: Event): void {\n const nextValue = (event.target as HTMLInputElement).value;\n if (this.menuOpen) {\n this.menuQuery = nextValue;\n this.activeIndex = filterTableSearchFields(this.availableFields, nextValue).length ? 0 : -1;\n this.focusRingVisible = false;\n this.positionController.schedule();\n return;\n }\n\n this.dsChange.emit(nextValue);\n }\n\n private selectField(field: TableSearchField): void {\n this.dsFieldsChange.emit({ selectedFieldIds: [...this.selectedFieldIds, field.id] });\n this.announcement = `${field.label} search field added.`;\n this.closeMenu();\n requestAnimationFrame(() => this.inputEl?.focus({ preventScroll: true }));\n }\n\n private removeLastField(): void {\n const selected = this.selectedFields;\n const removed = selected[selected.length - 1];\n if (!removed) return;\n this.dsFieldsChange.emit({\n selectedFieldIds: this.selectedFieldIds.filter(fieldId => fieldId !== removed.id),\n });\n this.announcement = `${removed.label} search field removed.`;\n }\n\n private removeField(field: TableSearchField): void {\n this.dsFieldsChange.emit({\n selectedFieldIds: this.selectedFieldIds.filter(fieldId => fieldId !== field.id),\n });\n this.announcement = `${field.label} search field removed.`;\n requestAnimationFrame(() => this.inputEl?.focus({ preventScroll: true }));\n }\n\n private clearSearch(): void {\n if (this.value) this.dsChange.emit('');\n if (this.selectedFieldIds.length) this.dsFieldsChange.emit({ selectedFieldIds: [] });\n this.dsClear.emit();\n this.announcement = 'Search cleared.';\n this.closeMenu();\n requestAnimationFrame(() => this.inputEl?.focus({ preventScroll: true }));\n }\n\n private handleKeyDown(event: KeyboardEvent): void {\n if (event.key === '/' && !this.menuOpen && !event.altKey && !event.ctrlKey && !event.metaKey) {\n event.preventDefault();\n this.openMenu(true);\n return;\n }\n\n if (event.key === 'Backspace' && this.menuOpen && this.menuQuery.length === 0) {\n event.preventDefault();\n this.closeMenu();\n return;\n }\n\n if (event.key === 'Backspace' && this.value.length === 0) {\n if (this.selectedFields.length) {\n event.preventDefault();\n this.removeLastField();\n }\n return;\n }\n\n if (!this.menuOpen) return;\n\n if (event.key === 'Escape') {\n event.preventDefault();\n this.closeMenu();\n return;\n }\n\n if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {\n event.preventDefault();\n this.focusRingVisible = true;\n this.activeIndex = nextTableSearchActiveIndex(\n this.activeIndex,\n this.visibleFields.length,\n event.key === 'ArrowDown' ? 1 : -1\n );\n this.el\n .querySelector<HTMLElement>(`#${CSS.escape(this.activeOptionId ?? '')}`)\n ?.scrollIntoView({ block: 'nearest' });\n return;\n }\n\n if (event.key === 'Enter') {\n const field = this.visibleFields[this.activeIndex];\n if (!field) return;\n event.preventDefault();\n this.selectField(field);\n return;\n }\n\n if (event.key === 'Tab') this.closeMenu();\n }\n\n render() {\n const selected = this.selectedFields;\n const available = this.availableFields;\n const visible = this.visibleFields;\n const inputValue = this.menuOpen ? this.menuQuery : this.value;\n const hasValue = this.value.length > 0 || selected.length > 0;\n const description = selected.length\n ? `Search limited to ${selected.map(field => field.label).join(' or ')}.`\n : 'Search includes every available table field. Type slash to choose fields.';\n const popupStyle: Record<string, string> = {\n position: 'fixed',\n left: '0',\n top: '0',\n transform: `translate(${Math.round(this.position.x)}px, ${Math.round(this.position.y)}px)`,\n zIndex: 'var(--dimension-z-index-floating)',\n visibility: this.positionReady ? 'visible' : 'hidden',\n };\n if (this.availableHeight !== null) {\n popupStyle['max-height'] = `${Math.floor(this.availableHeight)}px`;\n }\n\n return (\n <Host\n class={{\n 'table-search-host': true,\n 'table-search-host--borderless': !this.hasBorder,\n 'ds-control--md': true,\n 'ds-control-inactive': this.isInactive,\n }}\n data-filled={hasValue ? '' : undefined}\n data-expanded={this.menuOpen ? '' : undefined}\n >\n <div\n ref={element => {\n this.controlEl = element ?? null;\n }}\n class=\"table-search__control ds-control-frame ds-control--md ds-interaction-fill\"\n >\n <span\n class=\"table-search__icon ds-control-icon-box ds-interaction-fill__content\"\n aria-hidden=\"true\"\n >\n <ds-icon name=\"MagnifyingGlass\" size=\"md\" color=\"inherit\" />\n </span>\n <div class=\"table-search__editor ds-interaction-fill__content\">\n {selected.map(field => (\n <ds-chip\n class=\"table-search__chip\"\n key={field.id}\n label={field.label}\n size=\"md\"\n state=\"default\"\n isInset\n insetDepth=\"double\"\n isInactive={this.isInactive}\n onDsRemove={() => this.removeField(field)}\n />\n ))}\n <input\n ref={element => {\n this.inputEl = element ?? null;\n }}\n id={this.inputId}\n class=\"table-search__input ds-text--body-medium ds-text--regular\"\n type=\"search\"\n role=\"combobox\"\n value={inputValue}\n placeholder={this.placeholder}\n disabled={this.isInactive}\n aria-label={this.ariaLabel}\n aria-describedby={this.descriptionId}\n aria-haspopup=\"listbox\"\n aria-expanded={String(this.menuOpen)}\n aria-controls={this.menuOpen ? this.listboxId : undefined}\n aria-activedescendant={this.activeOptionId}\n aria-keyshortcuts=\"/\"\n autoComplete=\"off\"\n autoCapitalize=\"none\"\n autoCorrect=\"off\"\n spellcheck={false}\n onInput={event => this.handleInput(event)}\n onKeyDown={event => this.handleKeyDown(event)}\n />\n </div>\n {inputValue.length > 0 && !this.isInactive && (\n <ds-button-unfilled\n class=\"table-search__clear ds-interaction-fill__content\"\n variant=\"icon\"\n size=\"sm\"\n icon=\"CrossCircle\"\n hasBorder={false}\n rounded\n ariaLabel={this.clearLabel}\n onDsClick={() => this.clearSearch()}\n />\n )}\n </div>\n <span id={this.descriptionId} class=\"ds-visually-hidden\">\n {description}\n </span>\n <span class=\"ds-visually-hidden\" aria-live=\"polite\">\n {this.announcement}\n </span>\n {this.menuOpen && (\n <div\n popover=\"manual\"\n ref={element => {\n this.popupEl = element ?? null;\n }}\n class=\"table-search__popup ds-choice-popup\"\n style={popupStyle}\n >\n <div\n id={this.listboxId}\n class=\"table-search__list ds-choice-list\"\n role=\"listbox\"\n aria-label={this.fieldMenuLabel}\n >\n {visible.map((field, index) => {\n const active = index === this.activeIndex;\n return (\n /* eslint-disable-next-line local/prefer-direct-ds-text -- The option owns listbox semantics and pointer interaction while ds-text remains its typography child. */\n <div\n id={`${this.componentId}-option-${index}`}\n key={field.id}\n class={{\n 'table-search__option': true,\n 'ds-choice-item': true,\n 'ds-control-frame': true,\n 'ds-control--md': true,\n 'ds-focus-ring-inset': true,\n 'ds-focus-ring--visible': active && this.focusRingVisible,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--selected': active,\n }}\n role=\"option\"\n aria-selected={String(active)}\n onMouseDown={event => event.preventDefault()}\n onMouseMove={() => {\n this.focusRingVisible = false;\n this.activeIndex = index;\n }}\n onClick={() => this.selectField(field)}\n >\n <ds-text\n class=\"ds-choice-item__label ds-control-label-box ds-interaction-fill__content\"\n as=\"span\"\n variant=\"text-body-medium\"\n color={active ? 'primary' : 'secondary'}\n >\n {field.label}\n </ds-text>\n </div>\n );\n })}\n {visible.length === 0 && (\n <div class=\"table-search__empty ds-choice-empty ds-empty-region\">\n <ds-empty-state body=\"No matching data points\" />\n </div>\n )}\n </div>\n </div>\n )}\n </Host>\n );\n }\n}\n",
335
353
  "type": "registry:ui"
336
354
  },
337
355
  {
@@ -51,7 +51,9 @@
51
51
  "Selecting a different data field keeps the current direction, or starts ascending when no sort exists. Selecting a direction applies it to the current field, or to the first sortable field when no sort exists. Selecting the already-active field or direction is a no-op and does not toggle the way a header click does.",
52
52
  "The trigger stays Sort in regular-weight resting secondary chrome. Field and direction selection live in the menu and on column headers, so the trigger never uses active primary emphasis. It keeps pressed interaction paint without physically scaling inside the stable table caption row.",
53
53
  "Compound sortable headers contribute one Data row per segment. Action columns never appear.",
54
- "The trigger automatically holds the shared surface-open wash through menu exit while the selected sort remains independently controlled."
54
+ "The trigger automatically holds the shared surface-open wash through menu exit while the selected sort remains independently controlled.",
55
+ "Borderless toolbar compositions omit resting control borders while retaining focus and interaction feedback.",
56
+ "Expanded toolbar triggers use an icon and regular-weight text without a trailing chevron."
55
57
  ],
56
58
  "responsiveBehavior": [
57
59
  "Place the control in the trailing region of ds-table-toolbar with Filter and Group.",
@@ -100,8 +102,16 @@
100
102
  "resolvedType": "TableColumn[]",
101
103
  "default": "[]",
102
104
  "required": false,
105
+ "mutable": false
106
+ },
107
+ "hasBorder": {
108
+ "type": "boolean",
109
+ "resolvedType": "boolean",
110
+ "attribute": "has-border",
111
+ "default": "true",
112
+ "required": false,
103
113
  "mutable": false,
104
- "description": "Catalog used to derive sortable fields, including compound header segments."
114
+ "description": "Show the trigger border."
105
115
  },
106
116
  "sort": {
107
117
  "type": "TableSortState | null",
@@ -144,8 +154,16 @@
144
154
  "resolvedType": "TableColumn[]",
145
155
  "default": "[]",
146
156
  "required": false,
157
+ "mutable": false
158
+ },
159
+ "hasBorder": {
160
+ "type": "boolean",
161
+ "resolvedType": "boolean",
162
+ "attribute": "has-border",
163
+ "default": "true",
164
+ "required": false,
147
165
  "mutable": false,
148
- "description": "Catalog used to derive sortable fields, including compound header segments."
166
+ "description": "Show the trigger border."
149
167
  },
150
168
  "sort": {
151
169
  "type": "TableSortState | null",
@@ -223,7 +241,7 @@
223
241
  },
224
242
  {
225
243
  "path": "src/wc/components/TableSort/TableSort.tsx",
226
- "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n State,\n} from '@stencil/core';\nimport type { MenuItemData } from '../Menu/menu-types';\nimport type { TableColumn, TableSortChangeDetail, TableSortState } from '../Table/table-types';\nimport {\n nextTableSortStateFromMenuItem,\n tableSortFields,\n tableSortMenuSections,\n tableSortStatesEqual,\n} from './table-sort-menu';\n\nlet tableSortSeq = 0;\n\n@Component({\n tag: 'ds-table-sort',\n styleUrl: 'TableSort.css',\n scoped: true,\n})\nexport class TableSort {\n @Element() private el!: HTMLElement;\n\n /** Catalog used to derive sortable fields, including compound header segments. */\n @Prop() columns: TableColumn[] = [];\n /** Controlled table sort. Header sorting and this menu share the same value. */\n @Prop() sort: TableSortState | null = null;\n /** Accessible name for the trigger and menu. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n\n @Event({ bubbles: false }) dsSortChange!: EventEmitter<TableSortChangeDetail>;\n\n @State() private menuOpen = false;\n @State() private menuSurfaceOpen = false;\n @State() private initialFocusVisible = false;\n\n private readonly componentId = `ds-table-sort-${++tableSortSeq}`;\n private readonly triggerId = `${this.componentId}-trigger`;\n private readonly menuId = `${this.componentId}-menu`;\n\n @Method()\n async setFocus() {\n const trigger = this.el.querySelector<HTMLElement & { setFocus?: () => Promise<void> }>(\n `#${CSS.escape(this.triggerId)}`\n );\n await trigger?.setFocus?.();\n }\n\n render() {\n const fields = tableSortFields(this.columns);\n const name = this.ariaLabel?.trim() || 'Sort table';\n\n return (\n <Host hidden={fields.length === 0 ? true : undefined}>\n {fields.length === 0 ? null : (\n <ds-button-unfilled\n id={this.triggerId}\n variant=\"icon-label\"\n size=\"md\"\n icon=\"ArrowsVertical\"\n label=\"Sort\"\n labelEmphasis={false}\n pressScale={false}\n aria-label={name}\n hasMenu={true}\n collapseLabel={true}\n expanded={this.menuOpen}\n surfaceOpen={this.menuSurfaceOpen}\n controls={this.menuId}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n this.toggle(event.detail.detail === 0);\n }}\n />\n )}\n {fields.length === 0 ? null : (\n <ds-menu\n id={this.menuId}\n open={this.menuOpen}\n anchorId={this.triggerId}\n align=\"start\"\n side=\"bottom\"\n menuLabel={name}\n selectionMode=\"none\"\n initialFocusVisible={this.initialFocusVisible}\n sections={tableSortMenuSections(this.columns, this.sort)}\n onDsClose={() => this.close()}\n onDsAfterClose={() => {\n if (!this.menuOpen) this.menuSurfaceOpen = false;\n }}\n onDsSelect={event => this.handleSelect(event.detail)}\n />\n )}\n </Host>\n );\n }\n\n private toggle(fromKeyboard = false): void {\n if (this.menuOpen) this.close();\n else this.open(fromKeyboard);\n }\n\n private open(fromKeyboard = false): void {\n if (this.menuOpen) return;\n this.initialFocusVisible = fromKeyboard;\n this.menuOpen = true;\n this.menuSurfaceOpen = true;\n }\n\n private close(): void {\n this.menuOpen = false;\n }\n\n private handleSelect(item: MenuItemData): void {\n const next = nextTableSortStateFromMenuItem(this.columns, this.sort, item);\n if (tableSortStatesEqual(this.sort, next)) return;\n this.dsSortChange.emit({ sort: next });\n }\n}\n",
244
+ "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Method,\n Prop,\n State,\n} from '@stencil/core';\nimport type { MenuItemData } from '../Menu/menu-types';\nimport type { TableColumn, TableSortChangeDetail, TableSortState } from '../Table/table-types';\nimport {\n nextTableSortStateFromMenuItem,\n tableSortFields,\n tableSortMenuSections,\n tableSortStatesEqual,\n} from './table-sort-menu';\n\nlet tableSortSeq = 0;\n\n@Component({\n tag: 'ds-table-sort',\n styleUrl: 'TableSort.css',\n scoped: true,\n})\nexport class TableSort {\n @Element() private el!: HTMLElement;\n\n /** Catalog used to derive sortable fields, including compound header segments. */\n /** Show the trigger border. */\n @Prop() hasBorder: boolean = true;\n @Prop() columns: TableColumn[] = [];\n /** Controlled table sort. Header sorting and this menu share the same value. */\n @Prop() sort: TableSortState | null = null;\n /** Accessible name for the trigger and menu. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n\n @Event({ bubbles: false }) dsSortChange!: EventEmitter<TableSortChangeDetail>;\n\n @State() private menuOpen = false;\n @State() private menuSurfaceOpen = false;\n @State() private initialFocusVisible = false;\n\n private readonly componentId = `ds-table-sort-${++tableSortSeq}`;\n private readonly triggerId = `${this.componentId}-trigger`;\n private readonly menuId = `${this.componentId}-menu`;\n\n @Method()\n async setFocus() {\n const trigger = this.el.querySelector<HTMLElement & { setFocus?: () => Promise<void> }>(\n `#${CSS.escape(this.triggerId)}`\n );\n await trigger?.setFocus?.();\n }\n\n render() {\n const fields = tableSortFields(this.columns);\n const name = this.ariaLabel?.trim() || 'Sort table';\n\n return (\n <Host hidden={fields.length === 0 ? true : undefined}>\n {fields.length === 0 ? null : (\n <ds-button-unfilled\n hasBorder={this.hasBorder}\n id={this.triggerId}\n variant=\"icon-label\"\n size=\"md\"\n icon=\"ArrowsVertical\"\n label=\"Sort\"\n labelEmphasis={false}\n pressScale={false}\n aria-label={name}\n haspopup=\"menu\"\n collapseLabel={true}\n expanded={this.menuOpen}\n surfaceOpen={this.menuSurfaceOpen}\n controls={this.menuId}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n this.toggle(event.detail.detail === 0);\n }}\n />\n )}\n {fields.length === 0 ? null : (\n <ds-menu\n id={this.menuId}\n open={this.menuOpen}\n anchorId={this.triggerId}\n align=\"start\"\n side=\"bottom\"\n menuLabel={name}\n selectionMode=\"none\"\n initialFocusVisible={this.initialFocusVisible}\n sections={tableSortMenuSections(this.columns, this.sort)}\n onDsClose={() => this.close()}\n onDsAfterClose={() => {\n if (!this.menuOpen) this.menuSurfaceOpen = false;\n }}\n onDsSelect={event => this.handleSelect(event.detail)}\n />\n )}\n </Host>\n );\n }\n\n private toggle(fromKeyboard = false): void {\n if (this.menuOpen) this.close();\n else this.open(fromKeyboard);\n }\n\n private open(fromKeyboard = false): void {\n if (this.menuOpen) return;\n this.initialFocusVisible = fromKeyboard;\n this.menuOpen = true;\n this.menuSurfaceOpen = true;\n }\n\n private close(): void {\n this.menuOpen = false;\n }\n\n private handleSelect(item: MenuItemData): void {\n const next = nextTableSortStateFromMenuItem(this.columns, this.sort, item);\n if (tableSortStatesEqual(this.sort, next)) return;\n this.dsSortChange.emit({ sort: next });\n }\n}\n",
227
245
  "type": "registry:ui"
228
246
  },
229
247
  {
@@ -32,7 +32,8 @@
32
32
  "component:ds-table-group",
33
33
  "component:ds-select",
34
34
  "component:ds-table-sort",
35
- "component:ds-button-unfilled"
35
+ "component:ds-button-unfilled",
36
+ "component:ds-table-preferences"
36
37
  ],
37
38
  "patterns": [],
38
39
  "accessibility": [
@@ -43,14 +44,15 @@
43
44
  "states": [
44
45
  "The component owns layout only. It never mutates slotted control props, values, menus, loading, persistence, or consequences. Use ds-table-saved-views and ds-table-filter when those interactions need the shared CompoMo behavior while application data and persistence remain external.",
45
46
  "Buttons in the toolbar keep their hover and pressed interaction paint without physically scaling, so pointer feedback does not disturb the stable caption geometry.",
46
- "The start cluster is followed by a decorative vertical divider, then Search, optional leading controls, and one trailing action cluster. The start cluster and divider hide when nothing is projected. Search is a dedicated slot that hides when nothing is projected and grows to consume available row space. Empty leading or trailing clusters add no placeholder chrome. Search is the only toolbar region that grows and shrinks; all action controls retain their control geometry and align directly before the table-owned actions.",
47
- "Column customizer and data-mode switching stay table-owned caption actions. Do not recreate those menus in the toolbar. When slotted into a table header, the table caption owns overflow for the complete toolbar and table-owned action row."
47
+ "The start cluster is followed by Search, optional leading controls, and one trailing action cluster. The start cluster hides when nothing is projected. Search is a dedicated slot that hides when nothing is projected and grows to consume available row space. Empty leading or trailing clusters add no placeholder chrome. Search is the only toolbar region that grows and shrinks; all action controls retain their control geometry and align directly before the table-owned actions.",
48
+ "Data-mode switching remains a table-owned caption action. Column customization normally uses the table caption trigger; compact compositions use the shared ds-table-preferences control and hide that trigger. When slotted into a table header, the table caption owns overflow for the complete toolbar and table-owned action row.",
49
+ "Standard toolbars omit region dividers. Borderless compositions show dividers after populated start and search regions when another region follows; the application supplies borderless controls."
48
50
  ],
49
51
  "responsiveBehavior": [
50
52
  "The start cluster, search, trailing Filter/Sort/Group, and table caption trailing actions stay on one line. Saved views hug their current trigger content up to the compact menu-width token rather than reserving that full width; long names truncate while the dirty dot stays visible. Filter, Sort, and Group triggers cap at the same token, and Sort and Group popups keep that compact menu-width floor. A standalone toolbar owns its horizontal scrolling; inside a table, the complete caption row is the single overflow owner so table-owned actions never remain pinned over scrolling toolbar controls.",
51
- "Search consumes the row space left between fixed control groups and flexes smaller when the complete row needs space. Inside a named ds-table container narrower than 900px, ds-table-filter, ds-table-group, and ds-table-sort omit their labels and chevrons; each exposes its label through a hover and keyboard-focus tooltip. Empty leading and trailing slot groups do not add spacing. Active filtering promotes its prefix icon to primary. Group and Sort keep resting secondary chrome. Table-owned Customize also becomes the icon-only Table menu button at that width with the same tooltip behavior.",
53
+ "Search consumes the row space left between fixed control groups and flexes smaller when the complete row needs space. Inside a named ds-table container narrower than 900px, ds-table-filter, ds-table-group, and ds-table-sort omit their labels and chevrons; each exposes its label through a hover and keyboard-focus tooltip. Empty leading and trailing slot groups do not add spacing. Active filtering retains neutral trigger colors. Group and Sort keep resting secondary chrome. Table-owned Customize also becomes the icon-only Table menu button at that width with the same tooltip behavior.",
52
54
  "The scrollbar is visually suppressed without removing touch, trackpad, mouse-wheel, or keyboard scrolling from the overflow container.",
53
- "Trailing controls remain grouped after leading controls; the toolbar does not collapse controls into an application-specific overflow menu."
55
+ "For compact toolbars, the application can replace Filter, Sort, Group and the table Customize trigger with ds-table-preferences in the trailing slot. Share the same controlled values and events; the toolbar remains a data-agnostic layout."
54
56
  ],
55
57
  "frameworkCaveats": {
56
58
  "customElements": [
@@ -76,6 +78,15 @@
76
78
  },
77
79
  "api": {
78
80
  "props": {
81
+ "borderless": {
82
+ "type": "boolean",
83
+ "resolvedType": "boolean",
84
+ "attribute": "borderless",
85
+ "default": "false",
86
+ "required": false,
87
+ "mutable": false,
88
+ "description": "Use separators between regions when composing borderless controls."
89
+ },
79
90
  "label": {
80
91
  "type": "string",
81
92
  "resolvedType": "string",
@@ -91,6 +102,15 @@
91
102
  "slots": []
92
103
  },
93
104
  "props": {
105
+ "borderless": {
106
+ "type": "boolean",
107
+ "resolvedType": "boolean",
108
+ "attribute": "borderless",
109
+ "default": "false",
110
+ "required": false,
111
+ "mutable": false,
112
+ "description": "Use separators between regions when composing borderless controls."
113
+ },
94
114
  "label": {
95
115
  "type": "string",
96
116
  "resolvedType": "string",
@@ -153,7 +173,7 @@
153
173
  },
154
174
  {
155
175
  "path": "src/wc/components/TableToolbar/TableToolbar.tsx",
156
- "content": "import { Component, Element, h, Host, Prop, State } from '@stencil/core';\n\nconst TABLE_TOOLBAR_SLOT_START = 1 << 0;\nconst TABLE_TOOLBAR_SLOT_SEARCH = 1 << 1;\nconst TABLE_TOOLBAR_SLOT_LEADING = 1 << 2;\nconst TABLE_TOOLBAR_SLOT_TRAILING = 1 << 3;\n\n@Component({\n tag: 'ds-table-toolbar',\n styleUrl: 'TableToolbar.css',\n scoped: true,\n})\nexport class TableToolbar {\n @Element() el!: HTMLElement;\n\n /** Accessible name for the grouped table controls. */\n @Prop() label: string = 'Table controls';\n\n @State() private slotPresence = 0;\n\n componentWillLoad(): void {\n this.syncSlotPresence();\n }\n\n private hasOwnedSlot(name: string): boolean {\n return Array.from(this.el.querySelectorAll<HTMLElement>(`[slot=\"${name}\"]`)).some(\n element => element.closest('ds-table-toolbar') === this.el\n );\n }\n\n private syncSlotPresence = () => {\n let presence = 0;\n if (this.hasOwnedSlot('start')) presence |= TABLE_TOOLBAR_SLOT_START;\n if (this.hasOwnedSlot('search')) presence |= TABLE_TOOLBAR_SLOT_SEARCH;\n if (this.hasOwnedSlot('leading')) presence |= TABLE_TOOLBAR_SLOT_LEADING;\n if (this.hasOwnedSlot('trailing')) presence |= TABLE_TOOLBAR_SLOT_TRAILING;\n if (presence !== this.slotPresence) this.slotPresence = presence;\n };\n\n render() {\n const hasStart = (this.slotPresence & TABLE_TOOLBAR_SLOT_START) !== 0;\n const hasSearch = (this.slotPresence & TABLE_TOOLBAR_SLOT_SEARCH) !== 0;\n const hasLeading = (this.slotPresence & TABLE_TOOLBAR_SLOT_LEADING) !== 0;\n const hasTrailing = (this.slotPresence & TABLE_TOOLBAR_SLOT_TRAILING) !== 0;\n\n return (\n <Host>\n <div class=\"table-toolbar\" role=\"toolbar\" aria-label={this.label}>\n <div\n class={{\n 'table-toolbar__start': true,\n 'table-toolbar__slot--empty': !hasStart,\n }}\n >\n <slot name=\"start\" onSlotchange={this.syncSlotPresence} />\n </div>\n <ds-divider\n class={{\n 'table-toolbar__rule': true,\n 'table-toolbar__slot--empty': !hasStart,\n }}\n orientation=\"vertical\"\n length=\"32px\"\n />\n <div\n class={{\n 'table-toolbar__search': true,\n 'table-toolbar__slot--empty': !hasSearch,\n }}\n >\n <slot name=\"search\" onSlotchange={this.syncSlotPresence} />\n </div>\n <div\n class={{\n 'table-toolbar__leading': true,\n 'table-toolbar__slot--empty': !hasLeading,\n }}\n >\n <slot name=\"leading\" onSlotchange={this.syncSlotPresence} />\n </div>\n <div\n class={{\n 'table-toolbar__trailing': true,\n 'table-toolbar__slot--empty': !hasTrailing,\n }}\n >\n <slot name=\"trailing\" onSlotchange={this.syncSlotPresence} />\n </div>\n </div>\n </Host>\n );\n }\n}\n",
176
+ "content": "import { Component, Element, h, Host, Prop, State } from '@stencil/core';\n\nconst TABLE_TOOLBAR_SLOT_START = 1 << 0;\nconst TABLE_TOOLBAR_SLOT_SEARCH = 1 << 1;\nconst TABLE_TOOLBAR_SLOT_LEADING = 1 << 2;\nconst TABLE_TOOLBAR_SLOT_TRAILING = 1 << 3;\n\n@Component({\n tag: 'ds-table-toolbar',\n styleUrl: 'TableToolbar.css',\n scoped: true,\n})\nexport class TableToolbar {\n @Element() el!: HTMLElement;\n\n /** Accessible name for the grouped table controls. */\n @Prop() label: string = 'Table controls';\n\n /** Use separators between regions when composing borderless controls. */\n @Prop() borderless: boolean = false;\n\n @State() private slotPresence = 0;\n\n componentWillLoad(): void {\n this.syncSlotPresence();\n }\n\n private hasOwnedSlot(name: string): boolean {\n return Array.from(this.el.querySelectorAll<HTMLElement>(`[slot=\"${name}\"]`)).some(\n element => element.closest('ds-table-toolbar') === this.el\n );\n }\n\n private syncSlotPresence = () => {\n let presence = 0;\n if (this.hasOwnedSlot('start')) presence |= TABLE_TOOLBAR_SLOT_START;\n if (this.hasOwnedSlot('search')) presence |= TABLE_TOOLBAR_SLOT_SEARCH;\n if (this.hasOwnedSlot('leading')) presence |= TABLE_TOOLBAR_SLOT_LEADING;\n if (this.hasOwnedSlot('trailing')) presence |= TABLE_TOOLBAR_SLOT_TRAILING;\n if (presence !== this.slotPresence) this.slotPresence = presence;\n };\n\n render() {\n const hasStart = (this.slotPresence & TABLE_TOOLBAR_SLOT_START) !== 0;\n const hasSearch = (this.slotPresence & TABLE_TOOLBAR_SLOT_SEARCH) !== 0;\n const hasLeading = (this.slotPresence & TABLE_TOOLBAR_SLOT_LEADING) !== 0;\n const hasTrailing = (this.slotPresence & TABLE_TOOLBAR_SLOT_TRAILING) !== 0;\n\n return (\n <Host>\n <div class=\"table-toolbar\" role=\"toolbar\" aria-label={this.label}>\n <div\n class={{\n 'table-toolbar__start': true,\n 'table-toolbar__slot--empty': !hasStart,\n }}\n >\n <slot name=\"start\" onSlotchange={this.syncSlotPresence} />\n </div>\n <ds-divider\n class={{\n 'table-toolbar__rule': true,\n 'table-toolbar__slot--empty':\n !this.borderless || !hasStart || !(hasSearch || hasLeading || hasTrailing),\n }}\n orientation=\"vertical\"\n length=\"var(--dimension-space-250)\"\n />\n <div\n class={{\n 'table-toolbar__search': true,\n 'table-toolbar__slot--empty': !hasSearch,\n }}\n >\n <slot name=\"search\" onSlotchange={this.syncSlotPresence} />\n </div>\n <ds-divider\n class={{\n 'table-toolbar__rule': true,\n 'table-toolbar__slot--empty':\n !this.borderless || !hasSearch || !(hasLeading || hasTrailing),\n }}\n orientation=\"vertical\"\n length=\"var(--dimension-space-250)\"\n />\n <div\n class={{\n 'table-toolbar__leading': true,\n 'table-toolbar__slot--empty': !hasLeading,\n }}\n >\n <slot name=\"leading\" onSlotchange={this.syncSlotPresence} />\n </div>\n <div\n class={{\n 'table-toolbar__trailing': true,\n 'table-toolbar__slot--empty': !hasTrailing,\n }}\n >\n <slot name=\"trailing\" onSlotchange={this.syncSlotPresence} />\n </div>\n </div>\n </Host>\n );\n }\n}\n",
157
177
  "type": "registry:ui"
158
178
  }
159
179
  ]