@ds-mo/ui 14.35.0 → 15.0.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 (577) hide show
  1. package/dist/.package-ready.json +1 -1
  2. package/dist/agent-patterns.json +1 -1
  3. package/dist/agent.json +5733 -4656
  4. package/dist/angular/ds-button-inverted.d.ts +17 -0
  5. package/dist/angular/ds-button-inverted.js +46 -0
  6. package/dist/angular/ds-calendar.d.ts +15 -0
  7. package/dist/angular/ds-calendar.js +43 -0
  8. package/dist/angular/ds-conversation-list.d.ts +1 -1
  9. package/dist/angular/ds-conversation-list.js +3 -4
  10. package/dist/angular/ds-data-customize.d.ts +18 -0
  11. package/dist/angular/ds-data-customize.js +46 -0
  12. package/dist/angular/ds-data-filter.d.ts +50 -0
  13. package/dist/angular/{ds-table-filter.js → ds-data-filter.js} +10 -10
  14. package/dist/angular/ds-data-group.d.ts +29 -0
  15. package/dist/angular/{ds-table-group.js → ds-data-group.js} +10 -10
  16. package/dist/angular/ds-data-preferences.d.ts +39 -0
  17. package/dist/angular/{ds-table-preferences.js → ds-data-preferences.js} +15 -15
  18. package/dist/angular/ds-data-saved-views.d.ts +49 -0
  19. package/dist/angular/{ds-table-saved-views.js → ds-data-saved-views.js} +10 -10
  20. package/dist/angular/ds-data-search.d.ts +20 -0
  21. package/dist/angular/{ds-table-search.js → ds-data-search.js} +12 -12
  22. package/dist/angular/ds-data-sort.d.ts +16 -0
  23. package/dist/angular/{ds-table-sort.js → ds-data-sort.js} +12 -12
  24. package/dist/angular/ds-data-toolbar.d.ts +12 -0
  25. package/dist/angular/{ds-table-toolbar.js → ds-data-toolbar.js} +10 -10
  26. package/dist/angular/ds-field.d.ts +1 -1
  27. package/dist/angular/ds-field.js +1 -1
  28. package/dist/angular/ds-input-date.d.ts +15 -0
  29. package/dist/angular/ds-input-date.js +43 -0
  30. package/dist/angular/ds-input-time.d.ts +15 -0
  31. package/dist/angular/ds-input-time.js +43 -0
  32. package/dist/angular/ds-input.d.ts +1 -1
  33. package/dist/angular/ds-input.js +3 -3
  34. package/dist/angular/ds-select.d.ts +1 -1
  35. package/dist/angular/ds-select.js +3 -3
  36. package/dist/angular/ds-table.d.ts +7 -7
  37. package/dist/angular/ds-table.js +6 -6
  38. package/dist/angular/ds-time-picker.d.ts +15 -0
  39. package/dist/angular/ds-time-picker.js +43 -0
  40. package/dist/angular/index.d.ts +1 -1
  41. package/dist/angular/index.js +13 -7
  42. package/dist/angular/proxies.d.ts +13 -7
  43. package/dist/angular/proxies.js +13 -7
  44. package/dist/angular/text-value-accessor.d.ts +1 -1
  45. package/dist/angular/text-value-accessor.js +2 -2
  46. package/dist/components/ds-agent-activity.js +1 -1
  47. package/dist/components/ds-agent-questionnaire.js +1 -1
  48. package/dist/components/ds-agent-response.js +1 -1
  49. package/dist/components/ds-agent-source-list.js +1 -1
  50. package/dist/components/ds-agent-tool-call.js +1 -1
  51. package/dist/components/ds-attachment-list.js +1 -1
  52. package/dist/components/ds-avatar.js +1 -1
  53. package/dist/components/ds-badge.js +1 -1
  54. package/dist/components/ds-banner.js +1 -1
  55. package/dist/components/ds-bar-action.js +1 -1
  56. package/dist/components/ds-bar-nav.js +1 -1
  57. package/dist/components/ds-bar-page-title.js +1 -1
  58. package/dist/components/ds-bar-title.js +1 -1
  59. package/dist/components/ds-bar-workflow.js +1 -1
  60. package/dist/components/ds-breadcrumb.js +1 -1
  61. package/dist/components/ds-button-filled.js +1 -1
  62. package/dist/components/ds-button-inverted.d.ts +11 -0
  63. package/dist/components/ds-button-inverted.js +2 -0
  64. package/dist/components/ds-button-inverted.js.map +1 -0
  65. package/dist/components/ds-button-unfilled.js +1 -1
  66. package/dist/components/{ds-table-search.d.ts → ds-calendar.d.ts} +4 -4
  67. package/dist/components/ds-calendar.js +2 -0
  68. package/dist/components/ds-card-action-center.js +1 -1
  69. package/dist/components/ds-card-chart.js +1 -1
  70. package/dist/components/ds-card-navigation.js +1 -1
  71. package/dist/components/ds-card-navigation.js.map +1 -1
  72. package/dist/components/ds-card-overview.js +1 -1
  73. package/dist/components/ds-card-setting.js +1 -1
  74. package/dist/components/ds-card-settings-scope.js +1 -1
  75. package/dist/components/ds-card-settings-scope.js.map +1 -1
  76. package/dist/components/ds-chart-legend.js +1 -1
  77. package/dist/components/ds-chart.js +1 -1
  78. package/dist/components/ds-checkbox.js +1 -1
  79. package/dist/components/ds-chip.js +1 -1
  80. package/dist/components/ds-code-block.js +1 -1
  81. package/dist/components/ds-conversation-list-item.js +1 -1
  82. package/dist/components/ds-conversation-list-item.js.map +1 -1
  83. package/dist/components/ds-conversation-list-section.js +1 -1
  84. package/dist/components/ds-conversation-list.js +1 -1
  85. package/dist/components/ds-conversation-list.js.map +1 -1
  86. package/dist/components/ds-data-customize.d.ts +11 -0
  87. package/dist/components/ds-data-customize.js +2 -0
  88. package/dist/components/ds-data-customize.js.map +1 -0
  89. package/dist/components/{ds-table-group.d.ts → ds-data-filter.d.ts} +4 -4
  90. package/dist/components/ds-data-filter.js +2 -0
  91. package/dist/components/ds-data-filter.js.map +1 -0
  92. package/dist/components/{ds-table-sort.d.ts → ds-data-group.d.ts} +4 -4
  93. package/dist/components/ds-data-group.js +2 -0
  94. package/dist/components/ds-data-group.js.map +1 -0
  95. package/dist/components/{ds-table-saved-views.d.ts → ds-data-preferences.d.ts} +4 -4
  96. package/dist/components/ds-data-preferences.js +2 -0
  97. package/dist/components/ds-data-preferences.js.map +1 -0
  98. package/dist/components/ds-data-saved-views.d.ts +11 -0
  99. package/dist/components/ds-data-saved-views.js +2 -0
  100. package/dist/components/ds-data-saved-views.js.map +1 -0
  101. package/dist/components/ds-data-search.d.ts +11 -0
  102. package/dist/components/ds-data-search.js +2 -0
  103. package/dist/components/ds-data-search.js.map +1 -0
  104. package/dist/components/ds-data-sort.d.ts +11 -0
  105. package/dist/components/ds-data-sort.js +2 -0
  106. package/dist/components/ds-data-sort.js.map +1 -0
  107. package/dist/components/{ds-table-filter.d.ts → ds-data-toolbar.d.ts} +4 -4
  108. package/dist/components/ds-data-toolbar.js +2 -0
  109. package/dist/components/ds-data-toolbar.js.map +1 -0
  110. package/dist/components/ds-divider.js +1 -1
  111. package/dist/components/ds-empty-state.js +1 -1
  112. package/dist/components/ds-field.js +1 -1
  113. package/dist/components/ds-filter-menu.js +1 -1
  114. package/dist/components/ds-icon.js +1 -1
  115. package/dist/components/ds-input-date.d.ts +11 -0
  116. package/dist/components/ds-input-date.js +2 -0
  117. package/dist/components/ds-input-date.js.map +1 -0
  118. package/dist/components/ds-input-time.d.ts +11 -0
  119. package/dist/components/ds-input-time.js +2 -0
  120. package/dist/components/ds-input-time.js.map +1 -0
  121. package/dist/components/ds-input.js +1 -1
  122. package/dist/components/ds-markdown.js +1 -1
  123. package/dist/components/ds-menu.js +1 -1
  124. package/dist/components/ds-message-actions.js +1 -1
  125. package/dist/components/ds-message-bubble.js +1 -1
  126. package/dist/components/ds-message-bubble.js.map +1 -1
  127. package/dist/components/ds-message-composer.js +1 -1
  128. package/dist/components/ds-message-composer.js.map +1 -1
  129. package/dist/components/ds-message-scroller.js +1 -1
  130. package/dist/components/ds-message.js +1 -1
  131. package/dist/components/ds-mobile-bar-nav.js +1 -1
  132. package/dist/components/ds-mobile-header.js +1 -1
  133. package/dist/components/ds-mobile-section-switcher.js +1 -1
  134. package/dist/components/ds-mobile-sheet-nav.js +1 -1
  135. package/dist/components/ds-modal.js +1 -1
  136. package/dist/components/ds-pagination.js +1 -1
  137. package/dist/components/ds-panel-nav.js +1 -1
  138. package/dist/components/ds-panel-sub-nav.js +1 -1
  139. package/dist/components/ds-panel-tool-header.js +1 -1
  140. package/dist/components/ds-panel-tool-search.js +1 -1
  141. package/dist/components/ds-panel-tools.js +1 -1
  142. package/dist/components/ds-paper-texture.js +1 -1
  143. package/dist/components/ds-radio.js +1 -1
  144. package/dist/components/ds-score.js +1 -1
  145. package/dist/components/ds-scroll-overlay.js +1 -1
  146. package/dist/components/ds-select.js +1 -1
  147. package/dist/components/ds-shell-app.js +1 -1
  148. package/dist/components/ds-shell-page.js +1 -1
  149. package/dist/components/ds-shell-tools.js +1 -1
  150. package/dist/components/ds-skeleton.js +1 -1
  151. package/dist/components/ds-slider.js +1 -1
  152. package/dist/components/ds-slider.js.map +1 -1
  153. package/dist/components/ds-swatch-picker.js +1 -1
  154. package/dist/components/ds-switch.js +1 -1
  155. package/dist/components/ds-tab-group.js +1 -1
  156. package/dist/components/ds-table.js +1 -1
  157. package/dist/components/ds-table.js.map +1 -1
  158. package/dist/components/ds-tag.js +1 -1
  159. package/dist/components/ds-text.js +1 -1
  160. package/dist/components/ds-textarea.js +1 -1
  161. package/dist/components/ds-textarea.js.map +1 -1
  162. package/dist/components/ds-time-picker.d.ts +11 -0
  163. package/dist/components/ds-time-picker.js +2 -0
  164. package/dist/components/ds-time-picker.js.map +1 -0
  165. package/dist/components/ds-toast.js +1 -1
  166. package/dist/components/ds-tooltip-chart.js +1 -1
  167. package/dist/components/ds-tooltip.js +1 -1
  168. package/dist/components/ds-typing-indicator.js +1 -1
  169. package/dist/components/index.d.ts +5 -5
  170. package/dist/components/p-0FYD2bev.js +2 -0
  171. package/dist/components/p-0FYD2bev.js.map +1 -0
  172. package/dist/components/p-0qY_HDId.js +2 -0
  173. package/dist/components/{p-D_u8WhlU.js.map → p-0qY_HDId.js.map} +1 -1
  174. package/dist/components/{p-BQsVI_vO.js → p-5URekSny.js} +2 -2
  175. package/dist/components/{p-eo_pmPqp.js → p-7V1AhetH.js} +2 -2
  176. package/dist/components/{p-yZRXPHDe.js → p-8yBdCXCn.js} +2 -2
  177. package/dist/components/p-BKZdqJQB.js +2 -0
  178. package/dist/components/p-BKZdqJQB.js.map +1 -0
  179. package/dist/components/p-BYrBU_U9.js +2 -0
  180. package/dist/components/p-BYrBU_U9.js.map +1 -0
  181. package/dist/components/{p-D44g2Kw1.js → p-BciTtTwx.js} +2 -2
  182. package/dist/components/p-BciTtTwx.js.map +1 -0
  183. package/dist/components/{p-CZOiTaNa.js → p-BeNa6tw5.js} +2 -2
  184. package/dist/components/{p-CZOiTaNa.js.map → p-BeNa6tw5.js.map} +1 -1
  185. package/dist/components/p-BgQMEHRa.js +2 -0
  186. package/dist/components/{p-CwqFJMvP.js.map → p-BgQMEHRa.js.map} +1 -1
  187. package/dist/components/{p-BQaO7cg_.js → p-BhJXmuRA.js} +2 -2
  188. package/dist/components/{p-n68x-ZKp.js → p-Bm7rqs3C.js} +2 -2
  189. package/dist/components/{p-DdspY23v.js → p-Bn8I7iw6.js} +2 -2
  190. package/dist/components/{p-BnWxbhFP.js → p-Bpd6YCo4.js} +2 -2
  191. package/dist/components/{p-B-9fdIWv.js → p-C-3YqeTb.js} +2 -2
  192. package/dist/components/{p-umAf0fBn.js → p-CD4DZPy_.js} +2 -2
  193. package/dist/components/p-CElUM-xU.js +2 -0
  194. package/dist/components/p-CElUM-xU.js.map +1 -0
  195. package/dist/components/{p-CuwopElg.js → p-CGidq1iC.js} +2 -2
  196. package/dist/components/p-CNxvnQ6A.js +2 -0
  197. package/dist/components/p-CNxvnQ6A.js.map +1 -0
  198. package/dist/components/{p-CI7VfYC3.js → p-CUcUHwI3.js} +2 -2
  199. package/dist/components/p-CYzFfFcH.js +2 -0
  200. package/dist/components/{p-BINSI_KF.js.map → p-CYzFfFcH.js.map} +1 -1
  201. package/dist/components/{p-Dhw_W8nk.js → p-Cc3ZUQlJ.js} +2 -2
  202. package/dist/components/p-Cc3ZUQlJ.js.map +1 -0
  203. package/dist/components/p-CdgOLWX9.js +2 -0
  204. package/dist/components/p-CdgOLWX9.js.map +1 -0
  205. package/dist/components/{p-BcdHgPi5.js → p-CeXf9-X3.js} +2 -2
  206. package/dist/components/{p-CSnsz3Eb.js → p-CiH4GG6r.js} +2 -2
  207. package/dist/components/{p-y06IxxoN.js → p-CiJ1WR11.js} +2 -2
  208. package/dist/components/p-CmexIVnE.js +2 -0
  209. package/dist/components/{p-N5SHSVo2.js.map → p-CmexIVnE.js.map} +1 -1
  210. package/dist/components/{p-BmFeeR4q.js → p-CqW2eyiF.js} +2 -2
  211. package/dist/components/{p-CzbsTZt7.js → p-Cxr0IQjC.js} +2 -2
  212. package/dist/components/{p-CqlBlv55.js → p-CzIpquvw.js} +2 -2
  213. package/dist/components/{p-DC69DgAR.js → p-D0y7Sja6.js} +2 -2
  214. package/dist/components/{p-Cwz9TPVS.js → p-D1vQGJzc.js} +2 -2
  215. package/dist/components/{p-Cwz9TPVS.js.map → p-D1vQGJzc.js.map} +1 -1
  216. package/dist/components/{p-C7CZ9pnP.js → p-D6Wl3dRO.js} +2 -2
  217. package/dist/components/{p-BPhbwXOv.js → p-D922f6Tb.js} +2 -2
  218. package/dist/components/{p-beRFLHbB.js → p-DHICPS4n.js} +2 -2
  219. package/dist/components/{p-omPm8_vx.js → p-DOAybCp0.js} +2 -2
  220. package/dist/components/{p-DEGNrPLe.js → p-DVINzOoK.js} +2 -2
  221. package/dist/components/{p-DEGNrPLe.js.map → p-DVINzOoK.js.map} +1 -1
  222. package/dist/components/p-DVOjjuXc.js +2 -0
  223. package/dist/components/p-DVOjjuXc.js.map +1 -0
  224. package/dist/components/p-Di5FH7eh.js +2 -0
  225. package/dist/components/p-Di5FH7eh.js.map +1 -0
  226. package/dist/components/p-DjkUFlDW.js +2 -0
  227. package/dist/components/p-DjkUFlDW.js.map +1 -0
  228. package/dist/components/{p-BnzsBNUY.js → p-Dn9QHL4O.js} +2 -2
  229. package/dist/components/{p-DGzn4k4h.js → p-Do2FJd1y.js} +2 -2
  230. package/dist/components/{p-DTDLlinw.js → p-DpoTwJAR.js} +2 -2
  231. package/dist/components/{p-Dk-8Ju0Q.js → p-Dpr2y6_S.js} +2 -2
  232. package/dist/components/{p-Dk-8Ju0Q.js.map → p-Dpr2y6_S.js.map} +1 -1
  233. package/dist/components/p-Dw9sE6Iw.js +2 -0
  234. package/dist/components/p-Dw9sE6Iw.js.map +1 -0
  235. package/dist/components/{p-CLGtPE0K.js → p-DwbcNDJF.js} +2 -2
  236. package/dist/components/p-Dzndkdx5.js +2 -0
  237. package/dist/components/{p-xHGqrz_p.js.map → p-Dzndkdx5.js.map} +1 -1
  238. package/dist/components/{p-C8ek-1SP.js → p-E5W27Fu_.js} +2 -2
  239. package/dist/components/{p-mpk_Mm8_.js → p-FIBCluwV.js} +2 -2
  240. package/dist/components/{p-DQQFN2sw.js → p-IEq_06J0.js} +2 -2
  241. package/dist/components/{p-DQQFN2sw.js.map → p-IEq_06J0.js.map} +1 -1
  242. package/dist/components/{p-CE540WZ7.js → p-MG4lcN0D.js} +2 -2
  243. package/dist/components/p-Nc76seSq.js +2 -0
  244. package/dist/components/p-Nc76seSq.js.map +1 -0
  245. package/dist/components/{p-DtLQIMsa.js → p-ZhP2tVmM.js} +2 -2
  246. package/dist/components/p-aD5cw_m2.js +2 -0
  247. package/dist/components/p-aD5cw_m2.js.map +1 -0
  248. package/dist/components/p-etifxiDc.js +2 -0
  249. package/dist/components/{p-beCz9OnD.js.map → p-etifxiDc.js.map} +1 -1
  250. package/dist/components/p-kDZ0sEII.js +2 -0
  251. package/dist/components/p-kDZ0sEII.js.map +1 -0
  252. package/dist/components/{p-BPlWEidl.js → p-ljbw3UMZ.js} +2 -2
  253. package/dist/components/{p-DZ58Sg5a.js → p-vfCu7XSc.js} +2 -2
  254. package/dist/components/p-vfCu7XSc.js.map +1 -0
  255. package/dist/components/{p-DFjIBS5B.js → p-xdmJCPCg.js} +2 -2
  256. package/dist/components/{p-CAxDdYjh.js → p-xtGGIZmU.js} +2 -2
  257. package/dist/docs/components.json +18102 -14568
  258. package/dist/lib/components/Icon/flag-icon-catalog.d.ts +2 -0
  259. package/dist/lib/components/Icon/icon-cache.d.ts +36 -4
  260. package/dist/lib/components/Icon/map-icon-catalog.d.ts +2 -0
  261. package/dist/lib/shell/index.js +2 -2
  262. package/dist/lib/shell/index.js.map +1 -1
  263. package/dist/lib/utils/button-render.d.ts +1 -1
  264. package/dist/lib/utils/clock-time.d.ts +22 -0
  265. package/dist/lib/utils/date-filter-value.d.ts +4 -0
  266. package/dist/lib/utils/index.d.ts +3 -1
  267. package/dist/lib/utils/index.js +201 -5
  268. package/dist/lib/utils/index.js.map +4 -4
  269. package/dist/mcp-data/registry/agent-activity.json +2 -2
  270. package/dist/mcp-data/registry/agent-questionnaire.json +2 -2
  271. package/dist/mcp-data/registry/agent-response.json +1 -1
  272. package/dist/mcp-data/registry/agent-source-list.json +1 -1
  273. package/dist/mcp-data/registry/agent-tool-call.json +2 -2
  274. package/dist/mcp-data/registry/attachment-list.json +1 -1
  275. package/dist/mcp-data/registry/avatar.json +1 -1
  276. package/dist/mcp-data/registry/badge.json +1 -1
  277. package/dist/mcp-data/registry/banner.json +1 -1
  278. package/dist/mcp-data/registry/bar-action.json +1 -1
  279. package/dist/mcp-data/registry/bar-nav.json +1 -1
  280. package/dist/mcp-data/registry/bar-page-title.json +1 -1
  281. package/dist/mcp-data/registry/bar-title.json +3 -3
  282. package/dist/mcp-data/registry/bar-workflow.json +1 -1
  283. package/dist/mcp-data/registry/breadcrumb.json +1 -1
  284. package/dist/mcp-data/registry/button-filled.json +1 -1
  285. package/dist/mcp-data/registry/button-inverted.json +580 -0
  286. package/dist/mcp-data/registry/button-unfilled.json +1 -1
  287. package/dist/mcp-data/registry/calendar.json +273 -0
  288. package/dist/mcp-data/registry/card-action-center.json +1 -1
  289. package/dist/mcp-data/registry/card-chart.json +1 -1
  290. package/dist/mcp-data/registry/card-navigation.json +5 -3
  291. package/dist/mcp-data/registry/card-overview.json +1 -1
  292. package/dist/mcp-data/registry/card-setting.json +1 -1
  293. package/dist/mcp-data/registry/card-settings-scope.json +1 -1
  294. package/dist/mcp-data/registry/chart-legend.json +1 -1
  295. package/dist/mcp-data/registry/chart.json +1 -1
  296. package/dist/mcp-data/registry/checkbox.json +1 -1
  297. package/dist/mcp-data/registry/chip.json +1 -1
  298. package/dist/mcp-data/registry/code-block.json +1 -1
  299. package/dist/mcp-data/registry/conversation-list-item.json +1 -1
  300. package/dist/mcp-data/registry/conversation-list-section.json +1 -1
  301. package/dist/mcp-data/registry/conversation-list.json +10 -32
  302. package/dist/mcp-data/registry/data-customize.json +374 -0
  303. package/dist/mcp-data/registry/{table-filter.json → data-filter.json} +33 -33
  304. package/dist/mcp-data/registry/data-group.json +358 -0
  305. package/dist/mcp-data/registry/data-preferences.json +566 -0
  306. package/dist/mcp-data/registry/data-saved-views.json +433 -0
  307. package/dist/mcp-data/registry/data-search.json +367 -0
  308. package/dist/mcp-data/registry/data-sort.json +254 -0
  309. package/dist/mcp-data/registry/data-toolbar.json +181 -0
  310. package/dist/mcp-data/registry/divider.json +1 -1
  311. package/dist/mcp-data/registry/empty-state.json +1 -1
  312. package/dist/mcp-data/registry/field.json +20 -15
  313. package/dist/mcp-data/registry/filter-menu.json +7 -11
  314. package/dist/mcp-data/registry/icon.json +13 -7
  315. package/dist/mcp-data/registry/input-date.json +516 -0
  316. package/dist/mcp-data/registry/input-time.json +535 -0
  317. package/dist/mcp-data/registry/input.json +57 -9
  318. package/dist/mcp-data/registry/loader.json +1 -1
  319. package/dist/mcp-data/registry/markdown.json +1 -1
  320. package/dist/mcp-data/registry/menu.json +3 -3
  321. package/dist/mcp-data/registry/message-actions.json +1 -1
  322. package/dist/mcp-data/registry/message-bubble.json +1 -1
  323. package/dist/mcp-data/registry/message-composer.json +2 -2
  324. package/dist/mcp-data/registry/message-scroller.json +1 -1
  325. package/dist/mcp-data/registry/message.json +1 -1
  326. package/dist/mcp-data/registry/mobile-bar-nav.json +2 -2
  327. package/dist/mcp-data/registry/mobile-header.json +1 -1
  328. package/dist/mcp-data/registry/mobile-section-switcher.json +1 -1
  329. package/dist/mcp-data/registry/mobile-sheet-nav.json +2 -2
  330. package/dist/mcp-data/registry/modal.json +1 -1
  331. package/dist/mcp-data/registry/pagination.json +1 -1
  332. package/dist/mcp-data/registry/panel-nav.json +1 -1
  333. package/dist/mcp-data/registry/panel-sub-nav.json +1 -1
  334. package/dist/mcp-data/registry/panel-tool-header.json +1 -1
  335. package/dist/mcp-data/registry/panel-tool-search.json +1 -1
  336. package/dist/mcp-data/registry/panel-tools.json +3 -3
  337. package/dist/mcp-data/registry/paper-texture.json +1 -1
  338. package/dist/mcp-data/registry/radio.json +1 -1
  339. package/dist/mcp-data/registry/registry.json +5911 -4816
  340. package/dist/mcp-data/registry/score.json +1 -1
  341. package/dist/mcp-data/registry/scroll-overlay.json +1 -1
  342. package/dist/mcp-data/registry/select.json +64 -8
  343. package/dist/mcp-data/registry/shell-app.json +1 -1
  344. package/dist/mcp-data/registry/shell-page.json +1 -1
  345. package/dist/mcp-data/registry/shell-tools.json +1 -1
  346. package/dist/mcp-data/registry/skeleton.json +1 -1
  347. package/dist/mcp-data/registry/slider.json +1 -1
  348. package/dist/mcp-data/registry/swatch-picker.json +1 -1
  349. package/dist/mcp-data/registry/switch.json +1 -1
  350. package/dist/mcp-data/registry/tab-group.json +1 -1
  351. package/dist/mcp-data/registry/table.json +71 -71
  352. package/dist/mcp-data/registry/tag.json +1 -1
  353. package/dist/mcp-data/registry/text.json +1 -1
  354. package/dist/mcp-data/registry/textarea.json +3 -3
  355. package/dist/mcp-data/registry/time-picker.json +259 -0
  356. package/dist/mcp-data/registry/toast.json +1 -1
  357. package/dist/mcp-data/registry/tooltip-chart.json +1 -1
  358. package/dist/mcp-data/registry/tooltip.json +1 -1
  359. package/dist/mcp-data/registry/typing-indicator.json +1 -1
  360. package/dist/react/components.d.ts +13 -7
  361. package/dist/react/components.js +13 -7
  362. package/dist/react/ds-button-inverted.d.ts +13 -0
  363. package/dist/react/ds-button-inverted.js +15 -0
  364. package/dist/react/ds-calendar.d.ts +12 -0
  365. package/dist/react/ds-calendar.js +12 -0
  366. package/dist/react/ds-data-customize.d.ts +13 -0
  367. package/dist/react/ds-data-customize.js +15 -0
  368. package/dist/react/ds-data-filter.d.ts +18 -0
  369. package/dist/react/{ds-table-filter.js → ds-data-filter.js} +5 -5
  370. package/dist/react/ds-data-group.d.ts +14 -0
  371. package/dist/react/{ds-table-group.js → ds-data-group.js} +5 -5
  372. package/dist/react/ds-data-preferences.d.ts +21 -0
  373. package/dist/react/{ds-table-preferences.js → ds-data-preferences.js} +6 -6
  374. package/dist/react/ds-data-saved-views.d.ts +17 -0
  375. package/dist/react/{ds-table-saved-views.js → ds-data-saved-views.js} +5 -5
  376. package/dist/react/ds-data-search.d.ts +14 -0
  377. package/dist/react/{ds-table-search.js → ds-data-search.js} +5 -5
  378. package/dist/react/ds-data-sort.d.ts +12 -0
  379. package/dist/react/ds-data-sort.js +12 -0
  380. package/dist/react/{ds-table-toolbar.d.ts → ds-data-toolbar.d.ts} +3 -3
  381. package/dist/react/ds-data-toolbar.js +12 -0
  382. package/dist/react/ds-field.d.ts +1 -1
  383. package/dist/react/ds-input-date.d.ts +12 -0
  384. package/dist/react/ds-input-date.js +12 -0
  385. package/dist/react/ds-input-time.d.ts +12 -0
  386. package/dist/react/ds-input-time.js +12 -0
  387. package/dist/react/ds-table.d.ts +3 -3
  388. package/dist/react/ds-table.js +1 -1
  389. package/dist/react/ds-time-picker.d.ts +12 -0
  390. package/dist/react/ds-time-picker.js +12 -0
  391. package/dist/styles/table.css +11 -11
  392. package/dist/types/components/BarTitle/BarTitle.d.ts +1 -0
  393. package/dist/types/components/BarTitle/bar-title-action-chrome.d.ts +2 -0
  394. package/dist/types/components/ButtonInverted/ButtonInverted.d.ts +83 -0
  395. package/dist/types/components/ButtonInverted/index.d.ts +2 -0
  396. package/dist/types/components/Calendar/Calendar.d.ts +32 -0
  397. package/dist/types/components/Calendar/calendar-grid.d.ts +19 -0
  398. package/dist/types/components/ConversationList/ConversationList.d.ts +0 -7
  399. package/dist/types/components/DataCustomize/DataCustomize.d.ts +67 -0
  400. package/dist/types/components/{TableFilter/TableFilter.d.ts → DataFilter/DataFilter.d.ts} +2 -2
  401. package/dist/types/components/{TableGroup/TableGroup.d.ts → DataGroup/DataGroup.d.ts} +8 -8
  402. package/dist/types/components/{TablePreferences/TablePreferences.d.ts → DataPreferences/DataPreferences.d.ts} +29 -13
  403. package/dist/types/components/{TableSavedViews/TableSavedViews.d.ts → DataSavedViews/DataSavedViews.d.ts} +10 -10
  404. package/dist/types/components/DataSavedViews/data-saved-views-types.d.ts +24 -0
  405. package/dist/types/components/DataSavedViews/index.d.ts +2 -0
  406. package/dist/types/components/{TableSearch/TableSearch.d.ts → DataSearch/DataSearch.d.ts} +6 -6
  407. package/dist/types/components/{TableSearch/table-search-model.d.ts → DataSearch/data-search-model.d.ts} +6 -6
  408. package/dist/types/components/{TableSearch/table-search-types.d.ts → DataSearch/data-search-types.d.ts} +3 -3
  409. package/dist/types/components/{TableSort/TableSort.d.ts → DataSort/DataSort.d.ts} +7 -6
  410. package/dist/types/components/DataSort/data-sort-menu.d.ts +15 -0
  411. package/dist/types/components/{TableToolbar/TableToolbar.d.ts → DataToolbar/DataToolbar.d.ts} +3 -3
  412. package/dist/types/components/Field/Field.d.ts +7 -2
  413. package/dist/types/components/FilterMenu/FilterMenu.d.ts +0 -10
  414. package/dist/types/components/Icon/icon-cache.d.ts +36 -4
  415. package/dist/types/components/Icon/map-icon-catalog.d.ts +3 -0
  416. package/dist/types/components/Input/Input.d.ts +10 -0
  417. package/dist/types/components/InputDate/InputDate.d.ts +78 -0
  418. package/dist/types/components/InputTime/InputTime.d.ts +80 -0
  419. package/dist/types/components/PanelTools/PanelTools.d.ts +1 -1
  420. package/dist/types/components/Select/Select.d.ts +8 -1
  421. package/dist/types/components/Table/Table.d.ts +9 -9
  422. package/dist/types/components/Table/index.d.ts +1 -1
  423. package/dist/types/components/Table/table-action-menu.d.ts +1 -1
  424. package/dist/types/components/Table/table-body-renderer.d.ts +2 -2
  425. package/dist/types/components/Table/table-column-customizer.d.ts +9 -9
  426. package/dist/types/components/Table/table-group-load-controller.d.ts +2 -2
  427. package/dist/types/components/Table/table-model.d.ts +7 -7
  428. package/dist/types/components/Table/table-render-model.d.ts +3 -3
  429. package/dist/types/components/Table/table-types.d.ts +17 -36
  430. package/dist/types/components/Table/table-virtual-model.d.ts +2 -2
  431. package/dist/types/components/TimePicker/TimePicker.d.ts +33 -0
  432. package/dist/types/components.d.ts +5461 -4433
  433. package/dist/types/utils/button-render.d.ts +1 -1
  434. package/dist/types/utils/clock-time.d.ts +23 -0
  435. package/dist/types/utils/data-field.d.ts +38 -0
  436. package/dist/types/utils/date-filter-value.d.ts +4 -0
  437. package/dist/types/utils/index.d.ts +3 -1
  438. package/dist/types/utils/list-customizer.d.ts +38 -0
  439. package/dist/types/utils/table-caption-compact.d.ts +1 -1
  440. package/dist/vue/components.d.ts +13 -7
  441. package/dist/vue/components.js +13 -7
  442. package/dist/vue/ds-button-inverted.d.ts +3 -0
  443. package/dist/vue/ds-button-inverted.js +33 -0
  444. package/dist/vue/{ds-table-sort.d.ts → ds-calendar.d.ts} +1 -1
  445. package/dist/vue/ds-calendar.js +16 -0
  446. package/dist/vue/ds-conversation-list.js +1 -3
  447. package/dist/vue/ds-data-customize.d.ts +3 -0
  448. package/dist/vue/ds-data-customize.js +23 -0
  449. package/dist/vue/{ds-table-group.d.ts → ds-data-filter.d.ts} +1 -1
  450. package/dist/vue/{ds-table-filter.js → ds-data-filter.js} +2 -2
  451. package/dist/vue/{ds-table-filter.d.ts → ds-data-group.d.ts} +1 -1
  452. package/dist/vue/{ds-table-group.js → ds-data-group.js} +2 -2
  453. package/dist/vue/{ds-table-saved-views.d.ts → ds-data-preferences.d.ts} +1 -1
  454. package/dist/vue/{ds-table-preferences.js → ds-data-preferences.js} +10 -7
  455. package/dist/vue/ds-data-saved-views.d.ts +3 -0
  456. package/dist/vue/{ds-table-saved-views.js → ds-data-saved-views.js} +2 -2
  457. package/dist/vue/{ds-table-search.d.ts → ds-data-search.d.ts} +1 -1
  458. package/dist/vue/{ds-table-search.js → ds-data-search.js} +3 -3
  459. package/dist/vue/ds-data-sort.d.ts +3 -0
  460. package/dist/vue/{ds-table-sort.js → ds-data-sort.js} +3 -3
  461. package/dist/vue/ds-data-toolbar.d.ts +3 -0
  462. package/dist/vue/ds-data-toolbar.js +9 -0
  463. package/dist/vue/{ds-table-preferences.d.ts → ds-input-date.d.ts} +1 -1
  464. package/dist/vue/ds-input-date.js +31 -0
  465. package/dist/vue/ds-input-time.d.ts +3 -0
  466. package/dist/vue/ds-input-time.js +32 -0
  467. package/dist/vue/ds-input.js +2 -0
  468. package/dist/vue/ds-select.js +3 -0
  469. package/dist/vue/ds-table.js +4 -4
  470. package/dist/vue/ds-time-picker.d.ts +3 -0
  471. package/dist/vue/ds-time-picker.js +16 -0
  472. package/package.json +3 -3
  473. package/dist/angular/ds-table-filter.d.ts +0 -50
  474. package/dist/angular/ds-table-group.d.ts +0 -29
  475. package/dist/angular/ds-table-preferences.d.ts +0 -39
  476. package/dist/angular/ds-table-saved-views.d.ts +0 -49
  477. package/dist/angular/ds-table-search.d.ts +0 -20
  478. package/dist/angular/ds-table-sort.d.ts +0 -16
  479. package/dist/angular/ds-table-toolbar.d.ts +0 -12
  480. package/dist/components/ds-table-filter.js +0 -2
  481. package/dist/components/ds-table-filter.js.map +0 -1
  482. package/dist/components/ds-table-group.js +0 -2
  483. package/dist/components/ds-table-preferences.d.ts +0 -11
  484. package/dist/components/ds-table-preferences.js +0 -2
  485. package/dist/components/ds-table-preferences.js.map +0 -1
  486. package/dist/components/ds-table-saved-views.js +0 -2
  487. package/dist/components/ds-table-saved-views.js.map +0 -1
  488. package/dist/components/ds-table-search.js +0 -2
  489. package/dist/components/ds-table-search.js.map +0 -1
  490. package/dist/components/ds-table-sort.js +0 -2
  491. package/dist/components/ds-table-sort.js.map +0 -1
  492. package/dist/components/ds-table-toolbar.d.ts +0 -11
  493. package/dist/components/ds-table-toolbar.js +0 -2
  494. package/dist/components/ds-table-toolbar.js.map +0 -1
  495. package/dist/components/p-BINSI_KF.js +0 -2
  496. package/dist/components/p-BbfBvMRR.js +0 -2
  497. package/dist/components/p-BbfBvMRR.js.map +0 -1
  498. package/dist/components/p-C1_3CVma.js +0 -2
  499. package/dist/components/p-C1_3CVma.js.map +0 -1
  500. package/dist/components/p-CKarsuR2.js +0 -2
  501. package/dist/components/p-CKarsuR2.js.map +0 -1
  502. package/dist/components/p-Cea7rROU.js +0 -2
  503. package/dist/components/p-Cea7rROU.js.map +0 -1
  504. package/dist/components/p-Cmj04E5Q.js +0 -2
  505. package/dist/components/p-Cmj04E5Q.js.map +0 -1
  506. package/dist/components/p-Cq_Qpe_C.js +0 -2
  507. package/dist/components/p-Cq_Qpe_C.js.map +0 -1
  508. package/dist/components/p-CwqFJMvP.js +0 -2
  509. package/dist/components/p-D44g2Kw1.js.map +0 -1
  510. package/dist/components/p-DZ58Sg5a.js.map +0 -1
  511. package/dist/components/p-D_u8WhlU.js +0 -2
  512. package/dist/components/p-Dhw_W8nk.js.map +0 -1
  513. package/dist/components/p-FPZbS19l.js +0 -2
  514. package/dist/components/p-FPZbS19l.js.map +0 -1
  515. package/dist/components/p-KBrC9IG4.js +0 -2
  516. package/dist/components/p-KBrC9IG4.js.map +0 -1
  517. package/dist/components/p-N5SHSVo2.js +0 -2
  518. package/dist/components/p-QjkufCcn.js +0 -2
  519. package/dist/components/p-QjkufCcn.js.map +0 -1
  520. package/dist/components/p-UAIxh67Q.js +0 -2
  521. package/dist/components/p-UAIxh67Q.js.map +0 -1
  522. package/dist/components/p-beCz9OnD.js +0 -2
  523. package/dist/components/p-xHGqrz_p.js +0 -2
  524. package/dist/mcp-data/registry/table-group.json +0 -358
  525. package/dist/mcp-data/registry/table-preferences.json +0 -511
  526. package/dist/mcp-data/registry/table-saved-views.json +0 -431
  527. package/dist/mcp-data/registry/table-search.json +0 -366
  528. package/dist/mcp-data/registry/table-sort.json +0 -253
  529. package/dist/mcp-data/registry/table-toolbar.json +0 -180
  530. package/dist/react/ds-table-filter.d.ts +0 -18
  531. package/dist/react/ds-table-group.d.ts +0 -14
  532. package/dist/react/ds-table-preferences.d.ts +0 -21
  533. package/dist/react/ds-table-saved-views.d.ts +0 -17
  534. package/dist/react/ds-table-search.d.ts +0 -14
  535. package/dist/react/ds-table-sort.d.ts +0 -12
  536. package/dist/react/ds-table-sort.js +0 -12
  537. package/dist/react/ds-table-toolbar.js +0 -12
  538. package/dist/types/components/FilterMenu/filter-menu-calendar.d.ts +0 -13
  539. package/dist/types/components/TableSavedViews/index.d.ts +0 -2
  540. package/dist/types/components/TableSavedViews/table-saved-views-types.d.ts +0 -24
  541. package/dist/types/components/TableSort/table-sort-menu.d.ts +0 -15
  542. package/dist/vue/ds-table-toolbar.d.ts +0 -3
  543. package/dist/vue/ds-table-toolbar.js +0 -9
  544. /package/dist/components/{ds-table-group.js.map → ds-calendar.js.map} +0 -0
  545. /package/dist/components/{p-BQsVI_vO.js.map → p-5URekSny.js.map} +0 -0
  546. /package/dist/components/{p-eo_pmPqp.js.map → p-7V1AhetH.js.map} +0 -0
  547. /package/dist/components/{p-yZRXPHDe.js.map → p-8yBdCXCn.js.map} +0 -0
  548. /package/dist/components/{p-BQaO7cg_.js.map → p-BhJXmuRA.js.map} +0 -0
  549. /package/dist/components/{p-n68x-ZKp.js.map → p-Bm7rqs3C.js.map} +0 -0
  550. /package/dist/components/{p-DdspY23v.js.map → p-Bn8I7iw6.js.map} +0 -0
  551. /package/dist/components/{p-BnWxbhFP.js.map → p-Bpd6YCo4.js.map} +0 -0
  552. /package/dist/components/{p-B-9fdIWv.js.map → p-C-3YqeTb.js.map} +0 -0
  553. /package/dist/components/{p-umAf0fBn.js.map → p-CD4DZPy_.js.map} +0 -0
  554. /package/dist/components/{p-CuwopElg.js.map → p-CGidq1iC.js.map} +0 -0
  555. /package/dist/components/{p-CI7VfYC3.js.map → p-CUcUHwI3.js.map} +0 -0
  556. /package/dist/components/{p-BcdHgPi5.js.map → p-CeXf9-X3.js.map} +0 -0
  557. /package/dist/components/{p-CSnsz3Eb.js.map → p-CiH4GG6r.js.map} +0 -0
  558. /package/dist/components/{p-y06IxxoN.js.map → p-CiJ1WR11.js.map} +0 -0
  559. /package/dist/components/{p-BmFeeR4q.js.map → p-CqW2eyiF.js.map} +0 -0
  560. /package/dist/components/{p-CzbsTZt7.js.map → p-Cxr0IQjC.js.map} +0 -0
  561. /package/dist/components/{p-CqlBlv55.js.map → p-CzIpquvw.js.map} +0 -0
  562. /package/dist/components/{p-DC69DgAR.js.map → p-D0y7Sja6.js.map} +0 -0
  563. /package/dist/components/{p-C7CZ9pnP.js.map → p-D6Wl3dRO.js.map} +0 -0
  564. /package/dist/components/{p-BPhbwXOv.js.map → p-D922f6Tb.js.map} +0 -0
  565. /package/dist/components/{p-beRFLHbB.js.map → p-DHICPS4n.js.map} +0 -0
  566. /package/dist/components/{p-omPm8_vx.js.map → p-DOAybCp0.js.map} +0 -0
  567. /package/dist/components/{p-BnzsBNUY.js.map → p-Dn9QHL4O.js.map} +0 -0
  568. /package/dist/components/{p-DGzn4k4h.js.map → p-Do2FJd1y.js.map} +0 -0
  569. /package/dist/components/{p-DTDLlinw.js.map → p-DpoTwJAR.js.map} +0 -0
  570. /package/dist/components/{p-CLGtPE0K.js.map → p-DwbcNDJF.js.map} +0 -0
  571. /package/dist/components/{p-C8ek-1SP.js.map → p-E5W27Fu_.js.map} +0 -0
  572. /package/dist/components/{p-mpk_Mm8_.js.map → p-FIBCluwV.js.map} +0 -0
  573. /package/dist/components/{p-CE540WZ7.js.map → p-MG4lcN0D.js.map} +0 -0
  574. /package/dist/components/{p-DtLQIMsa.js.map → p-ZhP2tVmM.js.map} +0 -0
  575. /package/dist/components/{p-BPlWEidl.js.map → p-ljbw3UMZ.js.map} +0 -0
  576. /package/dist/components/{p-DFjIBS5B.js.map → p-xdmJCPCg.js.map} +0 -0
  577. /package/dist/components/{p-CAxDdYjh.js.map → p-xtGGIZmU.js.map} +0 -0
@@ -30,10 +30,10 @@
30
30
  "component:ds-skeleton",
31
31
  "component:ds-loader",
32
32
  "component:ds-pagination",
33
- "component:ds-table-toolbar",
34
- "component:ds-table-saved-views",
35
- "component:ds-table-filter",
36
- "component:ds-table-sort",
33
+ "component:ds-data-toolbar",
34
+ "component:ds-data-saved-views",
35
+ "component:ds-data-filter",
36
+ "component:ds-data-sort",
37
37
  "component:ds-bar-action",
38
38
  "component:ds-button-unfilled",
39
39
  "component:ds-menu",
@@ -49,18 +49,18 @@
49
49
  "The rendered structure uses a native table, caption, column headers, row groups, and data cells; keep the caption non-empty. The native caption remains visually hidden as the table's accessible name. When caption visibility is enabled, a 48px header bar renders as component chrome outside the data scrollport. It defaults to a presentational caption title that does not duplicate the accessible name. Application-owned labeled controls supplied through the single named header slot replace that fallback and share one horizontal caption overflow row with table-owned actions inside the component-owned 8px inset. Table caption buttons keep pressed interaction paint without physically scaling. When columnCustomizer is enabled, the table adds a trailing Customize control named Customize table. Its label uses regular weight and its resting foreground remains neutral when hidden or reordered columns differ from the catalog default. The control is icon-label at typical widths and the icon-only Table menu button below 900px, where a tooltip preserves the Customize label. The always-icon-only table-variant control also exposes its label through a tooltip. Otherwise the header slot receives the complete content width.",
50
50
  "The data-mode switcher is the shared Menu of the table's supported infinite, pagination, and virtual modes. It uses an Ellipses trigger. The switcher is the final caption-bar action; when the column customizer is also present, a decorative vertical divider separates the two controls. dataMode remains controlled, and the application responds to change intent by supplying the matching records, pagination state, loading behavior, and query consequences. Escape, dismissal, and selection return focus to the trigger.",
51
51
  "The column customizer is the shared Menu of switch rows, not a table-owned dialog. Data columns can be shown, hidden, and reordered. Action columns and the selection checkbox never appear in the menu. The last remaining visible data column cannot be hidden, but it stays focusable so it can still be reordered. Escape and outside click restore focus to the trigger. Persistence stays in the application.",
52
- "Sorting controls remain buttons inside column headers and expose the table's one controlled member-row sort through aria-sort. ds-table-sort in the toolbar trailing cluster is the matching menu of Data and Order sections; it emits the same dsSortChange shape so header clicks and menu choices stay in sync. Grouping never adds a second sort indicator or header control; ds-table-group controls the grouping data point and ascending or descending group-section order while the application supplies the resulting groups. Optional column help underlines the header label with the shared dotted decoration and shows ds-tooltip from that label; it does not add a second header control or replace the column name.",
52
+ "Sorting controls remain buttons inside column headers and expose the table's one controlled member-row sort through aria-sort. ds-data-sort in the toolbar trailing cluster is the matching menu of Data and Order sections; it emits the same dsSortChange shape so header clicks and menu choices stay in sync. Grouping never adds a second sort indicator or header control; ds-data-group controls the grouping data point and ascending or descending group-section order while the application supplies the resulting groups. Optional column help underlines the header label with the shared dotted decoration and shows ds-tooltip from that label; it does not add a second header control or replace the column name.",
53
53
  "Selection controls expose checkbox semantics and explicit row names; select-all affects selectable loaded rows only and preserves selected identities outside the loaded window. In virtual mode the supplied in-memory rows are that loaded set, including rows whose DOM is recycled. When grouping is active with multi-selection, each group section header also exposes a checkbox that selects or clears that group's selectable loaded rows.",
54
54
  "Interactive rows are focusable as native rows, activate with Enter or Space, and do not steal activation from their nested selection, action, overflow-menu, or primary-text link controls. When an application-owned details surface navigates between records, scrollRowIntoView reveals the newly associated row with nearest scrolling while preserving focus in that surface. It resolves false when the identity is absent from the supplied, expanded table model so the application can first load data, change its controlled page, or expand its group.",
55
55
  "Scrollable table regions become keyboard focusable and named only when overflow exists.",
56
56
  "Incremental loading, newly appended row counts, failures, and end-of-results transitions are announced through a polite status region without replacing visible state. In grouped tables, announcements name the independently loading group.",
57
- "Applications may pass their controlled literal search terms through highlightTerms after applying their own filtering. When TableSearch scopes the query, pass the same selected field identities through highlightFieldIds. A compound column maps its ordered headerSegments to primary, secondary-run, and tertiary-run data points; simple columns map their column ID to every owned text track. With no highlightFieldIds every text track remains eligible. Matching text is rendered with semantic mark elements, so the emphasis is identifiable independently of its faint-brand background and bold-brand foreground. Highlight markup preserves source order, the cell's complete accessible text, and does not create extra announcements.",
57
+ "Applications may pass their controlled literal search terms through highlightTerms after applying their own filtering. When DataSearch scopes the query, pass the same selected field identities through highlightFieldIds. A compound column maps its ordered segments to primary, secondary-run, and tertiary-run data points; simple columns map their column ID to every owned text track. With no highlightFieldIds every text track remains eligible. Matching text is rendered with semantic mark elements, so the emphasis is identifiable independently of its faint-brand background and bold-brand foreground. Highlight markup preserves source order, the cell's complete accessible text, and does not create extra announcements.",
58
58
  "Static and infinite tables may show an optional result summary footer when displayedCount and totalCount are finite. Virtual mode shows a total-only summary and ignores displayedCount. Pagination mode instead renders a named Pagination control with a page-size picker, vertical divider, and page position; range changes are announced independently with complete phrasing. Unmodified Left and Right Arrow request the adjacent page while focus is inside a paginated table or on a containing page scroller that owns it; editable fields, choice controls, sliders, open menus, and modified key combinations retain their own keyboard behavior. A table-owned footer slot can replace summary copy, while footer-leading and footer-trailing remain available in the same compact-bar anatomy. Nested dialog footers are ignored."
59
59
  ],
60
60
  "states": [
61
- "Columns, rows, groups, grouping, sorting, selected row IDs, collapsed group IDs, highlightTerms, highlightFieldIds, hiddenColumnIds, columnOrder, dataMode, pagination, displayedCount, totalCount, and incremental-loading state are controlled inputs. Events communicate intent and never mutate those inputs. The optional header is one composable table-local surface: the component owns its 48px chrome, 8px inset on every side, and sticky placement while the application owns the leading slot's internal layout, controls, labels, filtering behavior, and action handling. highlightTerms only marks matching table-owned text and never filters rows, changes query semantics, or highlights Tag labels. highlightFieldIds optionally narrows that marking to matching column IDs or compound header-segment sort keys. Opting into dataModeSwitcher and columnCustomizer adds their trailing Menu triggers and requires caption visibility. When both are present, Customize precedes a decorative divider and the data-mode trigger remains the final action. The Customize trigger keeps one stable label and neutral resting foreground whether or not hiddenColumnIds or columnOrder differ from the catalog default; the configured state remains visible inside the Menu. The table closes one trailing Menu before opening the other. The footer retains its compact-bar composition: application-owned leading status sits left while a controlled result summary or Pagination aligns right, before any trailing action. Content-sized tables remain natural for sparse or fixed-count results. height bounds the complete header, frame, and footer composition. fitViewport reserves the compact-state composition height in the nearest vertical scrollport while growing the visible table into that reservation as surrounding page chrome collapses.",
61
+ "Columns, rows, groups, grouping, sorting, selected row IDs, collapsed group IDs, highlightTerms, highlightFieldIds, hiddenFieldIds, fieldOrder, dataMode, pagination, displayedCount, totalCount, and incremental-loading state are controlled inputs. Events communicate intent and never mutate those inputs. The optional header is one composable table-local surface: the component owns its 48px chrome, 8px inset on every side, and sticky placement while the application owns the leading slot's internal layout, controls, labels, filtering behavior, and action handling. highlightTerms only marks matching table-owned text and never filters rows, changes query semantics, or highlights Tag labels. highlightFieldIds optionally narrows that marking to matching column IDs or compound header-segment sort keys. Opting into dataModeSwitcher and columnCustomizer adds their trailing Menu triggers and requires caption visibility. When both are present, Customize precedes a decorative divider and the data-mode trigger remains the final action. The Customize trigger keeps one stable label and neutral resting foreground whether or not hiddenFieldIds or fieldOrder differ from the catalog default; the configured state remains visible inside the Menu. The table closes one trailing Menu before opening the other. The footer retains its compact-bar composition: application-owned leading status sits left while a controlled result summary or Pagination aligns right, before any trailing action. Content-sized tables remain natural for sparse or fixed-count results. height bounds the complete header, frame, and footer composition. fitViewport reserves the compact-state composition height in the nearest vertical scrollport while growing the visible table into that reservation as surrounding page chrome collapses.",
62
62
  "Grouping supports one level. The application supplies groups in a fixed product-defined order and supplies ordered member rows independently. Group section headers show an emphasized text-body-medium label at the start and plain secondary loaded progress at the trailing edge beside the disclosure. A 20px tertiary divider centered on the label track marks the action-column boundary between the count and disclosure. The loaded value comes from the supplied member rows and the total comes from totalCount. A localized countLabel total phrase remains available for the complete assistive progress announcement. Virtual mode shows totals only, for example 166, because there is no loaded window. A collapsed section with no supplied member rows uses that same total-only copy so collapse does not advertise a zero loaded window, including 0 when no total is known. An expanded empty section still shows loaded progress, for example 0 of 166. Optional accessories occupy a second 24px track under the label, middle-dot separated, at most four items. An accessory with help uses the shared dotted underline and a bottom-start ds-tooltip from that copy; explicit line breaks create an unbulleted list. Without accessories the section stays the single-track row height; with accessories it matches a two-track body row, with the checkbox, trailing count, divider, and disclosure kept on the label track. An optional hero accessory sits before the label. The first supported hero is a safety-score fill. Its one-track presentation uses the default ds-score at sm and aligns to the label track on either row height. Setting hero tracks to 2 creates or spans the two-track header, renders the default ds-score at lg, and centers it across the label and accessory tracks. In selectable tables the two-track score starts at the selection-column boundary; selectionMode none omits that lane and uses the regular group-content start inset. A score hero does not use group intent coloring. Hero presentation does not change the 40 or 64 row-height contract beyond the explicitly requested second track. Groups without intent use the secondary background surface. Groups may declare an optional intent (brand, neutral, negative, warning, caution, positive) that paints an LTR gradient from the faint intent surface at the label edge into faint neutral at the trailing edge, plus a bold intent title color. Neutral groups remain solid faint neutral. Section separators are painted inside that viewport-width surface so translucent border tokens composite with each intent background. Internal sections keep their separators; a collapsed or empty section header yields its separator only when it is the table's final rendered item. The complete section header is the expand/collapse target and paints hover and press on that viewport-width surface. The section separator stays on the dedicated trailing-edge rule so the interaction wash cannot replace it. Trailing loaded progress sits on the label track beside a visually neutral icon-only ButtonUnfilled. The centered 20px tertiary divider at the action-column boundary separates the count from the 24px md double-inset disclosure. The disclosure keeps its accessible name and does not paint its own hover or press wash, showing ChevronUp while expanded and ChevronDown while collapsed. While any group is expanded, a ChevronDownUp action uses the xs sort-control recipe widened to a table-owned 24px lane and collapses every group. It appears at the inline-end edge of the trailing column declared with kind action when one exists; otherwise a button-hugging 24px by 16px medium-elevation surface floats at the visible scrollport's header edge so the action remains reachable while member columns scroll beneath it. With stickyHeader enabled, each contained section uses its one real section row as a native sticky header inside its real row-group boundary. The browser therefore owns pinning, push-off, scrolling, and resize layout in one pass without a duplicated lane, active-section state, transforms, or scroll-time geometry. Shared grid tracks retain exact header and member-cell alignment. The section surface also stays pinned to the visible horizontal scrollport and spans the smaller of the visible viewport or table width. Virtual grouped tables flatten headers and members into that same scrollport; expanding a section never introduces an inner scroller.",
63
- "When columnCustomizer is false, columns render in catalog order. When it is true, columns remains the catalog: hiddenColumnIds and columnOrder control visibility and data-column sequence, action columns stay last, cannot be hidden, and remain absent from the customizer Menu, and dsColumnsConfigChange reports the next hidden ids and data-column order without mutating those inputs. The customizer Menu live-applies show/hide and data-column reorder. The table does not persist preferences.",
63
+ "When columnCustomizer is false, columns render in catalog order. When it is true, columns remains the catalog: hiddenFieldIds and fieldOrder control visibility and data-column sequence, action columns stay last, cannot be hidden, and remain absent from the customizer Menu, and dsFieldsConfigChange reports the next hidden ids and data-column order without mutating those inputs. The customizer Menu live-applies show/hide and data-column reorder. The table does not persist preferences.",
64
64
  "An active member sort toggles only between ascending and descending; activating a different sortable label moves the one sorting state to that key. Applications may still clear the controlled sort state explicitly. Grouping does not introduce another interactive direction.",
65
65
  "Compound headers can expose multiple related labels with independent sort keys. Each label owns only its content-width interaction target while the active direction remains in the column's alignment-aware sort lane.",
66
66
  "Initial loading defaults to ten row-shaped skeletons and preserves row dividers, column dividers, selection lanes, and sticky boundaries. A selectable table renders a checkbox-sized control skeleton in its selection lane so loading geometry matches the real checkbox instead of a generic icon. Each column may declare representative text, multiline text, image, Tag, icon, icon-and-text, action, or intentionally blank loading geometry; the table applies the matching resolved-cell padding and track anatomy so the placeholder row has the same height and rhythm as its data row. Columns without a declaration use one text line, while action columns infer an icon action placeholder. chromeLoading replaces opted-in table-owned caption controls with non-interactive control skeletons in the same slots and at the same dimensions, without changing dataMode or the surrounding caption geometry. Empty and initial error replace only the body with ds-empty-state, using Inbox or ErrorTriangle plus the table heading and body copy. In height-bounded and maxHeight tables that region fills the remaining viewport below the column header; content-sized tables keep a minimum empty-region height. Empty, initial error, load-more, retry, loading-more, and terminal bands remain fixed to the visible table width while data columns scroll horizontally beneath them. Incremental loading retains existing rows and supports automatic sentinel, manual load-more, retry, and terminal states. When grouping is active, each expanded group owns its load band at the end of its member rows; internal group load bands keep their bottom divider, while a terminal group load band yields it to the table edge or footer. The global bottom-of-table loader is not rendered.",
@@ -73,7 +73,7 @@
73
73
  "The public @ds-mo/ui/table.css export is the authoritative zero-specificity visual recipe. Override its --ds-table-* properties or target its stable element classes for product-specific styling.",
74
74
  "While any checkbox selection is active, row activation toggles selection instead of opening details. Clearing the selection restores ordinary row activation. Nested controls retain their own intent, and disabled or non-selectable rows cannot be selected by activating their row.",
75
75
  "Use surfaceOpenRowId to identify the interactive row associated with a visible detail panel or other contextual surface. The application owns opening, same-row toggling, switching records, and closing; retain the identity through exit motion, then clear it. Move it when panel navigation changes the displayed record. This pressed wash spans sticky and scrolling cells, follows stable row identity through virtualization, and never checks a checkbox or changes selectedRowIds. Group expansion remains an inline disclosure.",
76
- "An external ds-table-preferences may own column controls. Hide only the built-in Customize trigger, keep column customization enabled, and share hidden-column and ordering state with the external surface.",
76
+ "An external ds-data-preferences may own column controls. Hide only the built-in Customize trigger, keep column customization enabled, and share hidden-column and ordering state with the external surface.",
77
77
  "Borderless toolbar compositions omit resting control borders while retaining focus and interaction feedback."
78
78
  ],
79
79
  "responsiveBehavior": [
@@ -89,12 +89,12 @@
89
89
  ],
90
90
  "frameworkCaveats": {
91
91
  "customElements": [
92
- "Assign columns, rows, groups, grouping, sort, selectedRowIds, collapsedGroupIds, highlightTerms, highlightFieldIds, hiddenColumnIds, columnOrder, pagination, displayedCount, and totalCount through JavaScript properties rather than HTML attribute strings.",
92
+ "Assign columns, rows, groups, grouping, sort, selectedRowIds, collapsedGroupIds, highlightTerms, highlightFieldIds, hiddenFieldIds, fieldOrder, pagination, displayedCount, and totalCount through JavaScript properties rather than HTML attribute strings.",
93
93
  "dataMode=virtual recycles row DOM over the supplied in-memory rows or groups and requires height, maxHeight, or fitViewport. It does not fetch or paginate.",
94
94
  "When an action cell supplies items, the table opens one shared ds-menu from the Ellipses trigger; dsCellAction fires for the selected command, not for opening or closing the menu.",
95
- "When columnCustomizer is enabled, the Table trigger opens the same shared ds-menu of reorderable switch rows; toggling a switch or reordering a row emits dsColumnsConfigChange and does not close the menu.",
95
+ "When columnCustomizer is enabled, the Table trigger opens the same shared ds-menu of reorderable switch rows; toggling a switch or reordering a row emits dsFieldsConfigChange and does not close the menu.",
96
96
  "Overflowing text tracks present one shared ds-tooltip with the omitted value. Unlimited wrapping tracks and disabled rows never show it. The tooltip is not linked with aria-describedby because the full string remains in the cell.",
97
- "Replace controlled property values in response to dsSortChange, dsGroupCollapseChange, dsSelectionChange, dsLoadMore, dsGroupLoadMore, dsPaginationChange, and dsColumnsConfigChange; route dsRowActivate in the application owner. When columnCustomizer is enabled, keep columns as the full catalog and replace hiddenColumnIds and columnOrder from that event."
97
+ "Replace controlled property values in response to dsSortChange, dsGroupCollapseChange, dsSelectionChange, dsLoadMore, dsGroupLoadMore, dsPaginationChange, and dsFieldsConfigChange; route dsRowActivate in the application owner. When columnCustomizer is enabled, keep columns as the full catalog and replace hiddenFieldIds and fieldOrder from that event."
98
98
  ],
99
99
  "react": [
100
100
  "Pass new array and state-object references when the controlled data or query state changes.",
@@ -178,14 +178,6 @@
178
178
  "mutable": false,
179
179
  "description": "Opt in to the table-owned column customizer. The `columns` prop remains the\ncatalog; hidden and ordered columns are controlled separately. The trigger\nopens the shared Menu of live show/hide switch rows."
180
180
  },
181
- "columnOrder": {
182
- "type": "string[]",
183
- "resolvedType": "string[]",
184
- "default": "[]",
185
- "required": false,
186
- "mutable": false,
187
- "description": "Controlled data-column identities in display order. Omitted ids append in catalog order."
188
- },
189
181
  "columns": {
190
182
  "type": "TableColumn[]",
191
183
  "resolvedType": "TableColumn[]",
@@ -301,6 +293,14 @@
301
293
  "required": false,
302
294
  "mutable": false
303
295
  },
296
+ "fieldOrder": {
297
+ "type": "string[]",
298
+ "resolvedType": "string[]",
299
+ "default": "[]",
300
+ "required": false,
301
+ "mutable": false,
302
+ "description": "Controlled data-column identities in display order. Omitted ids append in catalog order."
303
+ },
304
304
  "fitViewport": {
305
305
  "type": "boolean",
306
306
  "resolvedType": "boolean",
@@ -364,16 +364,16 @@
364
364
  "description": "Supports {group}, {count}, {loaded}, and {total} placeholders."
365
365
  },
366
366
  "grouping": {
367
- "type": "TableGroupingState | null",
368
- "resolvedType": "TableGroupingState | null",
367
+ "type": "DataGroupingState | null",
368
+ "resolvedType": "DataGroupingState | null",
369
369
  "default": "null",
370
370
  "required": false,
371
371
  "mutable": false,
372
372
  "description": "Controlled grouping column. Applications supply groups in their final fixed order."
373
373
  },
374
374
  "groups": {
375
- "type": "TableGroup[]",
376
- "resolvedType": "TableGroup[]",
375
+ "type": "DataGroup[]",
376
+ "resolvedType": "DataGroup[]",
377
377
  "default": "[]",
378
378
  "required": false,
379
379
  "mutable": false,
@@ -395,7 +395,7 @@
395
395
  "mutable": false,
396
396
  "description": "Fixed height for the complete header, table frame, and footer composition."
397
397
  },
398
- "hiddenColumnIds": {
398
+ "hiddenFieldIds": {
399
399
  "type": "string[]",
400
400
  "resolvedType": "string[]",
401
401
  "default": "[]",
@@ -417,7 +417,7 @@
417
417
  "default": "[]",
418
418
  "required": false,
419
419
  "mutable": false,
420
- "description": "Optional TableSearch field identities that restrict which data-point tracks are highlighted."
420
+ "description": "Optional DataSearch field identities that restrict which data-point tracks are highlighted."
421
421
  },
422
422
  "highlightTerms": {
423
423
  "type": "string[]",
@@ -602,8 +602,8 @@
602
602
  "description": "Initial-loading rows. Defaults to ten so bounded tables retain a useful filled viewport."
603
603
  },
604
604
  "sort": {
605
- "type": "TableSortState | null",
606
- "resolvedType": "TableSortState | null",
605
+ "type": "DataSortState | null",
606
+ "resolvedType": "DataSortState | null",
607
607
  "default": "null",
608
608
  "required": false,
609
609
  "mutable": false,
@@ -668,22 +668,22 @@
668
668
  "composed": true
669
669
  },
670
670
  {
671
- "name": "dsColumnsConfigChange",
672
- "detail": "TableColumnsConfigChangeDetail",
671
+ "name": "dsCustomizeOptionChange",
672
+ "detail": "string",
673
673
  "bubbles": true,
674
674
  "cancelable": true,
675
675
  "composed": true
676
676
  },
677
677
  {
678
- "name": "dsCustomizeOptionChange",
679
- "detail": "string",
678
+ "name": "dsDataModeChange",
679
+ "detail": "TableDataModeChangeDetail",
680
680
  "bubbles": true,
681
681
  "cancelable": true,
682
682
  "composed": true
683
683
  },
684
684
  {
685
- "name": "dsDataModeChange",
686
- "detail": "TableDataModeChangeDetail",
685
+ "name": "dsFieldsConfigChange",
686
+ "detail": "DataFieldsConfigChangeDetail",
687
687
  "bubbles": true,
688
688
  "cancelable": true,
689
689
  "composed": true
@@ -732,7 +732,7 @@
732
732
  },
733
733
  {
734
734
  "name": "dsSortChange",
735
- "detail": "TableSortChangeDetail",
735
+ "detail": "DataSortChangeDetail",
736
736
  "bubbles": true,
737
737
  "cancelable": true,
738
738
  "composed": true
@@ -800,14 +800,6 @@
800
800
  "mutable": false,
801
801
  "description": "Opt in to the table-owned column customizer. The `columns` prop remains the\ncatalog; hidden and ordered columns are controlled separately. The trigger\nopens the shared Menu of live show/hide switch rows."
802
802
  },
803
- "columnOrder": {
804
- "type": "string[]",
805
- "resolvedType": "string[]",
806
- "default": "[]",
807
- "required": false,
808
- "mutable": false,
809
- "description": "Controlled data-column identities in display order. Omitted ids append in catalog order."
810
- },
811
803
  "columns": {
812
804
  "type": "TableColumn[]",
813
805
  "resolvedType": "TableColumn[]",
@@ -923,6 +915,14 @@
923
915
  "required": false,
924
916
  "mutable": false
925
917
  },
918
+ "fieldOrder": {
919
+ "type": "string[]",
920
+ "resolvedType": "string[]",
921
+ "default": "[]",
922
+ "required": false,
923
+ "mutable": false,
924
+ "description": "Controlled data-column identities in display order. Omitted ids append in catalog order."
925
+ },
926
926
  "fitViewport": {
927
927
  "type": "boolean",
928
928
  "resolvedType": "boolean",
@@ -986,16 +986,16 @@
986
986
  "description": "Supports {group}, {count}, {loaded}, and {total} placeholders."
987
987
  },
988
988
  "grouping": {
989
- "type": "TableGroupingState | null",
990
- "resolvedType": "TableGroupingState | null",
989
+ "type": "DataGroupingState | null",
990
+ "resolvedType": "DataGroupingState | null",
991
991
  "default": "null",
992
992
  "required": false,
993
993
  "mutable": false,
994
994
  "description": "Controlled grouping column. Applications supply groups in their final fixed order."
995
995
  },
996
996
  "groups": {
997
- "type": "TableGroup[]",
998
- "resolvedType": "TableGroup[]",
997
+ "type": "DataGroup[]",
998
+ "resolvedType": "DataGroup[]",
999
999
  "default": "[]",
1000
1000
  "required": false,
1001
1001
  "mutable": false,
@@ -1017,7 +1017,7 @@
1017
1017
  "mutable": false,
1018
1018
  "description": "Fixed height for the complete header, table frame, and footer composition."
1019
1019
  },
1020
- "hiddenColumnIds": {
1020
+ "hiddenFieldIds": {
1021
1021
  "type": "string[]",
1022
1022
  "resolvedType": "string[]",
1023
1023
  "default": "[]",
@@ -1039,7 +1039,7 @@
1039
1039
  "default": "[]",
1040
1040
  "required": false,
1041
1041
  "mutable": false,
1042
- "description": "Optional TableSearch field identities that restrict which data-point tracks are highlighted."
1042
+ "description": "Optional DataSearch field identities that restrict which data-point tracks are highlighted."
1043
1043
  },
1044
1044
  "highlightTerms": {
1045
1045
  "type": "string[]",
@@ -1224,8 +1224,8 @@
1224
1224
  "description": "Initial-loading rows. Defaults to ten so bounded tables retain a useful filled viewport."
1225
1225
  },
1226
1226
  "sort": {
1227
- "type": "TableSortState | null",
1228
- "resolvedType": "TableSortState | null",
1227
+ "type": "DataSortState | null",
1228
+ "resolvedType": "DataSortState | null",
1229
1229
  "default": "null",
1230
1230
  "required": false,
1231
1231
  "mutable": false,
@@ -1290,22 +1290,22 @@
1290
1290
  "composed": true
1291
1291
  },
1292
1292
  {
1293
- "name": "dsColumnsConfigChange",
1294
- "detail": "TableColumnsConfigChangeDetail",
1293
+ "name": "dsCustomizeOptionChange",
1294
+ "detail": "string",
1295
1295
  "bubbles": true,
1296
1296
  "cancelable": true,
1297
1297
  "composed": true
1298
1298
  },
1299
1299
  {
1300
- "name": "dsCustomizeOptionChange",
1301
- "detail": "string",
1300
+ "name": "dsDataModeChange",
1301
+ "detail": "TableDataModeChangeDetail",
1302
1302
  "bubbles": true,
1303
1303
  "cancelable": true,
1304
1304
  "composed": true
1305
1305
  },
1306
1306
  {
1307
- "name": "dsDataModeChange",
1308
- "detail": "TableDataModeChangeDetail",
1307
+ "name": "dsFieldsConfigChange",
1308
+ "detail": "DataFieldsConfigChangeDetail",
1309
1309
  "bubbles": true,
1310
1310
  "cancelable": true,
1311
1311
  "composed": true
@@ -1354,7 +1354,7 @@
1354
1354
  },
1355
1355
  {
1356
1356
  "name": "dsSortChange",
1357
- "detail": "TableSortChangeDetail",
1357
+ "detail": "DataSortChangeDetail",
1358
1358
  "bubbles": true,
1359
1359
  "cancelable": true,
1360
1360
  "composed": true
@@ -1393,11 +1393,11 @@
1393
1393
  "import": "import { DsTable } from '@ds-mo/ui/angular/ds-table';",
1394
1394
  "example": "<ds-table></ds-table>"
1395
1395
  },
1396
- "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: collapsedGroupIds, columnOrder, columns, customizeOptions, grouping, groups, hiddenColumnIds, highlightFieldIds, highlightTerms, pagination, rows, selectedRowIds, sort.",
1396
+ "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: collapsedGroupIds, columns, customizeOptions, fieldOrder, grouping, groups, hiddenFieldIds, highlightFieldIds, highlightTerms, pagination, rows, selectedRowIds, sort.",
1397
1397
  "peerDependencies": {
1398
1398
  "required": [
1399
1399
  "@ds-mo/tokens ^6.5.1",
1400
- "@ds-mo/icons ^7.0.0"
1400
+ "@ds-mo/icons ^8.1.0"
1401
1401
  ],
1402
1402
  "frameworks": "Custom Elements; React 18/19 wrappers; Vue 3 wrappers; Angular 19-22 standalone adapters."
1403
1403
  }
@@ -1431,17 +1431,17 @@
1431
1431
  },
1432
1432
  {
1433
1433
  "path": "src/wc/components/Table/Table.tsx",
1434
- "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Listen,\n Method,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport {\n deriveTableSelectionState,\n formatTableResultSummary,\n formatTableTotalSummary,\n hasOwnedTableFooterSlot,\n isTableCellAction,\n nextTableSortState,\n tableColumnSize,\n tableModelIssues,\n TABLE_GROUP_HERO_SCORE_PRESENTATION,\n tableRows,\n toggleAllLoadedTableRows,\n toggleTableGroupCollapsed,\n toggleTableGroupSelection,\n nextTableGroupsCollapsed,\n toggleTableRowSelection,\n} from './table-model';\nimport { createTableRenderModel, type TableRenderModel } from './table-render-model';\nimport {\n isRenderableTableActionMenu,\n nextTableActionMenuElementId,\n tableActionMenuSections,\n tableActionTriggerId,\n} from './table-action-menu';\nimport {\n nextTableColumnCustomizerElementId,\n resolveTableColumnOrder,\n resolveTableHiddenColumnIds,\n resolveTableVisibleColumns,\n tableColumnCustomizerMenuItems,\n toggleTableColumnHidden,\n} from './table-column-customizer';\nimport {\n nextTableDataModeSwitcherElementId,\n tableDataModeFromMenuItem,\n tableDataModeMenuItems,\n} from './table-data-mode-switcher';\nimport { resolveTableTruncateTrack, tableTruncateLabel } from './table-truncate';\nimport { renderTableRow as renderTableRowView } from './table-row-view';\nimport { TableBodyRenderer } from './table-body-renderer';\nimport { renderTableSkeletonBody } from './table-skeleton-view';\nimport { renderTableLoadContent } from './table-load-view';\nimport { TableLayoutController } from './table-layout-controller';\nimport { TableLoadController } from './table-load-controller';\nimport { TableGroupLoadController } from './table-group-load-controller';\nimport {\n estimateTableRowBlockSize,\n flattenTableVirtualItems,\n resolveTableVirtualPlan,\n TABLE_VIRTUAL_TOTAL_SUMMARY_LABEL,\n TABLE_VIRTUAL_VIEWPORT_REQUIRED_BODY,\n TABLE_VIRTUAL_VIEWPORT_REQUIRED_HEADING,\n type TableVirtualItem,\n type TableVirtualPlan,\n} from './table-virtual-model';\nimport { TableVirtualController } from './table-virtual-controller';\nimport type { PaginationChangeDetail } from '../Pagination/pagination-types';\nimport type { MenuItemData, MenuReorderDetail } from '../Menu/menu-types';\nimport { resolvePaginationState } from '../Pagination/pagination-model';\nimport { resolveCssLengthPx } from '../../utils/resolve-css-length-px';\nimport { isElementTruncated } from '../../utils/is-element-truncated';\nimport { observeTableCaptionCompact } from '../../utils/table-caption-compact';\nimport { resolveTableFitPageSize } from './table-pagination-fit';\nimport { createTableHighlightMatcher, type TableHighlightMatcher } from './table-highlight';\nimport {\n paginationShortcutBlockedByPath,\n shouldHandleContainingPagePaginationShortcut,\n} from './table-pagination-shortcut';\nimport {\n TableViewportFitController,\n type TableViewportFitMetrics,\n} from './table-viewport-fit-controller';\nimport type {\n TableCaptionVisibility,\n TableCellActionDetail,\n TableColumn,\n TableColumnsConfigChangeDetail,\n TableDataMode,\n TableDataModeChangeDetail,\n TableGroup,\n TableGroupCollapseChangeDetail,\n TableGroupLoadMoreDetail,\n TableGroupingState,\n TableLoadMoreDetail,\n TableLoadMoreMode,\n TablePaginationState,\n TableRow,\n TableRowActivateDetail,\n TableSelectionChangeDetail,\n TableSelectionMode,\n TableSortChangeDetail,\n TableSortState,\n} from './table-types';\n\nconst TABLE_FOOTER_SLOT_LEADING = 1;\nconst TABLE_FOOTER_SLOT_COPY = 2;\nconst TABLE_FOOTER_SLOT_TRAILING = 4;\nconst TABLE_NON_VIRTUAL_WINDOW_THRESHOLD = 50;\n\n@Component({\n tag: 'ds-table',\n styleUrls: ['../../utils/focus-ring.css', '../../utils/interaction-fill.css', 'Table.css'],\n scoped: true,\n})\nexport class Table {\n @Element() el!: HTMLElement;\n /** Stable column definitions. Assign through JavaScript. */\n @Prop() columns: TableColumn[] = [];\n /** Additional toggle options in the column customizer. */\n @Prop() customizeOptions: MenuItemData[] = [];\n @Event() dsCustomizeOptionChange!: EventEmitter<string>;\n /** Ungrouped row data. Ignored while grouping is active. Assign through JavaScript. */\n @Prop() rows: TableRow[] = [];\n /** One level of application-owned grouped data. Assign through JavaScript. */\n @Prop() groups: TableGroup[] = [];\n /** Controlled grouping column. Applications supply groups in their final fixed order. */\n @Prop() grouping: TableGroupingState | null = null;\n /** Controlled member-row sort state. */\n @Prop() sort: TableSortState | null = null;\n /** Controlled collapsed group identities. Groups not listed remain expanded. */\n @Prop() collapsedGroupIds: string[] = [];\n /** Literal terms to highlight in table-owned text cells. Applications still own filtering. */\n @Prop() highlightTerms: string[] = [];\n /** Optional TableSearch field identities that restrict which data-point tracks are highlighted. */\n @Prop() highlightFieldIds: string[] = [];\n\n /** Required accessible table name, retained as a native caption. */\n @Prop() caption!: string;\n /** Shows a matching presentational title bar above the native table frame. */\n @Prop() captionVisibility: TableCaptionVisibility = 'hidden';\n /**\n * Opt in to the table-owned column customizer. The `columns` prop remains the\n * catalog; hidden and ordered columns are controlled separately. The trigger\n * opens the shared Menu of live show/hide switch rows.\n */\n @Prop() columnCustomizer: boolean = false;\n /** Hide only the built-in Customize trigger when an external preferences surface owns it. */\n /** Remove borders from table-owned caption controls. */\n @Prop() captionControlsBorderless: boolean = false;\n @Prop() hideColumnCustomizerTrigger: boolean = false;\n /** Controlled hidden data-column identities. Action ids are ignored. */\n @Prop() hiddenColumnIds: string[] = [];\n /** Controlled data-column identities in display order. Omitted ids append in catalog order. */\n @Prop() columnOrder: string[] = [];\n /**\n * Optional result summary footer. When both `displayedCount` and `totalCount`\n * are finite numbers, infinite mode shows “Displaying {displayed} of {total}”.\n * Virtual mode ignores `displayedCount` and derives its total from the\n * complete supplied rows; a mismatched `totalCount` emits a warning.\n */\n @Prop() displayedCount: number | undefined;\n @Prop() totalCount: number | undefined;\n /** Supports {displayed} and {total} placeholders. */\n @Prop() resultSummaryLabel: string = 'Displaying {displayed} of {total}';\n /** Supports the {total} placeholder. Used when dataMode is virtual. */\n @Prop() resultTotalSummaryLabel: string = TABLE_VIRTUAL_TOTAL_SUMMARY_LABEL;\n @Prop() stickyHeader: boolean = false;\n /** Maximum scroll-region height. Numbers resolve to CSS pixels. */\n @Prop() maxHeight: string | number | undefined;\n /** Fixed height for the complete header, table frame, and footer composition. */\n @Prop() height: string | number | undefined;\n /** Fit the complete table composition to its nearest vertical scrollport. */\n @Prop() fitViewport: boolean = false;\n /** Reserved space above a viewport-fitted table once surrounding chrome is compact. */\n @Prop() viewportInsetBlockStart: string | number = 0;\n /** Reserved space below a viewport-fitted table. */\n @Prop() viewportInsetBlockEnd: string | number = 0;\n /** Optional explicit label for the horizontal/vertical scroll region. */\n @Prop() scrollLabel: string | undefined;\n\n @Prop() selectionMode: TableSelectionMode = 'none';\n /** Controlled selected row identities. IDs outside the loaded rows are preserved. */\n @Prop() selectedRowIds: string[] = [];\n\n /**\n * Identity of the interactive row whose associated surface is visible.\n * Holds its pressed wash independently of checkbox selection. Keep the ID\n * through exit motion and clear it after close; the application owns toggling\n * and switching the surface. Off-window identities survive virtualization.\n */\n @Prop() surfaceOpenRowId: string | undefined;\n\n /** Initial loading state. Existing rows stay visible; incremental loading uses loadingMore. */\n @Prop() loading: boolean = false;\n /** Replace opted-in table-owned caption controls with same-size visual skeletons. */\n @Prop() chromeLoading: boolean = false;\n /** Initial-loading rows. Defaults to ten so bounded tables retain a useful filled viewport. */\n @Prop() skeletonRows: number = 10;\n @Prop() emptyHeading: string = 'No results';\n @Prop() emptyBody: string = 'No data is available.';\n /** Initial error state. Existing rows stay visible; incremental failures use loadMoreError. */\n @Prop() error: boolean = false;\n @Prop() errorHeading: string = 'Unable to load data';\n @Prop() errorBody: string = 'The data could not be loaded.';\n @Prop() emptyCellLabel: string = 'Not available';\n\n /** Top-level data-window strategy. Virtual mode recycles row DOM only. */\n @Prop() dataMode: TableDataMode = 'infinite';\n /** Opt in to the table-owned control for choosing between supported data modes. */\n @Prop() dataModeSwitcher: boolean = false;\n @Prop() dataModeSwitcherLabel: string = 'Change table variation';\n @Prop() dataModeMenuLabel: string = 'Table variation';\n @Prop() infiniteModeLabel: string = 'Infinite scroll';\n @Prop() paginationModeLabel: string = 'Pagination + Infinite groups';\n @Prop() virtualModeLabel: string = 'Virtual scroll';\n /** Controlled top-level pagination state. Required when dataMode is pagination. */\n @Prop() pagination: TablePaginationState | null = null;\n @Prop() loadMoreMode: TableLoadMoreMode = 'auto';\n @Prop() hasMore: boolean = false;\n @Prop() loadingMore: boolean = false;\n @Prop() loadMoreError: string | undefined;\n /** Reset key for a new query/group/sort dataset. */\n @Prop() loadIdentity: string | number = 'default';\n /** IntersectionObserver root margin in CSS pixels. */\n @Prop() loadMoreThreshold: number = 0;\n @Prop() loadMoreLabel: string = 'Load more';\n @Prop() retryLabel: string = 'Retry';\n @Prop() loadingMoreLabel: string = 'Loading more items';\n @Prop() endOfResultsLabel: string = 'All results loaded';\n /** Supports {count} and {total} placeholders. */\n @Prop() rowsLoadedLabel: string = '{count} more rows loaded. {total} rows loaded.';\n @Prop() groupLoadMoreLabel: string = 'Load more';\n /** Supports the {group} placeholder. */\n @Prop() groupLoadMoreAriaLabel: string = 'Load more {group} results';\n /** Supports the {group} placeholder. */\n @Prop() groupRetryLabel: string = 'Retry loading {group} results';\n /** Supports the {group} placeholder. */\n @Prop() groupLoadingMoreLabel: string = 'Loading more items';\n /** Supports the {group} placeholder. */\n @Prop() groupEndOfResultsLabel: string = 'All {group} results loaded';\n /** Supports {group}, {count}, {loaded}, and {total} placeholders. */\n @Prop() groupRowsLoadedLabel: string =\n '{count} more rows loaded in {group}. {loaded} of {total} rows loaded.';\n\n @Event() dsSortChange!: EventEmitter<TableSortChangeDetail>;\n @Event() dsGroupCollapseChange!: EventEmitter<TableGroupCollapseChangeDetail>;\n @Event() dsSelectionChange!: EventEmitter<TableSelectionChangeDetail>;\n @Event() dsLoadMore!: EventEmitter<TableLoadMoreDetail>;\n @Event() dsGroupLoadMore!: EventEmitter<TableGroupLoadMoreDetail>;\n @Event() dsPaginationChange!: EventEmitter<PaginationChangeDetail>;\n @Event() dsCellAction!: EventEmitter<TableCellActionDetail>;\n @Event() dsRowActivate!: EventEmitter<TableRowActivateDetail>;\n @Event() dsColumnsConfigChange!: EventEmitter<TableColumnsConfigChangeDetail>;\n @Event() dsDataModeChange!: EventEmitter<TableDataModeChangeDetail>;\n\n @State() private overflowStart = false;\n @State() private overflowEnd = false;\n @State() private scrollable = false;\n @State() private announcement = '';\n @State() private activeStickyGroupId: string | null = null;\n @State() private viewportFitSettled = false;\n @State() private headerPresent = false;\n @State() private headerUsesToolbar = false;\n @State() private footerSlotPresence = 0;\n @State() private fitPageSize: number | undefined;\n @State() private actionMenu: { rowId: string; columnId: string } | null = null;\n @State() private actionMenuInitialFocusVisible = false;\n @State() private actionMenuSurface: { rowId: string; columnId: string } | null = null;\n @State() private truncateTooltipLabel = '';\n @State() private captionCompact = false;\n @State() private columnCustomizerOpen = false;\n @State() private columnCustomizerSurfaceOpen = false;\n @State() private columnCustomizerInitialFocusVisible = false;\n @State() private dataModeSwitcherOpen = false;\n @State() private dataModeSwitcherSurfaceOpen = false;\n @State() private dataModeSwitcherInitialFocusVisible = false;\n @State() private virtualWindow: TableVirtualPlan | null = null;\n\n private readonly actionMenuElementId = nextTableActionMenuElementId();\n private readonly columnCustomizerElementId = nextTableColumnCustomizerElementId();\n private readonly dataModeSwitcherElementId = nextTableDataModeSwitcherElementId();\n private truncateTooltipEl?: HTMLDsTooltipElement;\n private truncateAnchor: HTMLElement | null = null;\n private truncateTooltipBound = false;\n private focusedRowId: string | null = null;\n private virtualItems: TableVirtualItem[] = [];\n private visibleColumnsCache: {\n columns: TableColumn[];\n columnCustomizer: boolean;\n hiddenColumnIds: string[];\n columnOrder: string[];\n value: TableColumn[];\n } | null = null;\n private renderModelCache: {\n columns: TableColumn[];\n rows: TableRow[];\n groups: TableGroup[];\n grouped: boolean;\n selectionMode: TableSelectionMode;\n selectedRowIds: string[];\n collapsedGroupIds: string[];\n virtualCounts: boolean;\n value: TableRenderModel;\n } | null = null;\n private highlightMatcherCache: {\n terms: string[];\n value: TableHighlightMatcher;\n } | null = null;\n private virtualItemsCache: {\n columns: TableColumn[];\n rows: TableRow[];\n groups: TableGroup[];\n grouped: boolean;\n collapsedGroupIds: string[];\n } | null = null;\n private rootEl: HTMLElement | null = null;\n private viewportEl: HTMLElement | null = null;\n private frameEl: HTMLElement | null = null;\n private interactiveHeadEl: HTMLTableSectionElement | null = null;\n private collapseAllOverlayEl: HTMLElement | null = null;\n private stickyHeaderTableEl: HTMLTableElement | null = null;\n private tableEl: HTMLTableElement | null = null;\n private sentinelEl: HTMLElement | null = null;\n private incrementalWindowActive = false;\n private readonly groupSentinelEls = new Map<string, HTMLElement>();\n private previousModelWarning = '';\n private modelWarningQueued = false;\n private initialModelWarningFrame: number | undefined;\n private hasLoaded = false;\n private renderedModel: TableRenderModel | null = null;\n private stickyGroupConnected = false;\n private headerSlotObserver: MutationObserver | null = null;\n private footerSlotObserver: MutationObserver | null = null;\n private captionCompactDisconnect: (() => void) | undefined;\n private fitResizeObserver: ResizeObserver | null = null;\n private fitObservedViewport: HTMLElement | null = null;\n private fitObservedTable: HTMLTableElement | null = null;\n private fitMeasurementPending = false;\n private readonly bodyRenderer = new TableBodyRenderer();\n private readonly layoutController = new TableLayoutController({\n elements: () => ({\n viewport: this.viewportEl,\n contentTable: this.tableEl,\n stickyHeaderTable: this.stickyHeaderTableEl,\n collapseAllOverlay: this.collapseAllOverlayEl,\n frame: this.frameEl,\n interactiveHead: this.interactiveHeadEl,\n }),\n mode: () => ({\n documentStickyHeader: this.documentStickyHeader,\n floatingCollapseAll: this.renderedModel?.collapseAllHost?.mode === 'floating',\n clampVerticalOverscroll: this.fitViewport && this.viewportFitSettled,\n }),\n verticalEdgeWheel: deltaY => this.viewportFitController.scrollOuterBy(deltaY),\n overflowChanged: state => {\n if (state.start !== this.overflowStart) this.overflowStart = state.start;\n if (state.end !== this.overflowEnd) this.overflowEnd = state.end;\n if (state.scrollable !== this.scrollable) this.scrollable = state.scrollable;\n },\n });\n private readonly loadController = new TableLoadController({\n state: () => ({\n lazyLoading: this.dataMode === 'infinite' && !this.grouped,\n loadMoreMode: this.loadMoreMode,\n hasMore: this.hasMore,\n loadingMore: this.loadingMore,\n loadMoreError: this.loadMoreError,\n loadIdentity: this.loadIdentity,\n loadMoreThreshold: this.loadMoreThreshold,\n containedScroll: this.containedScroll,\n loadingMoreLabel: this.loadingMoreLabel,\n endOfResultsLabel: this.endOfResultsLabel,\n rowsLoadedLabel: this.rowsLoadedLabel,\n loadedRowCount: this.currentLoadedRowCount(),\n viewport: this.viewportEl,\n sentinel: this.sentinelEl,\n }),\n announce: message => {\n this.announcement = message;\n },\n request: detail => {\n this.dsLoadMore.emit(detail);\n },\n });\n private readonly groupLoadController = new TableGroupLoadController({\n state: () => ({\n enabled: this.grouped && this.dataMode !== 'virtual',\n loadMoreMode: this.loadMoreMode,\n loadMoreThreshold: this.loadMoreThreshold,\n containedScroll: this.containedScroll,\n groups: this.groups,\n viewport: this.viewportEl,\n sentinels: this.groupSentinelEls,\n loadingMoreLabel: this.groupLoadingMoreLabel,\n endOfResultsLabel: this.groupEndOfResultsLabel,\n rowsLoadedLabel: this.groupRowsLoadedLabel,\n }),\n announce: message => {\n this.announcement = message;\n },\n request: detail => {\n this.dsGroupLoadMore.emit(detail);\n },\n });\n private readonly virtualController = new TableVirtualController({\n state: () => ({\n enabled: this.rowWindowingEnabled,\n items: this.virtualItems,\n pinnedRowIds: this.virtualPinnedRowIds(),\n viewport: this.viewportEl,\n viewportSize: this.estimateVirtualViewportSize(),\n }),\n windowChanged: plan => {\n this.virtualWindow = plan;\n },\n });\n private readonly viewportFitController = new TableViewportFitController({\n enabled: () => this.fitViewport,\n elements: () => ({ host: this.el, surface: this.rootEl }),\n insets: () => ({\n blockStart: this.viewportInsetBlockStart,\n blockEnd: this.viewportInsetBlockEnd,\n }),\n fitChanged: (metrics: TableViewportFitMetrics | null) => {\n const settled = metrics?.settled ?? false;\n if (settled !== this.viewportFitSettled) this.viewportFitSettled = settled;\n },\n });\n\n componentWillLoad(): void {\n // Reserve the final viewport-fit height before the first paint. The fitted\n // surface is connected after render, but the host can already resolve its\n // owning scrollport and prevent an initial auto-height frame.\n this.viewportFitController.connect();\n this.incrementalWindowActive = this.hasIncrementalState;\n this.syncHeaderSlotPresence();\n this.syncFooterSlotPresence();\n this.loadController.initialize();\n this.groupLoadController.initialize();\n }\n\n componentDidLoad(): void {\n this.hasLoaded = true;\n this.layoutController.connect();\n this.loadController.connect();\n this.groupLoadController.connect();\n this.virtualController.connect();\n this.viewportFitController.connect();\n this.syncStickyGroupConnection();\n this.connectHeaderSlotObserver();\n this.connectFooterSlotObserver();\n this.connectFitObserver();\n this.connectCaptionCompactObserver();\n this.syncFitPageSize();\n this.connectTruncateTooltip();\n this.scheduleInitialModelIssueWarning();\n }\n\n componentDidRender(): void {\n this.layoutController.refresh(false);\n this.loadController.refresh();\n this.groupLoadController.refresh();\n this.virtualController.refresh();\n this.virtualController.collectMeasurements(this.tableEl);\n if (\n this.rowWindowingEnabled &&\n this.truncateAnchor &&\n this.tableEl &&\n !this.tableEl.contains(this.truncateAnchor)\n ) {\n this.dismissTruncateTooltip();\n }\n this.viewportFitController.refresh(false);\n this.syncStickyGroupConnection();\n this.connectFitObserver();\n this.connectCaptionCompactObserver();\n this.syncFitPageSize();\n if (this.stickyGroupConnected) this.updateStickyGroup();\n }\n\n /**\n * Scroll the nearest vertical owner only enough to reveal a supplied row.\n * Resolves false when the row is absent from the supplied, expanded table model.\n * This method never moves focus.\n */\n @Method()\n async scrollRowIntoView(rowId: string): Promise<boolean> {\n if (!rowId) return false;\n\n if (this.rowWindowingEnabled) {\n const headerSize = this.tableEl?.tHead?.getBoundingClientRect().height ?? 0;\n const revealed = this.virtualController.scrollRowIntoView(\n rowId,\n headerSize,\n this.stickyHeader ? headerSize : 0\n );\n if (!revealed) return false;\n await this.nextAnimationFrame();\n }\n\n const row = this.findRenderedRow(rowId);\n if (!row) return false;\n this.revealRenderedRow(row);\n return true;\n }\n\n connectedCallback(): void {\n if (!this.hasLoaded) return;\n this.layoutController.connect();\n this.loadController.connect();\n this.groupLoadController.connect();\n this.virtualController.connect();\n this.viewportFitController.connect();\n this.syncStickyGroupConnection();\n this.connectHeaderSlotObserver();\n this.connectFooterSlotObserver();\n this.connectFitObserver();\n this.connectCaptionCompactObserver();\n this.connectTruncateTooltip();\n }\n\n disconnectedCallback(): void {\n this.layoutController.disconnect();\n this.loadController.disconnect();\n this.groupLoadController.disconnect();\n this.virtualController.disconnect();\n this.viewportFitController.disconnect();\n this.disconnectStickyGroup();\n this.headerSlotObserver?.disconnect();\n this.headerSlotObserver = null;\n this.footerSlotObserver?.disconnect();\n this.footerSlotObserver = null;\n this.disconnectFitObserver();\n this.disconnectCaptionCompactObserver();\n this.disconnectTruncateTooltip();\n this.closeColumnCustomizer();\n if (this.initialModelWarningFrame !== undefined) {\n cancelAnimationFrame(this.initialModelWarningFrame);\n this.initialModelWarningFrame = undefined;\n }\n }\n\n private syncHeaderSlotPresence = () => {\n const header = this.el.querySelector<HTMLElement>('[slot=\"header\"]');\n this.headerPresent = !!header;\n this.headerUsesToolbar = header?.tagName === 'DS-TABLE-TOOLBAR';\n };\n\n private connectHeaderSlotObserver(): void {\n if (this.headerSlotObserver || typeof MutationObserver === 'undefined') return;\n this.headerSlotObserver = new MutationObserver(this.syncHeaderSlotPresence);\n this.headerSlotObserver.observe(this.el, { childList: true });\n this.syncHeaderSlotPresence();\n }\n\n private syncFooterSlotPresence = () => {\n let presence = 0;\n if (hasOwnedTableFooterSlot(this.el, 'footer-leading')) presence |= TABLE_FOOTER_SLOT_LEADING;\n if (hasOwnedTableFooterSlot(this.el, 'footer')) presence |= TABLE_FOOTER_SLOT_COPY;\n if (hasOwnedTableFooterSlot(this.el, 'footer-trailing')) presence |= TABLE_FOOTER_SLOT_TRAILING;\n if (presence !== this.footerSlotPresence) this.footerSlotPresence = presence;\n };\n\n private connectFooterSlotObserver(): void {\n if (this.footerSlotObserver || typeof MutationObserver === 'undefined') return;\n this.footerSlotObserver = new MutationObserver(this.syncFooterSlotPresence);\n this.footerSlotObserver.observe(this.el, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['slot'],\n });\n this.syncFooterSlotPresence();\n }\n\n private connectStickyGroup(): void {\n if (this.stickyGroupConnected || typeof window === 'undefined') return;\n this.stickyGroupConnected = true;\n document.addEventListener('scroll', this.updateStickyGroup, {\n capture: true,\n passive: true,\n });\n window.addEventListener('resize', this.updateStickyGroup, { passive: true });\n this.updateStickyGroup();\n }\n\n private syncStickyGroupConnection(): void {\n if (this.documentStickyHeader && this.grouped) {\n this.connectStickyGroup();\n return;\n }\n\n this.disconnectStickyGroup();\n this.frameEl?.style.removeProperty('--_table-sticky-group-top');\n if (this.activeStickyGroupId !== null) {\n this.activeStickyGroupId = null;\n }\n }\n\n private disconnectStickyGroup(): void {\n if (!this.stickyGroupConnected || typeof window === 'undefined') return;\n this.stickyGroupConnected = false;\n document.removeEventListener('scroll', this.updateStickyGroup, true);\n window.removeEventListener('resize', this.updateStickyGroup);\n }\n\n private readonly updateStickyGroup = (): void => {\n if (!this.documentStickyHeader || !this.grouped || !this.frameEl || !this.tableEl) {\n if (this.activeStickyGroupId !== null) this.activeStickyGroupId = null;\n return;\n }\n\n const stickyHeader = this.frameEl.querySelector<HTMLElement>(\n '.ds-table__document-sticky-header'\n );\n if (!stickyHeader) return;\n const threshold = stickyHeader.getBoundingClientRect().bottom;\n let activeGroupId: string | null = null;\n let activeGroupTop = threshold;\n for (const body of this.tableEl.querySelectorAll<HTMLElement>('tbody[data-group-id]')) {\n const source = body.querySelector<HTMLElement>('.ds-table__group-cell');\n if (!source) continue;\n const sourceRect = source.getBoundingClientRect();\n const bodyRect = body.getBoundingClientRect();\n if (sourceRect.top <= threshold && bodyRect.bottom > threshold) {\n activeGroupId = body.dataset.groupId ?? null;\n // A sticky section is bounded by its own row group. As that boundary\n // approaches the sticky lane, move the outgoing section bar with it so\n // the incoming source bar pushes it away instead of rendering beneath it.\n activeGroupTop = Math.min(threshold, bodyRect.bottom - sourceRect.height);\n }\n }\n\n const frameRect = this.frameEl.getBoundingClientRect();\n this.frameEl.style.setProperty(\n '--_table-sticky-group-top',\n `${activeGroupTop - frameRect.top}px`\n );\n\n if (activeGroupId !== this.activeStickyGroupId) {\n this.activeStickyGroupId = activeGroupId;\n return;\n }\n };\n\n @Watch('columns')\n @Watch('grouping')\n @Watch('hiddenColumnIds')\n @Watch('columnOrder')\n @Watch('columnCustomizer')\n handleStructureChange(): void {\n this.scheduleModelIssueWarning();\n this.loadController.structureChanged();\n this.groupLoadController.structureChanged();\n this.virtualController.invalidateMeasures();\n }\n\n @Watch('rows')\n @Watch('groups')\n handleDataChange(): void {\n if (this.grouped) this.groupLoadController.dataChanged();\n else this.loadController.dataChanged();\n this.scheduleModelIssueWarning();\n this.syncActionMenu();\n this.virtualController.invalidateMeasures();\n }\n\n @Watch('totalCount')\n handleTotalCountChange(): void {\n this.scheduleModelIssueWarning();\n }\n\n @Watch('caption')\n handleCaptionChange(): void {\n this.scheduleModelIssueWarning();\n }\n\n @Watch('loadIdentity')\n handleLoadIdentityChange(): void {\n this.incrementalWindowActive = this.hasIncrementalState;\n this.loadController.identityChanged();\n this.virtualController.resetToTop();\n }\n\n @Watch('sort')\n @Watch('grouping')\n handleVirtualIndexReset(): void {\n this.virtualController.resetToTop();\n }\n\n @Watch('loadMoreMode')\n @Watch('loadMoreThreshold')\n handleLazyConfigurationChange(): void {\n this.loadController.configurationChanged();\n this.groupLoadController.configurationChanged();\n }\n\n @Watch('dataMode')\n handleDataModeChange(): void {\n this.incrementalWindowActive = this.hasIncrementalState;\n this.scheduleModelIssueWarning();\n this.handleLazyConfigurationChange();\n this.virtualController.resetToTop();\n }\n\n @Watch('pagination')\n handlePaginationChange(\n pagination: TablePaginationState | null,\n previous: TablePaginationState | null\n ): void {\n if (pagination?.pageSizeMode === 'fit' && pagination.fitIdentity !== previous?.fitIdentity) {\n this.fitMeasurementPending = true;\n }\n }\n\n @Watch('loadingMore')\n handleLoadingMoreChange(loading: boolean): void {\n if (loading) this.incrementalWindowActive = true;\n if (!this.grouped) this.loadController.loadingChanged(loading);\n }\n\n @Watch('loadMoreError')\n handleLoadMoreErrorChange(error: string | undefined): void {\n if (error?.trim()) this.incrementalWindowActive = true;\n if (!this.grouped) this.loadController.errorChanged(error);\n }\n\n @Watch('hasMore')\n handleHasMoreChange(hasMore: boolean, hadMore: boolean): void {\n if (hasMore) this.incrementalWindowActive = true;\n if (!this.grouped) this.loadController.hasMoreChanged(hasMore, hadMore);\n }\n\n @Watch('columnCustomizer')\n @Watch('captionVisibility')\n handleColumnCustomizerAvailability(): void {\n if (!this.showsColumnCustomizer) {\n this.closeColumnCustomizer();\n this.columnCustomizerSurfaceOpen = false;\n }\n }\n\n @Watch('dataModeSwitcher')\n @Watch('captionVisibility')\n handleDataModeSwitcherAvailability(): void {\n if (!this.showsDataModeSwitcher) {\n this.closeDataModeSwitcher();\n this.dataModeSwitcherSurfaceOpen = false;\n }\n }\n\n @Watch('chromeLoading')\n handleChromeLoadingChange(loading: boolean): void {\n if (!loading) return;\n this.closeColumnCustomizer();\n this.closeDataModeSwitcher();\n this.columnCustomizerSurfaceOpen = false;\n this.dataModeSwitcherSurfaceOpen = false;\n }\n\n private get grouped(): boolean {\n return this.grouping !== null;\n }\n\n private get hasIncrementalState(): boolean {\n return (\n this.dataMode === 'infinite' &&\n (this.hasMore || this.loadingMore || !!this.loadMoreError?.trim())\n );\n }\n\n private get selectable(): boolean {\n return this.selectionMode === 'multiple';\n }\n\n private get visibleColumns(): TableColumn[] {\n const cached = this.visibleColumnsCache;\n if (\n cached &&\n cached.columns === this.columns &&\n cached.columnCustomizer === this.columnCustomizer &&\n cached.hiddenColumnIds === this.hiddenColumnIds &&\n cached.columnOrder === this.columnOrder\n ) {\n return cached.value;\n }\n const value = resolveTableVisibleColumns(this.columns, {\n columnCustomizer: this.columnCustomizer,\n hiddenColumnIds: this.hiddenColumnIds,\n columnOrder: this.columnOrder,\n });\n this.visibleColumnsCache = {\n columns: this.columns,\n columnCustomizer: this.columnCustomizer,\n hiddenColumnIds: this.hiddenColumnIds,\n columnOrder: this.columnOrder,\n value,\n };\n return value;\n }\n\n private get showsColumnCustomizer(): boolean {\n return this.columnCustomizer && this.captionVisibility === 'visible';\n }\n\n private get showsDataModeSwitcher(): boolean {\n return this.dataModeSwitcher && this.captionVisibility === 'visible';\n }\n\n private get showsCaptionTrailing(): boolean {\n return (\n this.showsDataModeSwitcher ||\n (this.showsColumnCustomizer && !this.hideColumnCustomizerTrigger)\n );\n }\n\n private get documentStickyHeader(): boolean {\n return this.stickyHeader && !this.containedScroll;\n }\n\n private get fixedHeight(): boolean {\n return !this.fitViewport && this.resolvedHeight !== undefined;\n }\n\n private get boundedComposition(): boolean {\n return this.fitViewport || this.fixedHeight;\n }\n\n private get containedScroll(): boolean {\n return this.boundedComposition || this.resolvedMaxHeight !== undefined;\n }\n\n private connectFitObserver(): void {\n if (typeof ResizeObserver === 'undefined') return;\n if (this.fitObservedViewport === this.viewportEl && this.fitObservedTable === this.tableEl) {\n return;\n }\n this.disconnectFitObserver();\n this.fitObservedViewport = this.viewportEl;\n this.fitObservedTable = this.tableEl;\n this.fitResizeObserver = new ResizeObserver(this.syncFitPageSize);\n if (this.fitObservedViewport) this.fitResizeObserver.observe(this.fitObservedViewport);\n if (this.fitObservedTable) this.fitResizeObserver.observe(this.fitObservedTable);\n }\n\n private disconnectFitObserver(): void {\n this.fitResizeObserver?.disconnect();\n this.fitResizeObserver = null;\n this.fitObservedViewport = null;\n this.fitObservedTable = null;\n }\n\n private connectCaptionCompactObserver(): void {\n if (this.captionCompactDisconnect) return;\n this.captionCompactDisconnect = observeTableCaptionCompact(this.el, compact => {\n if (this.captionCompact !== compact) this.captionCompact = compact;\n });\n }\n\n private disconnectCaptionCompactObserver(): void {\n this.captionCompactDisconnect?.();\n this.captionCompactDisconnect = undefined;\n }\n\n private readonly syncFitPageSize = (): void => {\n const pagination = this.dataMode === 'pagination' ? this.pagination : null;\n if (\n !pagination?.fitToPage ||\n pagination.fitToPageInactive ||\n !this.containedScroll ||\n !this.viewportEl ||\n !this.tableEl\n ) {\n if (this.fitPageSize !== undefined) this.fitPageSize = undefined;\n this.fitMeasurementPending = false;\n return;\n }\n const header = this.tableEl.querySelector<HTMLElement>('.ds-table__head .ds-table__header-row');\n const item = this.tableEl.querySelector<HTMLElement>(\n this.grouped ? '.ds-table__group-row' : '.ds-table__body .ds-table__row'\n );\n const itemBlockSize =\n item?.getBoundingClientRect().height ||\n resolveCssLengthPx(\n 'var(--ds-table-row-min-block-size, var(--dimension-size-500))',\n 0,\n this.rootEl ?? this.el\n );\n const next = resolveTableFitPageSize({\n viewportBlockSize: this.viewportEl.clientHeight,\n headerBlockSize: header?.getBoundingClientRect().height ?? 0,\n itemBlockSize,\n });\n if (next !== this.fitPageSize) this.fitPageSize = next;\n if (!this.fitMeasurementPending || pagination.pageSizeMode !== 'fit' || next === undefined) {\n return;\n }\n this.fitMeasurementPending = false;\n if (next === pagination.pageSize && pagination.pageIndex === 0) return;\n this.dsPaginationChange.emit({\n pageIndex: 0,\n pageSize: next,\n pageSizeMode: 'fit',\n totalItems: pagination.totalItems,\n pageSizeOptions: pagination.pageSizeOptions ?? [25, 50, 100, 200],\n fitToPage: true,\n fitToPageInactive: false,\n fitPageSize: next,\n fitPageSizeLabel: pagination.fitPageSizeLabel ?? 'Fit to page',\n fitPageSizeTriggerLabel: pagination.fitPageSizeTriggerLabel ?? 'Fit',\n itemLabel: pagination.itemLabel ?? 'items',\n pageSizeLabel: pagination.pageSizeLabel ?? 'Items',\n ariaLabel: pagination.ariaLabel ?? `${this.caption} pagination`,\n showFirstLastButtons: pagination.showFirstLastButtons ?? false,\n previousPageIndex: pagination.pageIndex,\n previousPageSize: pagination.pageSize,\n previousPageSizeMode: 'fit',\n reason: 'fit',\n });\n };\n\n @Listen('keydown', { target: 'window' })\n handleWindowPaginationKeyDown(event: KeyboardEvent): void {\n this.handlePaginationKeyDown(event, true);\n }\n\n private handlePaginationKeyDown(event: KeyboardEvent, fromWindow = false): void {\n const pagination = this.dataMode === 'pagination' ? this.pagination : null;\n if (\n !pagination ||\n this.loading ||\n event.defaultPrevented ||\n event.altKey ||\n event.ctrlKey ||\n event.metaKey ||\n event.shiftKey ||\n (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')\n ) {\n return;\n }\n\n if (paginationShortcutBlockedByPath(event.composedPath())) return;\n\n if (fromWindow) {\n const origin = event.composedPath()[0];\n const originNode = origin instanceof Text ? origin.parentNode : origin;\n if (\n !shouldHandleContainingPagePaginationShortcut({\n origin: originNode,\n table: this.el,\n eventPath: event.composedPath(),\n })\n ) {\n return;\n }\n }\n\n const state = resolvePaginationState(pagination);\n const pageIndex = event.key === 'ArrowLeft' ? state.pageIndex - 1 : state.pageIndex + 1;\n if (pageIndex < 0 || pageIndex >= state.totalPages) return;\n\n event.preventDefault();\n this.dsPaginationChange.emit({\n pageIndex,\n pageSize: state.pageSize,\n pageSizeMode: state.pageSizeMode,\n totalItems: state.totalItems,\n pageSizeOptions: state.pageSizeOptions,\n fitToPage: pagination.fitToPage ?? false,\n fitToPageInactive: pagination.fitToPageInactive ?? false,\n fitPageSize: this.fitPageSize ?? pagination.fitPageSize,\n fitPageSizeLabel: pagination.fitPageSizeLabel ?? 'Fit to page',\n fitPageSizeTriggerLabel: pagination.fitPageSizeTriggerLabel ?? 'Fit',\n itemLabel: pagination.itemLabel ?? 'items',\n pageSizeLabel: pagination.pageSizeLabel ?? 'Items',\n ariaLabel: pagination.ariaLabel ?? `${this.caption} pagination`,\n showFirstLastButtons: pagination.showFirstLastButtons ?? false,\n previousPageIndex: state.pageIndex,\n previousPageSize: state.pageSize,\n previousPageSizeMode: state.pageSizeMode,\n reason: 'page',\n });\n }\n\n private currentLoadedRows(): TableRow[] {\n return tableRows(this.rows, this.groups, this.grouped);\n }\n\n private currentLoadedRowCount(): number {\n return this.grouped\n ? this.groups.reduce((count, group) => count + group.rows.length, 0)\n : this.rows.length;\n }\n\n private createRenderModel(): TableRenderModel {\n const columns = this.visibleColumns;\n const grouped = this.grouped;\n const virtualCounts = this.dataMode === 'virtual';\n const cached = this.renderModelCache;\n if (\n cached &&\n cached.columns === columns &&\n cached.rows === this.rows &&\n cached.groups === this.groups &&\n cached.grouped === grouped &&\n cached.selectionMode === this.selectionMode &&\n cached.selectedRowIds === this.selectedRowIds &&\n cached.collapsedGroupIds === this.collapsedGroupIds &&\n cached.virtualCounts === virtualCounts\n ) {\n return cached.value;\n }\n const value = createTableRenderModel({\n columns,\n rows: this.rows,\n groups: this.groups,\n grouped,\n selectionMode: this.selectionMode,\n selectedRowIds: this.selectedRowIds,\n collapsedGroupIds: this.collapsedGroupIds,\n groupCountPresentation: virtualCounts ? 'total' : 'loaded-progress',\n });\n this.renderModelCache = {\n columns,\n rows: this.rows,\n groups: this.groups,\n grouped,\n selectionMode: this.selectionMode,\n selectedRowIds: this.selectedRowIds,\n collapsedGroupIds: this.collapsedGroupIds,\n virtualCounts,\n value,\n };\n return value;\n }\n\n private get highlightMatcher(): TableHighlightMatcher {\n if (this.highlightMatcherCache?.terms === this.highlightTerms) {\n return this.highlightMatcherCache.value;\n }\n const value = createTableHighlightMatcher(this.highlightTerms);\n this.highlightMatcherCache = { terms: this.highlightTerms, value };\n return value;\n }\n\n private get rowWindowingEnabled(): boolean {\n if (!this.hasVirtualViewportBounds) return false;\n if (this.dataMode === 'virtual') return true;\n return !this.grouped && this.currentLoadedRowCount() > TABLE_NON_VIRTUAL_WINDOW_THRESHOLD;\n }\n\n private get virtualViewportMissing(): boolean {\n return this.dataMode === 'virtual' && !this.hasVirtualViewportBounds;\n }\n\n private get hasVirtualViewportBounds(): boolean {\n return this.containedScroll && this.estimateVirtualViewportSize() > 0;\n }\n\n private estimateVirtualViewportSize(): number {\n const measured =\n this.virtualController.currentViewportSize() || this.viewportEl?.clientHeight || 0;\n const chrome =\n (this.captionVisibility === 'visible' ? 48 : 0) + (this.hasResultFooter ? 48 : 0);\n if (this.fixedHeight && this.resolvedHeight) {\n if (measured > 0) return measured;\n return Math.max(\n 0,\n resolveCssLengthPx(this.resolvedHeight, 0, this.rootEl ?? this.el) - chrome\n );\n }\n if (this.fitViewport) return Math.max(0, measured);\n if (this.resolvedMaxHeight) {\n if (this.resolvedMaxHeight.trim().toLowerCase() === 'none') return 0;\n return resolveCssLengthPx(this.resolvedMaxHeight, 0, this.rootEl ?? this.el);\n }\n return 0;\n }\n\n private virtualPinnedRowIds(): Set<string> {\n const pinned = new Set<string>();\n if (this.focusedRowId) pinned.add(this.focusedRowId);\n if (this.actionMenu?.rowId) pinned.add(this.actionMenu.rowId);\n return pinned;\n }\n\n private findRenderedRow(rowId: string): HTMLElement | null {\n return (\n [...this.el.querySelectorAll<HTMLElement>('[data-row-id]')].find(\n candidate => candidate.dataset.rowId === rowId\n ) ?? null\n );\n }\n\n private revealRenderedRow(row: HTMLElement): void {\n if (!this.containedScroll || !this.viewportEl) {\n row.scrollIntoView({ block: 'nearest', inline: 'nearest' });\n return;\n }\n\n const viewportRect = this.viewportEl.getBoundingClientRect();\n const rowRect = row.getBoundingClientRect();\n let visibleStart = viewportRect.top;\n if (this.stickyHeader) {\n visibleStart = Math.max(\n visibleStart,\n this.tableEl?.tHead?.getBoundingClientRect().bottom ?? visibleStart\n );\n }\n const groupRow = row.closest('tbody')?.querySelector<HTMLElement>('.ds-table__group-row');\n if (groupRow && getComputedStyle(groupRow).position === 'sticky') {\n visibleStart = Math.max(visibleStart, groupRow.getBoundingClientRect().bottom);\n }\n\n if (rowRect.top < visibleStart) {\n this.viewportEl.scrollTop += rowRect.top - visibleStart;\n } else if (rowRect.bottom > viewportRect.bottom) {\n this.viewportEl.scrollTop += rowRect.bottom - viewportRect.bottom;\n }\n }\n\n private nextAnimationFrame(): Promise<void> {\n return new Promise(resolve => requestAnimationFrame(() => resolve()));\n }\n\n private syncVirtualItems(model: TableRenderModel): void {\n if (!this.rowWindowingEnabled) {\n this.virtualItems = [];\n this.virtualItemsCache = null;\n this.bodyRenderer.resetVirtualState();\n return;\n }\n const columns = this.visibleColumns;\n const cached = this.virtualItemsCache;\n if (\n cached &&\n cached.columns === columns &&\n cached.rows === this.rows &&\n cached.groups === this.groups &&\n cached.grouped === model.grouped &&\n cached.collapsedGroupIds === this.collapsedGroupIds\n ) {\n return;\n }\n this.virtualItems = flattenTableVirtualItems({\n grouped: model.grouped,\n rows: this.rows,\n groups: this.groups,\n collapsedGroupIds: this.collapsedGroupIds,\n columns,\n });\n this.virtualItemsCache = {\n columns,\n rows: this.rows,\n groups: this.groups,\n grouped: model.grouped,\n collapsedGroupIds: this.collapsedGroupIds,\n };\n }\n\n private shouldWindowRows(model: TableRenderModel): boolean {\n return (\n this.rowWindowingEnabled && (model.hasData || (model.grouped && model.groups.length > 0))\n );\n }\n\n private get resolvedMaxHeight(): string | undefined {\n if (this.maxHeight == null || this.maxHeight === '') return undefined;\n return typeof this.maxHeight === 'number' ? `${Math.max(0, this.maxHeight)}px` : this.maxHeight;\n }\n\n private get resolvedHeight(): string | undefined {\n if (this.height == null || this.height === '') return undefined;\n return typeof this.height === 'number' ? `${Math.max(0, this.height)}px` : this.height;\n }\n\n private warnModelIssues(): void {\n const issues = tableModelIssues(this.columns, this.rows, this.groups, this.grouped);\n const visibleColumns = this.visibleColumns;\n if (!this.caption?.trim()) issues.unshift('A non-empty caption is required.');\n if (this.grouping) {\n const groupingColumn = this.columns.find(column => column.id === this.grouping!.columnId);\n if (!groupingColumn) {\n issues.push(`Grouping references unknown column id: ${this.grouping.columnId}`);\n } else if (!visibleColumns.some(column => column.id === groupingColumn.id)) {\n issues.push(`Grouping references hidden column id: ${this.grouping.columnId}`);\n }\n }\n if (this.sort) {\n const sortColumn = this.columns.find(\n column =>\n column.id === this.sort!.columnId ||\n column.headerSegments?.some(segment => segment.sortKey === this.sort!.columnId)\n );\n if (!sortColumn) {\n issues.push(`Sorting references unknown column id: ${this.sort.columnId}`);\n } else if (!visibleColumns.some(column => column.id === sortColumn.id)) {\n issues.push(`Sorting references hidden column id: ${this.sort.columnId}`);\n }\n }\n if (\n this.dataMode === 'virtual' &&\n Number.isFinite(this.totalCount) &&\n Math.max(0, Math.trunc(this.totalCount!)) !== this.currentLoadedRowCount()\n ) {\n issues.push('Virtual mode requires totalCount to match the complete supplied row count.');\n }\n const stickyStart = visibleColumns.filter(column => column.sticky === 'start');\n const stickyEnd = visibleColumns.filter(column => column.sticky === 'end');\n if (stickyStart.length > 1 || (this.selectable && stickyStart.length > 0)) {\n issues.push(\n 'Only one sticky start column is supported, and row selection already owns that lane.'\n );\n }\n if (stickyEnd.length > 1) issues.push('Only one sticky end column is supported.');\n for (const column of [...stickyStart, ...stickyEnd]) {\n if (!tableColumnSize(column))\n issues.push(`Sticky column ${column.id} requires an explicit size.`);\n }\n const message = issues.join(' ');\n if (!message) {\n this.previousModelWarning = '';\n return;\n }\n if (message === this.previousModelWarning) return;\n this.previousModelWarning = message;\n console.warn(`[ds-table] ${message}`);\n }\n\n private scheduleModelIssueWarning(): void {\n if (this.modelWarningQueued) return;\n this.modelWarningQueued = true;\n queueMicrotask(() => {\n this.modelWarningQueued = false;\n this.warnModelIssues();\n });\n }\n\n private scheduleInitialModelIssueWarning(): void {\n if (this.initialModelWarningFrame !== undefined) {\n cancelAnimationFrame(this.initialModelWarningFrame);\n }\n this.initialModelWarningFrame = requestAnimationFrame(() => {\n this.initialModelWarningFrame = undefined;\n this.warnModelIssues();\n });\n }\n\n private emitSort(column: TableColumn, sortKey = column.id): void {\n if (!column.sortable) return;\n this.dsSortChange.emit({ sort: nextTableSortState(this.sort, sortKey) });\n }\n\n private sortButtonLabel(column: TableColumn, sortKey = column.id, label = column.header): string {\n if (this.sort?.columnId !== sortKey) return `Sort ${label} ascending`;\n if (this.sort.direction === 'asc') return `Sort ${label} descending. Currently ascending.`;\n return `Sort ${label} ascending. Currently descending.`;\n }\n\n private emitRowSelection(row: TableRow): void {\n const next = toggleTableRowSelection(this.selectedRowIds, row);\n const selected = next.includes(row.id);\n this.dsSelectionChange.emit({\n selectedRowIds: next,\n scope: 'row',\n changedRowId: row.id,\n selected,\n });\n }\n\n private emitAllSelection(): void {\n const loadedRows = this.currentLoadedRows();\n const selection = deriveTableSelectionState(loadedRows, this.selectedRowIds);\n this.dsSelectionChange.emit({\n selectedRowIds: toggleAllLoadedTableRows(this.selectedRowIds, loadedRows),\n scope: 'all-loaded',\n selected: !selection.allSelected,\n });\n }\n\n private emitGroupSelection(group: TableGroup): void {\n const state = deriveTableSelectionState(group.rows, this.selectedRowIds);\n this.dsSelectionChange.emit({\n selectedRowIds: toggleTableGroupSelection(this.selectedRowIds, group.rows),\n scope: 'group',\n groupId: group.id,\n selected: !state.allSelected,\n });\n }\n\n private rowEventOwnsActivation(event: Event): boolean {\n const currentTarget = event.currentTarget;\n return !event.composedPath().some(target => {\n if (!(target instanceof HTMLElement) || target === currentTarget) return false;\n return target.matches(\n 'button, a, input, select, textarea, [role=\"button\"], [role=\"checkbox\"], [popover], ds-button-unfilled, ds-menu'\n );\n });\n }\n\n private groupRowEventOwnsCollapse(event: Event): boolean {\n const currentTarget = event.currentTarget;\n return !event.composedPath().some(target => {\n if (!(target instanceof HTMLElement) || target === currentTarget) return false;\n return target.matches(\n 'input, select, textarea, [role=\"checkbox\"], .ds-table__selection-control, .ds-table__group-selection, ds-tooltip'\n );\n });\n }\n\n private handleGroupRowClick(group: TableGroup, event: Event): void {\n if (!this.groupRowEventOwnsCollapse(event)) return;\n this.emitGroupCollapse(group);\n }\n\n private emitRowActivation(row: TableRow, event: Event): void {\n if (row.disabled || !this.rowEventOwnsActivation(event)) return;\n if (this.selectionMode === 'multiple' && this.selectedRowIds.length > 0) {\n if (row.selectable !== false) this.emitRowSelection(row);\n return;\n }\n if (row.interactive) this.dsRowActivate.emit({ rowId: row.id });\n }\n\n private syncActionMenu(): void {\n if (!this.actionMenu) return;\n const row = tableRows(this.rows, this.groups, this.grouped).find(\n candidate => candidate.id === this.actionMenu?.rowId\n );\n const value = row && !row.disabled ? row.cells[this.actionMenu.columnId] : undefined;\n if (!isTableCellAction(value) || !isRenderableTableActionMenu(value)) {\n this.actionMenu = null;\n }\n }\n\n private closeActionMenu(): void {\n this.actionMenu = null;\n }\n\n private handleActionMenuSelect(item: MenuItemData): void {\n const open = this.actionMenu;\n const actionId = item.value;\n if (!open || !actionId || item.isInactive) return;\n this.dsCellAction.emit({\n actionId,\n rowId: open.rowId,\n columnId: open.columnId,\n });\n const triggerId = tableActionTriggerId(this.actionMenuElementId, open.rowId, open.columnId);\n this.closeActionMenu();\n requestAnimationFrame(() => {\n const trigger = this.el.querySelector<HTMLElement & { setFocus?: () => void }>(\n `#${CSS.escape(triggerId)}`\n );\n trigger?.setFocus?.();\n });\n }\n\n private toggleActionMenu(row: TableRow, column: TableColumn, event: MouseEvent): void {\n if (row.disabled) return;\n const open = this.actionMenu?.rowId === row.id && this.actionMenu?.columnId === column.id;\n this.actionMenuInitialFocusVisible = event.detail === 0;\n this.actionMenu = open ? null : { rowId: row.id, columnId: column.id };\n if (this.actionMenu) this.actionMenuSurface = this.actionMenu;\n }\n\n private renderOverflowActionMenu() {\n const open = this.actionMenu ?? this.actionMenuSurface;\n const row = open\n ? tableRows(this.rows, this.groups, this.grouped).find(\n candidate => candidate.id === open.rowId\n )\n : undefined;\n const value = row && open ? row.cells[open.columnId] : undefined;\n const menu = isTableCellAction(value) && isRenderableTableActionMenu(value) ? value : undefined;\n const sections = menu ? tableActionMenuSections(menu.items) : [];\n const triggerId = open\n ? tableActionTriggerId(this.actionMenuElementId, open.rowId, open.columnId)\n : undefined;\n\n return (\n <ds-menu\n id={this.actionMenuElementId}\n open={Boolean(this.actionMenu && sections.length)}\n anchorId={triggerId}\n align=\"end\"\n side=\"bottom\"\n menuLabel={menu?.ariaLabel ?? 'More actions'}\n initialFocusVisible={this.actionMenuInitialFocusVisible}\n sections={sections}\n onDsClose={() => this.closeActionMenu()}\n onDsAfterClose={() => {\n if (!this.actionMenu) this.actionMenuSurface = null;\n }}\n onDsSelect={event => this.handleActionMenuSelect(event.detail)}\n />\n );\n }\n\n private connectTruncateTooltip(): void {\n if (this.truncateTooltipBound) return;\n this.el.addEventListener('pointerover', this.onTruncatePointerOver);\n this.el.addEventListener('pointerout', this.onTruncatePointerOut);\n this.el.addEventListener('focusin', this.onTruncateFocusIn);\n this.el.addEventListener('focusout', this.onTruncateFocusOut);\n this.truncateTooltipBound = true;\n }\n\n private disconnectTruncateTooltip(): void {\n if (!this.truncateTooltipBound) return;\n this.el.removeEventListener('pointerover', this.onTruncatePointerOver);\n this.el.removeEventListener('pointerout', this.onTruncatePointerOut);\n this.el.removeEventListener('focusin', this.onTruncateFocusIn);\n this.el.removeEventListener('focusout', this.onTruncateFocusOut);\n this.truncateTooltipBound = false;\n this.dismissTruncateTooltip();\n }\n\n private onTruncatePointerOver = (event: PointerEvent) => {\n if (event.pointerType === 'touch') return;\n const track = resolveTableTruncateTrack(event.target);\n if (!track) return;\n this.presentOrDismissTruncateTrack(track);\n };\n\n private onTruncatePointerOut = (event: PointerEvent) => {\n if (event.pointerType === 'touch') return;\n if (resolveTableTruncateTrack(event.relatedTarget)) return;\n const next = event.relatedTarget;\n const cell = this.truncateAnchor?.closest('.ds-table__cell');\n if (cell && next instanceof Node && cell.contains(next)) return;\n this.dismissTruncateTooltip();\n };\n\n private onTruncateFocusIn = (event: FocusEvent) => {\n const track = resolveTableTruncateTrack(event.target);\n if (!track) {\n this.dismissTruncateTooltip();\n return;\n }\n this.presentOrDismissTruncateTrack(track);\n };\n\n private onTruncateFocusOut = (event: FocusEvent) => {\n if (resolveTableTruncateTrack(event.relatedTarget)) return;\n this.dismissTruncateTooltip();\n };\n\n private presentOrDismissTruncateTrack(track: HTMLElement): void {\n if (!isElementTruncated(track)) {\n this.dismissTruncateTooltip();\n return;\n }\n const label = tableTruncateLabel(track);\n if (!label) {\n this.dismissTruncateTooltip();\n return;\n }\n this.truncateAnchor = track;\n this.truncateTooltipLabel = label;\n void this.truncateTooltipEl?.presentFrom(track, label);\n }\n\n private dismissTruncateTooltip(): void {\n this.truncateAnchor = null;\n if (!this.truncateTooltipLabel && !this.truncateTooltipEl) return;\n this.truncateTooltipLabel = '';\n void this.truncateTooltipEl?.dismiss();\n }\n\n private renderTruncateTooltip() {\n return (\n <ds-tooltip\n ref={element => {\n this.truncateTooltipEl = element;\n }}\n label={this.truncateTooltipLabel}\n describedBy={false}\n wrapLabel={true}\n size=\"sm\"\n delay={0}\n side=\"top\"\n />\n );\n }\n\n private handleRowKeydown(row: TableRow, event: KeyboardEvent): void {\n if (event.target !== event.currentTarget || (event.key !== 'Enter' && event.key !== ' '))\n return;\n event.preventDefault();\n this.emitRowActivation(row, event);\n }\n\n private onVirtualFocusIn = (event: FocusEvent): void => {\n const target = event.target;\n if (!(target instanceof HTMLElement)) return;\n const row = target.closest('[data-row-id]');\n const id = row?.getAttribute('data-row-id') ?? null;\n if (id === this.focusedRowId) return;\n this.focusedRowId = id;\n if (this.rowWindowingEnabled) this.virtualController.schedule();\n };\n\n private renderSelectionControl(\n label: string,\n checked: boolean,\n indeterminate: boolean,\n disabled: boolean,\n onActivate: () => void\n ) {\n return (\n <button\n class=\"ds-table__selection-control ds-focus-ring ds-interaction-fill__content\"\n type=\"button\"\n role=\"checkbox\"\n aria-label={label}\n aria-checked={indeterminate ? 'mixed' : String(checked)}\n disabled={disabled}\n onClick={onActivate}\n >\n <ds-checkbox\n label=\"\"\n size=\"md\"\n checked={checked}\n indeterminate={indeterminate}\n presentation={true}\n />\n </button>\n );\n }\n\n private renderStickyEdge(sticky: TableColumn['sticky']) {\n if (!sticky) return null;\n return (\n <span class={`ds-table__sticky-edge ds-table__sticky-edge--${sticky}`} aria-hidden=\"true\" />\n );\n }\n\n private renderCollapseAllButton() {\n return (\n <ds-button-unfilled\n class=\"ds-table__collapse-all\"\n variant=\"icon\"\n icon=\"ChevronDownUp\"\n size=\"xs\"\n style={{ width: 'var(--dimension-size-300)' }}\n aria-label=\"Collapse all groups\"\n hasBorder={false}\n activeFill={false}\n pressScale={false}\n onDsClick={event => {\n event.stopPropagation();\n this.emitAllGroupsCollapse();\n }}\n />\n );\n }\n\n private renderFloatingCollapseAll(model: TableRenderModel) {\n if (model.collapseAllHost?.mode !== 'floating') return null;\n return (\n <span\n class=\"ds-table__collapse-all-overlay\"\n ref={element => {\n this.collapseAllOverlayEl = element ?? null;\n }}\n >\n <span class=\"ds-table__collapse-all-surface ds-control-elevation ds-control-elevation--md\">\n {this.renderCollapseAllButton()}\n </span>\n </span>\n );\n }\n\n private renderColumnHeader(\n column: TableColumn,\n model: TableRenderModel,\n interactive = true,\n presentational = false\n ) {\n const groupedColumn = this.grouping?.columnId === column.id;\n const headerSegments = column.headerSegments?.length\n ? column.headerSegments\n : [{ label: column.header, sortKey: column.id }];\n const activeMemberSegment = headerSegments.find(\n segment => segment.sortKey === this.sort?.columnId\n );\n const activeMemberSort = !!activeMemberSegment;\n const activeSort = activeMemberSort;\n const align = column.align ?? 'start';\n const direction = activeMemberSort ? this.sort!.direction : undefined;\n const memberAriaSort = activeMemberSort\n ? this.sort!.direction === 'asc'\n ? 'ascending'\n : 'descending'\n : undefined;\n\n const help = column.help?.trim();\n const labels = (\n <span\n class=\"ds-table__header-labels\"\n tabIndex={interactive && help && !column.sortable ? 0 : undefined}\n data-header-help={help ? '' : undefined}\n >\n {headerSegments.map((segment, index) => {\n const segmentActive = activeMemberSegment?.sortKey === segment.sortKey;\n const segmentInteractive = interactive && !!column.sortable;\n const label = (\n <ds-text\n class=\"ds-table__header-label-box ds-control-label-box\"\n as=\"span\"\n variant=\"text-caption\"\n emphasis={segmentActive}\n color=\"inherit\"\n decoration={help ? 'dotted-underline' : undefined}\n lineTruncation={1}\n >\n {segment.label}\n </ds-text>\n );\n const control = segmentInteractive ? (\n <button\n class=\"ds-table__header-label ds-table__header-label--interactive ds-focus-ring\"\n type=\"button\"\n aria-label={this.sortButtonLabel(\n column,\n segment.sortKey,\n segment.dataLabel?.trim() || segment.label\n )}\n data-sort-control=\"label\"\n data-sort-key={segment.sortKey}\n data-sort-active={segmentActive ? 'true' : undefined}\n onClick={() => this.emitSort(column, segment.sortKey)}\n >\n {label}\n </button>\n ) : (\n <span class=\"ds-table__header-label ds-table__header-static\">{label}</span>\n );\n\n return (\n <span class=\"ds-table__header-segment\" key={segment.sortKey}>\n {control}\n {index < headerSegments.length - 1 && (\n <ds-text\n class=\"ds-table__header-separator\"\n as=\"span\"\n variant=\"text-caption\"\n color=\"tertiary\"\n aria-hidden=\"true\"\n >\n {segment.separator ?? '/'}\n </ds-text>\n )}\n </span>\n );\n })}\n </span>\n );\n const labelControl =\n interactive && help ? (\n <ds-tooltip label={help} side=\"top\" size=\"sm\" wrapLabel={true}>\n {labels}\n </ds-tooltip>\n ) : (\n labels\n );\n const sortControl = interactive ? (\n <span class=\"ds-table__sort-slot\">\n {activeSort && (\n <ds-button-unfilled\n class=\"ds-table__sort-direction\"\n variant=\"icon\"\n icon={direction === 'asc' ? 'ArrowUp' : 'ArrowDown'}\n size=\"xs\"\n aria-label={this.sortButtonLabel(\n column,\n activeMemberSegment?.sortKey ?? column.id,\n activeMemberSegment?.label ?? column.header\n )}\n hasBorder={false}\n activeFill={false}\n isActive={true}\n pressScale={false}\n data-sort-control=\"direction\"\n onDsClick={() => this.emitSort(column, activeMemberSegment?.sortKey ?? column.id)}\n />\n )}\n </span>\n ) : null;\n const collapseHost = model.collapseAllHost;\n const actionCollapseHost =\n collapseHost?.mode === 'action' && collapseHost.columnId === column.id;\n const blankActionCollapseHost =\n actionCollapseHost && !column.header.trim() && !column.headerSegments?.length;\n const collapseControl =\n interactive && actionCollapseHost ? (\n <span class=\"ds-table__collapse-slot\">{this.renderCollapseAllButton()}</span>\n ) : null;\n\n return (\n <th\n key={column.id}\n class={{\n 'ds-table__header-cell': true,\n [`ds-table__cell--align-${align}`]: true,\n 'ds-table__cell--sticky-start': column.sticky === 'start',\n 'ds-table__cell--sticky-end': column.sticky === 'end',\n 'ds-table__header-cell--collapse-all': actionCollapseHost,\n }}\n scope={presentational ? undefined : 'col'}\n aria-sort={presentational ? undefined : memberAriaSort}\n data-column-id={column.id}\n data-grouped={groupedColumn ? 'true' : undefined}\n data-sort-active={activeSort ? 'true' : undefined}\n >\n {!column.header.trim() && column.headerLabel?.trim() && (\n <span class=\"ds-visually-hidden\">{column.headerLabel}</span>\n )}\n {blankActionCollapseHost ? (\n <span class=\"ds-table__header-content ds-table__header-content--collapse-all\">\n {collapseControl}\n </span>\n ) : (\n <span class=\"ds-table__header-content\">\n {align === 'end' && sortControl}\n {align === 'center' && (\n <span\n class=\"ds-table__sort-slot ds-table__sort-slot--balance\"\n data-sort-balance=\"true\"\n aria-hidden=\"true\"\n />\n )}\n {labelControl}\n {align !== 'end' && sortControl}\n {collapseControl}\n </span>\n )}\n {this.renderStickyEdge(column.sticky)}\n </th>\n );\n }\n\n private renderColgroup(model: TableRenderModel) {\n const beforeSpacer =\n model.elasticSpacerIndex == null\n ? this.visibleColumns\n : this.visibleColumns.slice(0, model.elasticSpacerIndex);\n const afterSpacer =\n model.elasticSpacerIndex == null ? [] : this.visibleColumns.slice(model.elasticSpacerIndex);\n const renderColumn = (column: TableColumn) => {\n const width = tableColumnSize(column);\n return (\n <col\n key={column.id}\n class={{ 'ds-table__action-column': column.kind === 'action' }}\n style={width ? { width } : undefined}\n />\n );\n };\n return (\n <colgroup>\n {model.selectable && <col class=\"ds-table__selection-column\" />}\n {beforeSpacer.map(renderColumn)}\n {model.elasticSpacerIndex != null && <col class=\"ds-table__elastic-spacer-column\" />}\n {afterSpacer.map(renderColumn)}\n </colgroup>\n );\n }\n\n private renderHeader(\n model: TableRenderModel,\n interactive = true,\n presentational = false,\n ariaRowIndex?: number\n ) {\n const selection = model.selection;\n const beforeSpacer =\n model.elasticSpacerIndex == null\n ? this.visibleColumns\n : this.visibleColumns.slice(0, model.elasticSpacerIndex);\n const afterSpacer =\n model.elasticSpacerIndex == null ? [] : this.visibleColumns.slice(model.elasticSpacerIndex);\n return (\n <thead\n class={{\n 'ds-table__head': true,\n 'ds-table__head--semantic-copy': !interactive,\n }}\n ref={element => {\n if (interactive) this.interactiveHeadEl = element ?? null;\n }}\n >\n <tr class=\"ds-table__header-row\" aria-rowindex={ariaRowIndex}>\n {model.selectable && (\n <th\n class=\"ds-table__header-cell ds-table__selection-cell ds-table__cell--sticky-start\"\n scope={presentational ? undefined : 'col'}\n >\n {interactive ? (\n this.renderSelectionControl(\n selection.allSelected ? 'Deselect all loaded rows' : 'Select all loaded rows',\n selection.allSelected,\n selection.indeterminate,\n selection.selectableRowIds.length === 0,\n () => this.emitAllSelection()\n )\n ) : (\n <span class=\"ds-visually-hidden\">Select rows</span>\n )}\n {this.renderStickyEdge('start')}\n </th>\n )}\n {beforeSpacer.map(column =>\n this.renderColumnHeader(column, model, interactive, presentational)\n )}\n {model.elasticSpacerIndex != null && (\n <th\n class=\"ds-table__header-cell ds-table__elastic-spacer-cell\"\n aria-hidden=\"true\"\n role=\"presentation\"\n data-elastic-spacer=\"true\"\n />\n )}\n {afterSpacer.map(column =>\n this.renderColumnHeader(column, model, interactive, presentational)\n )}\n </tr>\n </thead>\n );\n }\n\n private renderRow(\n row: TableRow,\n model: TableRenderModel,\n ariaRowIndex?: number,\n variableVirtualSize = false,\n rowKey = row.id\n ) {\n return renderTableRowView({\n row,\n model,\n visibleColumns: this.visibleColumns,\n emptyCellLabel: this.emptyCellLabel,\n actionMenuElementId: this.actionMenuElementId,\n actionMenu: this.actionMenu,\n actionMenuSurface: this.actionMenuSurface,\n surfaceOpenRowId: this.surfaceOpenRowId,\n highlightMatcher: this.highlightMatcher,\n highlightFieldIds: this.highlightFieldIds,\n ariaRowIndex,\n variableVirtualSize,\n intrinsicBlockSize:\n this.dataMode !== 'virtual'\n ? estimateTableRowBlockSize(row, this.visibleColumns)\n : undefined,\n rowKey,\n renderSelectionControl: (label, checked, indeterminate, disabled, onActivate) =>\n this.renderSelectionControl(label, checked, indeterminate, disabled, onActivate),\n renderStickyEdge: sticky => this.renderStickyEdge(sticky),\n onRowActivate: (targetRow, event) => this.emitRowActivation(targetRow, event),\n onRowKeyDown: (targetRow, event) => this.handleRowKeydown(targetRow, event),\n onRowSelection: targetRow => this.emitRowSelection(targetRow),\n onCellAction: detail => this.dsCellAction.emit(detail),\n onActionMenuToggle: (targetRow, column, event) =>\n this.toggleActionMenu(targetRow, column, event),\n });\n }\n\n private emitGroupCollapse(group: TableGroup) {\n const collapsedGroupIds = toggleTableGroupCollapsed(this.collapsedGroupIds, group.id);\n this.dsGroupCollapseChange.emit({\n scope: 'group',\n groupId: group.id,\n collapsed: collapsedGroupIds.includes(group.id),\n collapsedGroupIds,\n });\n }\n\n private emitAllGroupsCollapse() {\n const groupIds = this.groups.map(group => group.id);\n const collapsedGroupIds = nextTableGroupsCollapsed(this.collapsedGroupIds, groupIds);\n this.dsGroupCollapseChange.emit({\n scope: 'all',\n collapsed: collapsedGroupIds.length > 0,\n collapsedGroupIds,\n });\n }\n\n private renderGroupAccessory(\n item: TableRenderModel['groups'][number]['accessories'][number],\n index: number\n ) {\n const text = (\n <ds-text\n key={`acc-${index}`}\n class=\"ds-table__group-accessory\"\n as=\"span\"\n variant=\"text-body-small\"\n color=\"secondary\"\n decoration={item.help ? 'dotted-underline' : undefined}\n lineTruncation={1}\n >\n {item.text}\n </ds-text>\n );\n if (!item.help) return text;\n return (\n <ds-tooltip\n key={`acc-${index}`}\n label={item.help}\n side=\"bottom\"\n align=\"start\"\n size=\"sm\"\n wrapLabel={true}\n >\n <span class=\"ds-table__group-accessory-tip\" tabIndex={0}>\n {text}\n </span>\n </ds-tooltip>\n );\n }\n\n private renderGroupContent(groupModel: TableRenderModel['groups'][number]) {\n const {\n group,\n countLabel,\n collapsed: isCollapsed,\n labelColor,\n selection: groupSelection,\n accessories,\n hero,\n } = groupModel;\n const heroTracks = hero?.kind === 'score' && hero.tracks === 2 ? 2 : 1;\n const heroPresentation = TABLE_GROUP_HERO_SCORE_PRESENTATION[heroTracks];\n return (\n <span\n class={{\n 'ds-table__group-content': true,\n 'ds-table__group-content--multi': accessories.length > 0 || heroTracks === 2,\n 'ds-table__group-content--hero': Boolean(hero),\n 'ds-table__group-content--hero-two-track': Boolean(hero) && heroTracks === 2,\n }}\n onClick={event => this.handleGroupRowClick(group, event)}\n >\n {groupSelection && (\n <span class=\"ds-table__group-selection\">\n {this.renderSelectionControl(\n groupSelection.allSelected\n ? `Deselect loaded rows in ${group.label} group`\n : `Select loaded rows in ${group.label} group`,\n groupSelection.allSelected,\n groupSelection.indeterminate,\n groupSelection.selectableRowIds.length === 0,\n () => this.emitGroupSelection(group)\n )}\n </span>\n )}\n {hero?.kind === 'score' && (\n <span class=\"ds-table__group-hero\">\n <ds-score\n size={heroPresentation.size}\n variant={heroPresentation.variant}\n value={hero.value}\n level={hero.level}\n label={hero.label ?? 'Safety score'}\n isLoading={hero.isLoading === true}\n />\n </span>\n )}\n <span class=\"ds-table__group-copy\">\n <span class=\"ds-table__group-primary\">\n <ds-text\n class=\"ds-table__group-label\"\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis={true}\n color={labelColor}\n >\n {group.label}\n </ds-text>\n <span class=\"ds-visually-hidden\">{countLabel}</span>\n </span>\n {accessories.length > 0 && (\n <span class=\"ds-table__group-accessories\">\n {accessories.flatMap((item, index) => [\n ...(index > 0\n ? [\n <ds-text\n key={`sep-${index}`}\n class=\"ds-table__group-accessory-separator\"\n as=\"span\"\n variant=\"text-body-small\"\n color=\"secondary\"\n aria-hidden=\"true\"\n >\n ·\n </ds-text>,\n ]\n : []),\n this.renderGroupAccessory(item, index),\n ])}\n </span>\n )}\n </span>\n <span class=\"ds-table__group-trailing\">\n <ds-text\n class=\"ds-table__group-count\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n aria-hidden=\"true\"\n >\n {groupModel.visibleCountText}\n </ds-text>\n <span class=\"ds-table__group-action\">\n <ds-button-unfilled\n class=\"ds-table__group-toggle\"\n variant=\"icon\"\n size=\"md\"\n isInset={true}\n insetDepth=\"double\"\n icon={isCollapsed ? 'ChevronDown' : 'ChevronUp'}\n expanded={!isCollapsed}\n aria-label={\n isCollapsed ? `Expand ${group.label} group` : `Collapse ${group.label} group`\n }\n hasBorder={false}\n pressScale={false}\n />\n </span>\n </span>\n </span>\n );\n }\n\n private formatGroupLoadLabel(template: string, group: TableGroup): string {\n return template.split('{group}').join(group.label);\n }\n\n private renderGroupLoadRow(group: TableGroup, totalColumns: number) {\n const error = group.loadMoreError?.trim();\n if (!error && !group.loadingMore && !group.hasMore) return null;\n const manualFallback =\n this.loadMoreMode === 'manual' || !this.groupLoadController.intersectionSupported;\n\n return (\n <tr\n class=\"ds-table__load-row ds-table__group-load-row\"\n data-group-id={group.id}\n ref={element => {\n if (element) this.groupSentinelEls.set(group.id, element);\n else this.groupSentinelEls.delete(group.id);\n }}\n >\n <td class=\"ds-table__load-cell\" colSpan={totalColumns}>\n <div class=\"ds-table__viewport-band ds-table__load-band\">\n {renderTableLoadContent({\n error,\n loading: !!group.loadingMore,\n hasMore: !!group.hasMore,\n manualFallback,\n retryLabel: this.formatGroupLoadLabel(this.groupRetryLabel, group),\n loadingLabel: this.formatGroupLoadLabel(this.groupLoadingMoreLabel, group),\n loadMoreLabel: this.groupLoadMoreLabel,\n loadMoreAriaLabel: this.formatGroupLoadLabel(this.groupLoadMoreAriaLabel, group),\n onRetry: () => this.groupLoadController.request(group.id, 'retry'),\n onLoadMore: () => this.groupLoadController.request(group.id, 'manual'),\n })}\n </div>\n </td>\n </tr>\n );\n }\n\n private renderStickyGroup(model: TableRenderModel) {\n if (!this.documentStickyHeader || !this.activeStickyGroupId) return null;\n const groupModel = model.groups.find(item => item.group.id === this.activeStickyGroupId);\n if (!groupModel) return null;\n return (\n <div\n class={{\n 'ds-table__sticky-group': true,\n 'ds-table__sticky-group--selectable': model.selectable,\n [groupModel.intentClass ?? '']: !!groupModel.intentClass,\n }}\n data-group-id={groupModel.group.id}\n data-group-intent={groupModel.intent}\n >\n {this.renderGroupContent(groupModel)}\n </div>\n );\n }\n\n private renderDataBodies(model: TableRenderModel, plan: TableVirtualPlan | null) {\n return this.bodyRenderer.render({\n model,\n plan,\n rows: this.rows,\n stickyHeader: this.stickyHeader,\n documentStickyHeader: this.documentStickyHeader,\n activeStickyGroupId: this.activeStickyGroupId,\n renderRow: (row, renderModel, ariaRowIndex, variableVirtualSize, rowKey) =>\n this.renderRow(row, renderModel, ariaRowIndex, variableVirtualSize, rowKey),\n renderGroupContent: groupModel => this.renderGroupContent(groupModel),\n renderGroupLoadRow: (group, totalColumns) => this.renderGroupLoadRow(group, totalColumns),\n });\n }\n\n private renderSkeletonBody(model: TableRenderModel) {\n return renderTableSkeletonBody({\n model,\n visibleColumns: this.visibleColumns,\n skeletonRows: this.skeletonRows,\n renderStickyEdge: sticky => this.renderStickyEdge(sticky),\n });\n }\n\n private renderStateBody(kind: 'empty' | 'error' | 'virtual-viewport', totalColumns: number) {\n const error = kind !== 'empty';\n const heading =\n kind === 'virtual-viewport'\n ? TABLE_VIRTUAL_VIEWPORT_REQUIRED_HEADING\n : error\n ? this.errorHeading\n : this.emptyHeading;\n const body =\n kind === 'virtual-viewport'\n ? TABLE_VIRTUAL_VIEWPORT_REQUIRED_BODY\n : error\n ? this.errorBody\n : this.emptyBody;\n return (\n <tbody class=\"ds-table__body ds-table__state-body\">\n <tr class=\"ds-table__state-row\">\n <td class=\"ds-table__state-cell\" colSpan={totalColumns}>\n <div class=\"ds-table__viewport-band ds-table__state-band\">\n <ds-empty-state\n icon={error ? 'ErrorTriangle' : 'Inbox'}\n heading={heading}\n body={body}\n />\n </div>\n </td>\n </tr>\n </tbody>\n );\n }\n\n private renderLazyBody(model: TableRenderModel) {\n if (\n this.dataMode !== 'infinite' ||\n !this.incrementalWindowActive ||\n !model.hasData ||\n model.grouped\n )\n return null;\n const error = this.loadMoreError?.trim();\n const manualFallback =\n this.loadMoreMode === 'manual' || !this.loadController.intersectionSupported;\n\n return (\n <tbody class=\"ds-table__body ds-table__load-body\">\n <tr\n class=\"ds-table__load-row\"\n ref={element => {\n this.sentinelEl = element ?? null;\n }}\n >\n <td class=\"ds-table__load-cell\" colSpan={model.totalColumns}>\n <div class=\"ds-table__viewport-band ds-table__load-band\">\n {renderTableLoadContent({\n error,\n loading: this.loadingMore,\n hasMore: this.hasMore,\n manualFallback,\n retryLabel: this.retryLabel,\n loadingLabel: this.loadingMoreLabel,\n loadMoreLabel: this.loadMoreLabel,\n endOfResultsLabel: this.endOfResultsLabel,\n onRetry: () => this.loadController.request('retry'),\n onLoadMore: () => this.loadController.request('manual'),\n })}\n </div>\n </td>\n </tr>\n </tbody>\n );\n }\n\n private renderDocumentStickyHeader(model: TableRenderModel) {\n if (!this.documentStickyHeader) return null;\n return (\n <div class=\"ds-table__document-sticky-header\">\n <table\n class={{\n 'ds-table__table': true,\n 'ds-table__sticky-header-table': true,\n 'ds-table__table--selectable': model.selectable,\n 'ds-table__table--grouped': model.grouped,\n }}\n style={model.tableStyle}\n role=\"presentation\"\n ref={element => {\n this.stickyHeaderTableEl = element ?? null;\n }}\n >\n {this.renderColgroup(model)}\n {this.renderHeader(model, true, true)}\n </table>\n {this.renderFloatingCollapseAll(model)}\n </div>\n );\n }\n\n private get resultSummary(): string | null {\n if (this.dataMode === 'pagination') return null;\n if (this.dataMode === 'virtual') {\n return formatTableTotalSummary(this.currentLoadedRowCount(), this.resultTotalSummaryLabel);\n }\n return formatTableResultSummary(this.displayedCount, this.totalCount, this.resultSummaryLabel);\n }\n\n private get hasResultFooter(): boolean {\n return (\n (this.dataMode === 'pagination' && !!this.pagination) ||\n !!this.resultSummary ||\n this.footerSlotPresence !== 0\n );\n }\n\n private renderResultFooter() {\n const summary = this.resultSummary;\n const hasLeading = (this.footerSlotPresence & TABLE_FOOTER_SLOT_LEADING) !== 0;\n const hasCopy = (this.footerSlotPresence & TABLE_FOOTER_SLOT_COPY) !== 0;\n const hasTrailing = (this.footerSlotPresence & TABLE_FOOTER_SLOT_TRAILING) !== 0;\n const pagination = this.dataMode === 'pagination' ? this.pagination : null;\n if (!this.hasResultFooter) return null;\n return (\n <div class=\"ds-table__footer ds-table__bar ds-chrome-header ds-control--md\">\n <div class=\"ds-table__bar-copy ds-chrome-header__copy ds-control--md\">\n {hasLeading && (\n <div class=\"ds-table__bar-status ds-chrome-header__heading\">\n <slot name=\"footer-leading\" onSlotchange={this.syncFooterSlotPresence} />\n </div>\n )}\n {hasCopy ? (\n <slot name=\"footer\" onSlotchange={this.syncFooterSlotPresence} />\n ) : summary ? (\n <ds-text\n class=\"ds-table__footer-summary ds-table__bar-text\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n lineTruncation={1}\n >\n {summary}\n </ds-text>\n ) : null}\n </div>\n {(pagination || hasTrailing) && (\n <div class=\"ds-table__bar-trailing ds-chrome-header__trailing\">\n {pagination && (\n <ds-pagination\n pageIndex={pagination.pageIndex}\n pageSize={pagination.pageSize}\n pageSizeMode={pagination.pageSizeMode ?? 'fixed'}\n totalItems={pagination.totalItems}\n pageSizeOptions={pagination.pageSizeOptions ?? [25, 50, 100, 200]}\n fitToPage={pagination.fitToPage ?? false}\n {...{ fitToPageInactive: pagination.fitToPageInactive ?? false }}\n fitPageSize={this.fitPageSize ?? pagination.fitPageSize}\n fitPageSizeLabel={pagination.fitPageSizeLabel ?? 'Fit to page'}\n fitPageSizeTriggerLabel={pagination.fitPageSizeTriggerLabel ?? 'Fit'}\n itemLabel={pagination.itemLabel ?? 'items'}\n pageSizeLabel={pagination.pageSizeLabel ?? 'Items'}\n label={pagination.ariaLabel ?? `${this.caption} pagination`}\n showFirstLastButtons={pagination.showFirstLastButtons ?? false}\n loading={this.loading}\n onDsChange={(event: CustomEvent<PaginationChangeDetail>) =>\n this.dsPaginationChange.emit(event.detail)\n }\n />\n )}\n <slot name=\"footer-trailing\" onSlotchange={this.syncFooterSlotPresence} />\n </div>\n )}\n </div>\n );\n }\n\n private renderCaptionBar() {\n if (this.captionVisibility !== 'visible') return null;\n return (\n <div class=\"ds-table__caption-bar ds-table__bar ds-control--md\">\n <div\n class={{\n 'ds-table__caption-content': true,\n 'ds-table__caption-content--trailing': this.showsCaptionTrailing,\n }}\n >\n <div\n class={{\n 'ds-table__caption-leading': true,\n 'ds-table__caption-leading--toolbar': this.headerUsesToolbar,\n }}\n >\n <slot name=\"header\" onSlotchange={this.syncHeaderSlotPresence} />\n {!this.headerPresent ? (\n <ds-text\n class=\"ds-table__caption-title ds-table__bar-text\"\n as=\"div\"\n variant=\"text-title-small\"\n emphasis={true}\n color=\"primary\"\n aria-hidden=\"true\"\n >\n {this.caption}\n </ds-text>\n ) : null}\n </div>\n {this.renderCaptionTrailing()}\n </div>\n <slot name=\"header-after\" />\n </div>\n );\n }\n\n private renderCaptionTrailing() {\n if (!this.showsCaptionTrailing) return null;\n return (\n <div class=\"ds-table__caption-trailing\">\n {this.renderColumnCustomizerTrigger()}\n {this.showsColumnCustomizer &&\n !this.hideColumnCustomizerTrigger &&\n this.showsDataModeSwitcher ? (\n <ds-divider orientation=\"vertical\" length=\"32px\" />\n ) : null}\n {this.renderDataModeSwitcherTrigger()}\n </div>\n );\n }\n\n private renderDataModeSwitcherTrigger() {\n if (!this.showsDataModeSwitcher) return null;\n if (this.chromeLoading) {\n return <ds-skeleton variant=\"control\" controlSize=\"md\" width=\"var(--dimension-size-400)\" />;\n }\n return (\n <span class=\"ds-table__caption-mode-switcher\">\n <ds-tooltip label={this.dataModeSwitcherLabel} side=\"top\" size=\"sm\">\n <ds-button-unfilled\n hasBorder={!this.captionControlsBorderless}\n id={`${this.dataModeSwitcherElementId}-trigger`}\n variant=\"icon\"\n size=\"md\"\n icon=\"Ellipses\"\n aria-label={this.dataModeSwitcherLabel}\n hasMenu={true}\n expanded={this.dataModeSwitcherOpen}\n surfaceOpen={this.dataModeSwitcherSurfaceOpen}\n controls={this.dataModeSwitcherElementId}\n activeFill={false}\n pressScale={false}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n this.toggleDataModeSwitcher(event.detail.detail === 0);\n }}\n />\n </ds-tooltip>\n </span>\n );\n }\n\n private renderDataModeSwitcherMenu() {\n if (!this.showsDataModeSwitcher || this.chromeLoading) return null;\n return (\n <ds-menu\n id={this.dataModeSwitcherElementId}\n open={this.dataModeSwitcherOpen}\n anchorId={`${this.dataModeSwitcherElementId}-trigger`}\n align=\"end\"\n side=\"bottom\"\n menuLabel={this.dataModeMenuLabel}\n initialFocusVisible={this.dataModeSwitcherInitialFocusVisible}\n items={tableDataModeMenuItems(this.dataMode, {\n infinite: this.infiniteModeLabel,\n pagination: this.paginationModeLabel,\n virtual: this.virtualModeLabel,\n })}\n onDsClose={() => this.closeDataModeSwitcher()}\n onDsAfterClose={() => {\n if (!this.dataModeSwitcherOpen) this.dataModeSwitcherSurfaceOpen = false;\n }}\n onDsSelect={event => this.handleDataModeSwitcherSelect(event.detail)}\n />\n );\n }\n\n private toggleDataModeSwitcher(fromKeyboard = false): void {\n if (this.dataModeSwitcherOpen) this.closeDataModeSwitcher();\n else this.openDataModeSwitcher(fromKeyboard);\n }\n\n private openDataModeSwitcher(fromKeyboard = false): void {\n if (!this.showsDataModeSwitcher || this.dataModeSwitcherOpen) return;\n this.closeColumnCustomizer();\n this.dataModeSwitcherInitialFocusVisible = fromKeyboard;\n this.dataModeSwitcherOpen = true;\n this.dataModeSwitcherSurfaceOpen = true;\n }\n\n private closeDataModeSwitcher(): void {\n this.dataModeSwitcherOpen = false;\n }\n\n private handleDataModeSwitcherSelect(item: MenuItemData): void {\n const dataMode = tableDataModeFromMenuItem(item);\n if (!dataMode) return;\n this.closeDataModeSwitcher();\n if (dataMode !== this.dataMode) this.dsDataModeChange.emit({ dataMode });\n requestAnimationFrame(() => {\n this.el\n .querySelector<\n HTMLElement & { setFocus?: () => void }\n >(`#${CSS.escape(`${this.dataModeSwitcherElementId}-trigger`)}`)\n ?.setFocus?.();\n });\n }\n\n private renderColumnCustomizerTrigger() {\n if (this.hideColumnCustomizerTrigger) return null;\n if (!this.showsColumnCustomizer) return null;\n return (\n <div\n class={{\n 'ds-table__caption-customizer': true,\n 'ds-table__caption-customizer--loading': this.chromeLoading,\n }}\n aria-hidden={this.chromeLoading ? 'true' : undefined}\n >\n <ds-tooltip label={this.captionCompact ? 'Customize' : ''} side=\"top\" size=\"sm\">\n <ds-button-unfilled\n hasBorder={!this.captionControlsBorderless}\n id={`${this.columnCustomizerElementId}-trigger`}\n variant={this.captionCompact ? 'icon' : 'icon-label'}\n size=\"md\"\n icon=\"Table\"\n label=\"Customize\"\n labelEmphasis={false}\n pressScale={false}\n aria-label=\"Customize table\"\n haspopup=\"menu\"\n expanded={this.columnCustomizerOpen}\n surfaceOpen={this.columnCustomizerSurfaceOpen}\n controls={this.columnCustomizerElementId}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n if (this.chromeLoading) return;\n this.toggleColumnCustomizer(event.detail.detail === 0);\n }}\n />\n </ds-tooltip>\n {this.chromeLoading ? (\n <ds-skeleton variant=\"control\" controlSize=\"md\" width=\"100%\" />\n ) : null}\n </div>\n );\n }\n\n private renderColumnCustomizerMenu() {\n if (!this.showsColumnCustomizer || this.chromeLoading) return null;\n return (\n <ds-menu\n id={this.columnCustomizerElementId}\n open={this.columnCustomizerOpen}\n anchorId={`${this.columnCustomizerElementId}-trigger`}\n align=\"end\"\n side=\"bottom\"\n menuLabel=\"Customize table\"\n initialFocusVisible={this.columnCustomizerInitialFocusVisible}\n sections={[\n {\n header: 'Columns',\n items: tableColumnCustomizerMenuItems(\n this.columns,\n this.hiddenColumnIds,\n this.columnOrder\n ),\n },\n ...(this.customizeOptions.length\n ? [{ header: 'Options', items: this.customizeOptions }]\n : []),\n ]}\n onDsClose={() => this.closeColumnCustomizer()}\n onDsAfterClose={() => {\n if (!this.columnCustomizerOpen) this.columnCustomizerSurfaceOpen = false;\n }}\n onDsSelect={event => {\n if (this.customizeOptions.some(option => option.value === event.detail.value)) {\n this.dsCustomizeOptionChange.emit(event.detail.value!);\n } else this.handleColumnCustomizerSelect(event.detail);\n }}\n onDsReorder={event => this.handleColumnCustomizerReorder(event.detail)}\n />\n );\n }\n\n private toggleColumnCustomizer(fromKeyboard = false): void {\n if (this.columnCustomizerOpen) this.closeColumnCustomizer();\n else this.openColumnCustomizer(fromKeyboard);\n }\n\n private openColumnCustomizer(fromKeyboard = false): void {\n if (!this.showsColumnCustomizer || this.columnCustomizerOpen) return;\n this.closeDataModeSwitcher();\n this.columnCustomizerInitialFocusVisible = fromKeyboard;\n this.columnCustomizerOpen = true;\n this.columnCustomizerSurfaceOpen = true;\n }\n\n private closeColumnCustomizer(): void {\n this.columnCustomizerOpen = false;\n }\n\n private emitColumnsConfigChange(hiddenColumnIds: string[], columnOrder: string[]): void {\n this.dsColumnsConfigChange.emit({\n hiddenColumnIds: resolveTableHiddenColumnIds(this.columns, hiddenColumnIds),\n columnOrder: resolveTableColumnOrder(this.columns, columnOrder),\n });\n }\n\n private handleColumnCustomizerReorder(detail: MenuReorderDetail): void {\n const order = detail.items\n .filter(item => item.reorderable)\n .map(item => item.value)\n .filter((id): id is string => !!id);\n this.emitColumnsConfigChange(this.hiddenColumnIds, order);\n }\n\n private handleColumnCustomizerSelect(item: MenuItemData): void {\n const columnId = item.value;\n if (!columnId || item.isInactive) return;\n this.emitColumnsConfigChange(\n toggleTableColumnHidden(this.columns, this.hiddenColumnIds, columnId),\n resolveTableColumnOrder(this.columns, this.columnOrder)\n );\n }\n\n render() {\n this.groupSentinelEls.clear();\n const model = this.createRenderModel();\n this.syncVirtualItems(model);\n this.renderedModel = model;\n const regionLabel = this.scrollLabel?.trim() || `${this.caption} scroll area`;\n const initialLoading = this.loading && !model.hasData;\n const initialError = !model.hasData && this.error;\n const hasGroupedStructure = model.grouped && model.groups.length > 0;\n const windowRows = this.shouldWindowRows(model);\n const virtualViewportMissing =\n this.virtualViewportMissing && !initialLoading && (model.hasData || hasGroupedStructure);\n const virtualPlan = windowRows\n ? (this.virtualWindow ??\n resolveTableVirtualPlan({\n items: this.virtualItems,\n sizes: this.virtualItems.map(item => this.virtualController.sizeFor(item)),\n scrollOffset: this.viewportEl?.scrollTop ?? 0,\n viewportSize: this.estimateVirtualViewportSize(),\n pinnedRowIds: this.virtualPinnedRowIds(),\n }))\n : null;\n const groupLoadingMore = hasGroupedStructure && this.groups.some(group => group.loadingMore);\n const viewportStyle =\n !this.boundedComposition && this.resolvedMaxHeight\n ? { '--ds-table-max-block-size': this.resolvedMaxHeight }\n : undefined;\n const hostStyle = this.fixedHeight\n ? { '--_table-fixed-block-size': this.resolvedHeight }\n : undefined;\n\n return (\n <Host\n class={{\n 'table-host': true,\n 'table-host--bounded': this.boundedComposition,\n 'table-host--fixed-height': this.fixedHeight,\n 'table-host--viewport-fit': this.fitViewport,\n }}\n style={hostStyle}\n onKeyDown={(event: KeyboardEvent) => this.handlePaginationKeyDown(event)}\n onFocusin={this.onVirtualFocusIn}\n >\n <div\n class={{\n 'ds-table': true,\n 'ds-table--bounded': this.boundedComposition,\n 'ds-table--contained-scroll': this.containedScroll,\n 'ds-table--fixed-height': this.fixedHeight,\n 'ds-table--viewport-fit': this.fitViewport,\n 'ds-table--viewport-fit-pending': this.fitViewport && !this.viewportFitSettled,\n 'ds-table--viewport-fit-settled': this.fitViewport && this.viewportFitSettled,\n 'ds-table--sticky-header': this.stickyHeader,\n 'ds-table--document-sticky-header': this.documentStickyHeader,\n 'ds-table--contained-sticky-header': this.stickyHeader && !this.documentStickyHeader,\n 'ds-table--caption-visible': this.captionVisibility === 'visible',\n 'ds-table--footer-visible': this.hasResultFooter,\n 'ds-table--state-fill':\n !initialLoading &&\n (initialError || virtualViewportMissing || !(model.hasData || hasGroupedStructure)),\n }}\n ref={element => {\n this.rootEl = element ?? null;\n }}\n >\n {this.renderCaptionBar()}\n <div\n class={{\n 'ds-table__frame': true,\n 'ds-table__frame--overflow-start': this.overflowStart,\n 'ds-table__frame--overflow-end': this.overflowEnd,\n }}\n ref={element => {\n this.frameEl = element ?? null;\n }}\n >\n {this.renderDocumentStickyHeader(model)}\n {this.renderStickyGroup(model)}\n {!this.documentStickyHeader && this.renderFloatingCollapseAll(model)}\n <div\n class={{\n 'ds-table__viewport': true,\n 'ds-focus-ring': this.scrollable,\n }}\n style={viewportStyle}\n ref={element => {\n this.viewportEl = element ?? null;\n }}\n role={this.scrollable ? 'region' : undefined}\n aria-label={this.scrollable ? regionLabel : undefined}\n tabIndex={this.scrollable ? 0 : undefined}\n >\n <table\n class={{\n 'ds-table__table': true,\n 'ds-table__table--selectable': model.selectable,\n 'ds-table__table--grouped': model.grouped,\n 'ds-table__table--deferred-rows': this.dataMode !== 'virtual',\n 'ds-table__table--windowed': windowRows,\n 'ds-table__table--virtual': this.dataMode === 'virtual' && windowRows,\n 'ds-table__table--native-group-sticky':\n (model.grouped && this.stickyHeader && !this.documentStickyHeader) ||\n windowRows,\n }}\n style={model.tableStyle}\n aria-rowcount={windowRows ? 1 + this.virtualItems.length : undefined}\n aria-busy={\n initialLoading ||\n (this.dataMode === 'infinite' && (this.loadingMore || groupLoadingMore))\n ? 'true'\n : undefined\n }\n ref={element => {\n this.tableEl = element ?? null;\n }}\n >\n <caption class=\"ds-table__caption ds-visually-hidden\">\n <ds-text as=\"span\" variant=\"text-title-small\" emphasis={true} color=\"primary\">\n {this.caption}\n </ds-text>\n </caption>\n {this.renderColgroup(model)}\n {this.renderHeader(\n model,\n !this.documentStickyHeader,\n false,\n windowRows ? 1 : undefined\n )}\n {initialLoading\n ? this.renderSkeletonBody(model)\n : initialError\n ? this.renderStateBody('error', model.totalColumns)\n : virtualViewportMissing\n ? this.renderStateBody('virtual-viewport', model.totalColumns)\n : model.hasData || hasGroupedStructure\n ? this.renderDataBodies(model, virtualPlan)\n : this.renderStateBody('empty', model.totalColumns)}\n {!initialLoading &&\n !initialError &&\n this.dataMode !== 'virtual' &&\n this.renderLazyBody(model)}\n </table>\n </div>\n </div>\n {this.renderResultFooter()}\n {this.renderOverflowActionMenu()}\n {this.renderDataModeSwitcherMenu()}\n {this.renderColumnCustomizerMenu()}\n {this.renderTruncateTooltip()}\n <div class=\"ds-visually-hidden\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n {this.announcement}\n </div>\n </div>\n </Host>\n );\n }\n}\n",
1434
+ "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Listen,\n Method,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport {\n deriveTableSelectionState,\n formatTableResultSummary,\n formatTableTotalSummary,\n hasOwnedTableFooterSlot,\n isTableCellAction,\n nextDataSortState,\n tableColumnSize,\n tableModelIssues,\n TABLE_GROUP_HERO_SCORE_PRESENTATION,\n tableRows,\n toggleAllLoadedTableRows,\n toggleTableGroupCollapsed,\n toggleTableGroupSelection,\n nextTableGroupsCollapsed,\n toggleTableRowSelection,\n} from './table-model';\nimport { createTableRenderModel, type TableRenderModel } from './table-render-model';\nimport {\n isRenderableTableActionMenu,\n nextTableActionMenuElementId,\n tableActionMenuSections,\n tableActionTriggerId,\n} from './table-action-menu';\nimport {\n nextTableColumnCustomizerElementId,\n resolveTableColumnOrder,\n resolveTableHiddenColumnIds,\n resolveTableVisibleColumns,\n tableColumnCustomizerMenuItems,\n toggleTableColumnHidden,\n} from './table-column-customizer';\nimport {\n nextTableDataModeSwitcherElementId,\n tableDataModeFromMenuItem,\n tableDataModeMenuItems,\n} from './table-data-mode-switcher';\nimport { resolveTableTruncateTrack, tableTruncateLabel } from './table-truncate';\nimport { renderTableRow as renderTableRowView } from './table-row-view';\nimport { TableBodyRenderer } from './table-body-renderer';\nimport { renderTableSkeletonBody } from './table-skeleton-view';\nimport { renderTableLoadContent } from './table-load-view';\nimport { TableLayoutController } from './table-layout-controller';\nimport { TableLoadController } from './table-load-controller';\nimport { TableGroupLoadController } from './table-group-load-controller';\nimport {\n estimateTableRowBlockSize,\n flattenTableVirtualItems,\n resolveTableVirtualPlan,\n TABLE_VIRTUAL_TOTAL_SUMMARY_LABEL,\n TABLE_VIRTUAL_VIEWPORT_REQUIRED_BODY,\n TABLE_VIRTUAL_VIEWPORT_REQUIRED_HEADING,\n type TableVirtualItem,\n type TableVirtualPlan,\n} from './table-virtual-model';\nimport { TableVirtualController } from './table-virtual-controller';\nimport type { PaginationChangeDetail } from '../Pagination/pagination-types';\nimport type { MenuItemData, MenuReorderDetail } from '../Menu/menu-types';\nimport { resolvePaginationState } from '../Pagination/pagination-model';\nimport { resolveCssLengthPx } from '../../utils/resolve-css-length-px';\nimport { isElementTruncated } from '../../utils/is-element-truncated';\nimport { observeTableCaptionCompact } from '../../utils/table-caption-compact';\nimport { resolveTableFitPageSize } from './table-pagination-fit';\nimport { createTableHighlightMatcher, type TableHighlightMatcher } from './table-highlight';\nimport {\n paginationShortcutBlockedByPath,\n shouldHandleContainingPagePaginationShortcut,\n} from './table-pagination-shortcut';\nimport {\n TableViewportFitController,\n type TableViewportFitMetrics,\n} from './table-viewport-fit-controller';\nimport type {\n TableCaptionVisibility,\n TableCellActionDetail,\n TableColumn,\n DataFieldsConfigChangeDetail,\n TableDataMode,\n TableDataModeChangeDetail,\n DataGroup,\n TableGroupCollapseChangeDetail,\n TableGroupLoadMoreDetail,\n DataGroupingState,\n TableLoadMoreDetail,\n TableLoadMoreMode,\n TablePaginationState,\n TableRow,\n TableRowActivateDetail,\n TableSelectionChangeDetail,\n TableSelectionMode,\n DataSortChangeDetail,\n DataSortState,\n} from './table-types';\n\nconst TABLE_FOOTER_SLOT_LEADING = 1;\nconst TABLE_FOOTER_SLOT_COPY = 2;\nconst TABLE_FOOTER_SLOT_TRAILING = 4;\nconst TABLE_NON_VIRTUAL_WINDOW_THRESHOLD = 50;\n\n@Component({\n tag: 'ds-table',\n styleUrls: ['../../utils/focus-ring.css', '../../utils/interaction-fill.css', 'Table.css'],\n scoped: true,\n})\nexport class Table {\n @Element() el!: HTMLElement;\n /** Stable column definitions. Assign through JavaScript. */\n @Prop() columns: TableColumn[] = [];\n /** Additional toggle options in the column customizer. */\n @Prop() customizeOptions: MenuItemData[] = [];\n @Event() dsCustomizeOptionChange!: EventEmitter<string>;\n /** Ungrouped row data. Ignored while grouping is active. Assign through JavaScript. */\n @Prop() rows: TableRow[] = [];\n /** One level of application-owned grouped data. Assign through JavaScript. */\n @Prop() groups: DataGroup[] = [];\n /** Controlled grouping column. Applications supply groups in their final fixed order. */\n @Prop() grouping: DataGroupingState | null = null;\n /** Controlled member-row sort state. */\n @Prop() sort: DataSortState | null = null;\n /** Controlled collapsed group identities. Groups not listed remain expanded. */\n @Prop() collapsedGroupIds: string[] = [];\n /** Literal terms to highlight in table-owned text cells. Applications still own filtering. */\n @Prop() highlightTerms: string[] = [];\n /** Optional DataSearch field identities that restrict which data-point tracks are highlighted. */\n @Prop() highlightFieldIds: string[] = [];\n\n /** Required accessible table name, retained as a native caption. */\n @Prop() caption!: string;\n /** Shows a matching presentational title bar above the native table frame. */\n @Prop() captionVisibility: TableCaptionVisibility = 'hidden';\n /**\n * Opt in to the table-owned column customizer. The `columns` prop remains the\n * catalog; hidden and ordered columns are controlled separately. The trigger\n * opens the shared Menu of live show/hide switch rows.\n */\n @Prop() columnCustomizer: boolean = false;\n /** Hide only the built-in Customize trigger when an external preferences surface owns it. */\n /** Remove borders from table-owned caption controls. */\n @Prop() captionControlsBorderless: boolean = false;\n @Prop() hideColumnCustomizerTrigger: boolean = false;\n /** Controlled hidden data-column identities. Action ids are ignored. */\n @Prop() hiddenFieldIds: string[] = [];\n /** Controlled data-column identities in display order. Omitted ids append in catalog order. */\n @Prop() fieldOrder: string[] = [];\n /**\n * Optional result summary footer. When both `displayedCount` and `totalCount`\n * are finite numbers, infinite mode shows “Displaying {displayed} of {total}”.\n * Virtual mode ignores `displayedCount` and derives its total from the\n * complete supplied rows; a mismatched `totalCount` emits a warning.\n */\n @Prop() displayedCount: number | undefined;\n @Prop() totalCount: number | undefined;\n /** Supports {displayed} and {total} placeholders. */\n @Prop() resultSummaryLabel: string = 'Displaying {displayed} of {total}';\n /** Supports the {total} placeholder. Used when dataMode is virtual. */\n @Prop() resultTotalSummaryLabel: string = TABLE_VIRTUAL_TOTAL_SUMMARY_LABEL;\n @Prop() stickyHeader: boolean = false;\n /** Maximum scroll-region height. Numbers resolve to CSS pixels. */\n @Prop() maxHeight: string | number | undefined;\n /** Fixed height for the complete header, table frame, and footer composition. */\n @Prop() height: string | number | undefined;\n /** Fit the complete table composition to its nearest vertical scrollport. */\n @Prop() fitViewport: boolean = false;\n /** Reserved space above a viewport-fitted table once surrounding chrome is compact. */\n @Prop() viewportInsetBlockStart: string | number = 0;\n /** Reserved space below a viewport-fitted table. */\n @Prop() viewportInsetBlockEnd: string | number = 0;\n /** Optional explicit label for the horizontal/vertical scroll region. */\n @Prop() scrollLabel: string | undefined;\n\n @Prop() selectionMode: TableSelectionMode = 'none';\n /** Controlled selected row identities. IDs outside the loaded rows are preserved. */\n @Prop() selectedRowIds: string[] = [];\n\n /**\n * Identity of the interactive row whose associated surface is visible.\n * Holds its pressed wash independently of checkbox selection. Keep the ID\n * through exit motion and clear it after close; the application owns toggling\n * and switching the surface. Off-window identities survive virtualization.\n */\n @Prop() surfaceOpenRowId: string | undefined;\n\n /** Initial loading state. Existing rows stay visible; incremental loading uses loadingMore. */\n @Prop() loading: boolean = false;\n /** Replace opted-in table-owned caption controls with same-size visual skeletons. */\n @Prop() chromeLoading: boolean = false;\n /** Initial-loading rows. Defaults to ten so bounded tables retain a useful filled viewport. */\n @Prop() skeletonRows: number = 10;\n @Prop() emptyHeading: string = 'No results';\n @Prop() emptyBody: string = 'No data is available.';\n /** Initial error state. Existing rows stay visible; incremental failures use loadMoreError. */\n @Prop() error: boolean = false;\n @Prop() errorHeading: string = 'Unable to load data';\n @Prop() errorBody: string = 'The data could not be loaded.';\n @Prop() emptyCellLabel: string = 'Not available';\n\n /** Top-level data-window strategy. Virtual mode recycles row DOM only. */\n @Prop() dataMode: TableDataMode = 'infinite';\n /** Opt in to the table-owned control for choosing between supported data modes. */\n @Prop() dataModeSwitcher: boolean = false;\n @Prop() dataModeSwitcherLabel: string = 'Change table variation';\n @Prop() dataModeMenuLabel: string = 'Table variation';\n @Prop() infiniteModeLabel: string = 'Infinite scroll';\n @Prop() paginationModeLabel: string = 'Pagination + Infinite groups';\n @Prop() virtualModeLabel: string = 'Virtual scroll';\n /** Controlled top-level pagination state. Required when dataMode is pagination. */\n @Prop() pagination: TablePaginationState | null = null;\n @Prop() loadMoreMode: TableLoadMoreMode = 'auto';\n @Prop() hasMore: boolean = false;\n @Prop() loadingMore: boolean = false;\n @Prop() loadMoreError: string | undefined;\n /** Reset key for a new query/group/sort dataset. */\n @Prop() loadIdentity: string | number = 'default';\n /** IntersectionObserver root margin in CSS pixels. */\n @Prop() loadMoreThreshold: number = 0;\n @Prop() loadMoreLabel: string = 'Load more';\n @Prop() retryLabel: string = 'Retry';\n @Prop() loadingMoreLabel: string = 'Loading more items';\n @Prop() endOfResultsLabel: string = 'All results loaded';\n /** Supports {count} and {total} placeholders. */\n @Prop() rowsLoadedLabel: string = '{count} more rows loaded. {total} rows loaded.';\n @Prop() groupLoadMoreLabel: string = 'Load more';\n /** Supports the {group} placeholder. */\n @Prop() groupLoadMoreAriaLabel: string = 'Load more {group} results';\n /** Supports the {group} placeholder. */\n @Prop() groupRetryLabel: string = 'Retry loading {group} results';\n /** Supports the {group} placeholder. */\n @Prop() groupLoadingMoreLabel: string = 'Loading more items';\n /** Supports the {group} placeholder. */\n @Prop() groupEndOfResultsLabel: string = 'All {group} results loaded';\n /** Supports {group}, {count}, {loaded}, and {total} placeholders. */\n @Prop() groupRowsLoadedLabel: string =\n '{count} more rows loaded in {group}. {loaded} of {total} rows loaded.';\n\n @Event() dsSortChange!: EventEmitter<DataSortChangeDetail>;\n @Event() dsGroupCollapseChange!: EventEmitter<TableGroupCollapseChangeDetail>;\n @Event() dsSelectionChange!: EventEmitter<TableSelectionChangeDetail>;\n @Event() dsLoadMore!: EventEmitter<TableLoadMoreDetail>;\n @Event() dsGroupLoadMore!: EventEmitter<TableGroupLoadMoreDetail>;\n @Event() dsPaginationChange!: EventEmitter<PaginationChangeDetail>;\n @Event() dsCellAction!: EventEmitter<TableCellActionDetail>;\n @Event() dsRowActivate!: EventEmitter<TableRowActivateDetail>;\n @Event() dsFieldsConfigChange!: EventEmitter<DataFieldsConfigChangeDetail>;\n @Event() dsDataModeChange!: EventEmitter<TableDataModeChangeDetail>;\n\n @State() private overflowStart = false;\n @State() private overflowEnd = false;\n @State() private scrollable = false;\n @State() private announcement = '';\n @State() private activeStickyGroupId: string | null = null;\n @State() private viewportFitSettled = false;\n @State() private headerPresent = false;\n @State() private headerUsesToolbar = false;\n @State() private footerSlotPresence = 0;\n @State() private fitPageSize: number | undefined;\n @State() private actionMenu: { rowId: string; columnId: string } | null = null;\n @State() private actionMenuInitialFocusVisible = false;\n @State() private actionMenuSurface: { rowId: string; columnId: string } | null = null;\n @State() private truncateTooltipLabel = '';\n @State() private captionCompact = false;\n @State() private columnCustomizerOpen = false;\n @State() private columnCustomizerSurfaceOpen = false;\n @State() private columnCustomizerInitialFocusVisible = false;\n @State() private dataModeSwitcherOpen = false;\n @State() private dataModeSwitcherSurfaceOpen = false;\n @State() private dataModeSwitcherInitialFocusVisible = false;\n @State() private virtualWindow: TableVirtualPlan | null = null;\n\n private readonly actionMenuElementId = nextTableActionMenuElementId();\n private readonly columnCustomizerElementId = nextTableColumnCustomizerElementId();\n private readonly dataModeSwitcherElementId = nextTableDataModeSwitcherElementId();\n private truncateTooltipEl?: HTMLDsTooltipElement;\n private truncateAnchor: HTMLElement | null = null;\n private truncateTooltipBound = false;\n private focusedRowId: string | null = null;\n private virtualItems: TableVirtualItem[] = [];\n private visibleColumnsCache: {\n columns: TableColumn[];\n columnCustomizer: boolean;\n hiddenFieldIds: string[];\n fieldOrder: string[];\n value: TableColumn[];\n } | null = null;\n private renderModelCache: {\n columns: TableColumn[];\n rows: TableRow[];\n groups: DataGroup[];\n grouped: boolean;\n selectionMode: TableSelectionMode;\n selectedRowIds: string[];\n collapsedGroupIds: string[];\n virtualCounts: boolean;\n value: TableRenderModel;\n } | null = null;\n private highlightMatcherCache: {\n terms: string[];\n value: TableHighlightMatcher;\n } | null = null;\n private virtualItemsCache: {\n columns: TableColumn[];\n rows: TableRow[];\n groups: DataGroup[];\n grouped: boolean;\n collapsedGroupIds: string[];\n } | null = null;\n private rootEl: HTMLElement | null = null;\n private viewportEl: HTMLElement | null = null;\n private frameEl: HTMLElement | null = null;\n private interactiveHeadEl: HTMLTableSectionElement | null = null;\n private collapseAllOverlayEl: HTMLElement | null = null;\n private stickyHeaderTableEl: HTMLTableElement | null = null;\n private tableEl: HTMLTableElement | null = null;\n private sentinelEl: HTMLElement | null = null;\n private incrementalWindowActive = false;\n private readonly groupSentinelEls = new Map<string, HTMLElement>();\n private previousModelWarning = '';\n private modelWarningQueued = false;\n private initialModelWarningFrame: number | undefined;\n private hasLoaded = false;\n private renderedModel: TableRenderModel | null = null;\n private stickyGroupConnected = false;\n private headerSlotObserver: MutationObserver | null = null;\n private footerSlotObserver: MutationObserver | null = null;\n private captionCompactDisconnect: (() => void) | undefined;\n private fitResizeObserver: ResizeObserver | null = null;\n private fitObservedViewport: HTMLElement | null = null;\n private fitObservedTable: HTMLTableElement | null = null;\n private fitMeasurementPending = false;\n private readonly bodyRenderer = new TableBodyRenderer();\n private readonly layoutController = new TableLayoutController({\n elements: () => ({\n viewport: this.viewportEl,\n contentTable: this.tableEl,\n stickyHeaderTable: this.stickyHeaderTableEl,\n collapseAllOverlay: this.collapseAllOverlayEl,\n frame: this.frameEl,\n interactiveHead: this.interactiveHeadEl,\n }),\n mode: () => ({\n documentStickyHeader: this.documentStickyHeader,\n floatingCollapseAll: this.renderedModel?.collapseAllHost?.mode === 'floating',\n clampVerticalOverscroll: this.fitViewport && this.viewportFitSettled,\n }),\n verticalEdgeWheel: deltaY => this.viewportFitController.scrollOuterBy(deltaY),\n overflowChanged: state => {\n if (state.start !== this.overflowStart) this.overflowStart = state.start;\n if (state.end !== this.overflowEnd) this.overflowEnd = state.end;\n if (state.scrollable !== this.scrollable) this.scrollable = state.scrollable;\n },\n });\n private readonly loadController = new TableLoadController({\n state: () => ({\n lazyLoading: this.dataMode === 'infinite' && !this.grouped,\n loadMoreMode: this.loadMoreMode,\n hasMore: this.hasMore,\n loadingMore: this.loadingMore,\n loadMoreError: this.loadMoreError,\n loadIdentity: this.loadIdentity,\n loadMoreThreshold: this.loadMoreThreshold,\n containedScroll: this.containedScroll,\n loadingMoreLabel: this.loadingMoreLabel,\n endOfResultsLabel: this.endOfResultsLabel,\n rowsLoadedLabel: this.rowsLoadedLabel,\n loadedRowCount: this.currentLoadedRowCount(),\n viewport: this.viewportEl,\n sentinel: this.sentinelEl,\n }),\n announce: message => {\n this.announcement = message;\n },\n request: detail => {\n this.dsLoadMore.emit(detail);\n },\n });\n private readonly groupLoadController = new TableGroupLoadController({\n state: () => ({\n enabled: this.grouped && this.dataMode !== 'virtual',\n loadMoreMode: this.loadMoreMode,\n loadMoreThreshold: this.loadMoreThreshold,\n containedScroll: this.containedScroll,\n groups: this.groups,\n viewport: this.viewportEl,\n sentinels: this.groupSentinelEls,\n loadingMoreLabel: this.groupLoadingMoreLabel,\n endOfResultsLabel: this.groupEndOfResultsLabel,\n rowsLoadedLabel: this.groupRowsLoadedLabel,\n }),\n announce: message => {\n this.announcement = message;\n },\n request: detail => {\n this.dsGroupLoadMore.emit(detail);\n },\n });\n private readonly virtualController = new TableVirtualController({\n state: () => ({\n enabled: this.rowWindowingEnabled,\n items: this.virtualItems,\n pinnedRowIds: this.virtualPinnedRowIds(),\n viewport: this.viewportEl,\n viewportSize: this.estimateVirtualViewportSize(),\n }),\n windowChanged: plan => {\n this.virtualWindow = plan;\n },\n });\n private readonly viewportFitController = new TableViewportFitController({\n enabled: () => this.fitViewport,\n elements: () => ({ host: this.el, surface: this.rootEl }),\n insets: () => ({\n blockStart: this.viewportInsetBlockStart,\n blockEnd: this.viewportInsetBlockEnd,\n }),\n fitChanged: (metrics: TableViewportFitMetrics | null) => {\n const settled = metrics?.settled ?? false;\n if (settled !== this.viewportFitSettled) this.viewportFitSettled = settled;\n },\n });\n\n componentWillLoad(): void {\n // Reserve the final viewport-fit height before the first paint. The fitted\n // surface is connected after render, but the host can already resolve its\n // owning scrollport and prevent an initial auto-height frame.\n this.viewportFitController.connect();\n this.incrementalWindowActive = this.hasIncrementalState;\n this.syncHeaderSlotPresence();\n this.syncFooterSlotPresence();\n this.loadController.initialize();\n this.groupLoadController.initialize();\n }\n\n componentDidLoad(): void {\n this.hasLoaded = true;\n this.layoutController.connect();\n this.loadController.connect();\n this.groupLoadController.connect();\n this.virtualController.connect();\n this.viewportFitController.connect();\n this.syncStickyGroupConnection();\n this.connectHeaderSlotObserver();\n this.connectFooterSlotObserver();\n this.connectFitObserver();\n this.connectCaptionCompactObserver();\n this.syncFitPageSize();\n this.connectTruncateTooltip();\n this.scheduleInitialModelIssueWarning();\n }\n\n componentDidRender(): void {\n this.layoutController.refresh(false);\n this.loadController.refresh();\n this.groupLoadController.refresh();\n this.virtualController.refresh();\n this.virtualController.collectMeasurements(this.tableEl);\n if (\n this.rowWindowingEnabled &&\n this.truncateAnchor &&\n this.tableEl &&\n !this.tableEl.contains(this.truncateAnchor)\n ) {\n this.dismissTruncateTooltip();\n }\n this.viewportFitController.refresh(false);\n this.syncStickyGroupConnection();\n this.connectFitObserver();\n this.connectCaptionCompactObserver();\n this.syncFitPageSize();\n if (this.stickyGroupConnected) this.updateStickyGroup();\n }\n\n /**\n * Scroll the nearest vertical owner only enough to reveal a supplied row.\n * Resolves false when the row is absent from the supplied, expanded table model.\n * This method never moves focus.\n */\n @Method()\n async scrollRowIntoView(rowId: string): Promise<boolean> {\n if (!rowId) return false;\n\n if (this.rowWindowingEnabled) {\n const headerSize = this.tableEl?.tHead?.getBoundingClientRect().height ?? 0;\n const revealed = this.virtualController.scrollRowIntoView(\n rowId,\n headerSize,\n this.stickyHeader ? headerSize : 0\n );\n if (!revealed) return false;\n await this.nextAnimationFrame();\n }\n\n const row = this.findRenderedRow(rowId);\n if (!row) return false;\n this.revealRenderedRow(row);\n return true;\n }\n\n connectedCallback(): void {\n if (!this.hasLoaded) return;\n this.layoutController.connect();\n this.loadController.connect();\n this.groupLoadController.connect();\n this.virtualController.connect();\n this.viewportFitController.connect();\n this.syncStickyGroupConnection();\n this.connectHeaderSlotObserver();\n this.connectFooterSlotObserver();\n this.connectFitObserver();\n this.connectCaptionCompactObserver();\n this.connectTruncateTooltip();\n }\n\n disconnectedCallback(): void {\n this.layoutController.disconnect();\n this.loadController.disconnect();\n this.groupLoadController.disconnect();\n this.virtualController.disconnect();\n this.viewportFitController.disconnect();\n this.disconnectStickyGroup();\n this.headerSlotObserver?.disconnect();\n this.headerSlotObserver = null;\n this.footerSlotObserver?.disconnect();\n this.footerSlotObserver = null;\n this.disconnectFitObserver();\n this.disconnectCaptionCompactObserver();\n this.disconnectTruncateTooltip();\n this.closeColumnCustomizer();\n if (this.initialModelWarningFrame !== undefined) {\n cancelAnimationFrame(this.initialModelWarningFrame);\n this.initialModelWarningFrame = undefined;\n }\n }\n\n private syncHeaderSlotPresence = () => {\n const header = this.el.querySelector<HTMLElement>('[slot=\"header\"]');\n this.headerPresent = !!header;\n this.headerUsesToolbar = header?.tagName === 'DS-DATA-TOOLBAR';\n };\n\n private connectHeaderSlotObserver(): void {\n if (this.headerSlotObserver || typeof MutationObserver === 'undefined') return;\n this.headerSlotObserver = new MutationObserver(this.syncHeaderSlotPresence);\n this.headerSlotObserver.observe(this.el, { childList: true });\n this.syncHeaderSlotPresence();\n }\n\n private syncFooterSlotPresence = () => {\n let presence = 0;\n if (hasOwnedTableFooterSlot(this.el, 'footer-leading')) presence |= TABLE_FOOTER_SLOT_LEADING;\n if (hasOwnedTableFooterSlot(this.el, 'footer')) presence |= TABLE_FOOTER_SLOT_COPY;\n if (hasOwnedTableFooterSlot(this.el, 'footer-trailing')) presence |= TABLE_FOOTER_SLOT_TRAILING;\n if (presence !== this.footerSlotPresence) this.footerSlotPresence = presence;\n };\n\n private connectFooterSlotObserver(): void {\n if (this.footerSlotObserver || typeof MutationObserver === 'undefined') return;\n this.footerSlotObserver = new MutationObserver(this.syncFooterSlotPresence);\n this.footerSlotObserver.observe(this.el, {\n childList: true,\n subtree: true,\n attributes: true,\n attributeFilter: ['slot'],\n });\n this.syncFooterSlotPresence();\n }\n\n private connectStickyGroup(): void {\n if (this.stickyGroupConnected || typeof window === 'undefined') return;\n this.stickyGroupConnected = true;\n document.addEventListener('scroll', this.updateStickyGroup, {\n capture: true,\n passive: true,\n });\n window.addEventListener('resize', this.updateStickyGroup, { passive: true });\n this.updateStickyGroup();\n }\n\n private syncStickyGroupConnection(): void {\n if (this.documentStickyHeader && this.grouped) {\n this.connectStickyGroup();\n return;\n }\n\n this.disconnectStickyGroup();\n this.frameEl?.style.removeProperty('--_table-sticky-group-top');\n if (this.activeStickyGroupId !== null) {\n this.activeStickyGroupId = null;\n }\n }\n\n private disconnectStickyGroup(): void {\n if (!this.stickyGroupConnected || typeof window === 'undefined') return;\n this.stickyGroupConnected = false;\n document.removeEventListener('scroll', this.updateStickyGroup, true);\n window.removeEventListener('resize', this.updateStickyGroup);\n }\n\n private readonly updateStickyGroup = (): void => {\n if (!this.documentStickyHeader || !this.grouped || !this.frameEl || !this.tableEl) {\n if (this.activeStickyGroupId !== null) this.activeStickyGroupId = null;\n return;\n }\n\n const stickyHeader = this.frameEl.querySelector<HTMLElement>(\n '.ds-table__document-sticky-header'\n );\n if (!stickyHeader) return;\n const threshold = stickyHeader.getBoundingClientRect().bottom;\n let activeGroupId: string | null = null;\n let activeGroupTop = threshold;\n for (const body of this.tableEl.querySelectorAll<HTMLElement>('tbody[data-group-id]')) {\n const source = body.querySelector<HTMLElement>('.ds-table__group-cell');\n if (!source) continue;\n const sourceRect = source.getBoundingClientRect();\n const bodyRect = body.getBoundingClientRect();\n if (sourceRect.top <= threshold && bodyRect.bottom > threshold) {\n activeGroupId = body.dataset.groupId ?? null;\n // A sticky section is bounded by its own row group. As that boundary\n // approaches the sticky lane, move the outgoing section bar with it so\n // the incoming source bar pushes it away instead of rendering beneath it.\n activeGroupTop = Math.min(threshold, bodyRect.bottom - sourceRect.height);\n }\n }\n\n const frameRect = this.frameEl.getBoundingClientRect();\n this.frameEl.style.setProperty(\n '--_table-sticky-group-top',\n `${activeGroupTop - frameRect.top}px`\n );\n\n if (activeGroupId !== this.activeStickyGroupId) {\n this.activeStickyGroupId = activeGroupId;\n return;\n }\n };\n\n @Watch('columns')\n @Watch('grouping')\n @Watch('hiddenFieldIds')\n @Watch('fieldOrder')\n @Watch('columnCustomizer')\n handleStructureChange(): void {\n this.scheduleModelIssueWarning();\n this.loadController.structureChanged();\n this.groupLoadController.structureChanged();\n this.virtualController.invalidateMeasures();\n }\n\n @Watch('rows')\n @Watch('groups')\n handleDataChange(): void {\n if (this.grouped) this.groupLoadController.dataChanged();\n else this.loadController.dataChanged();\n this.scheduleModelIssueWarning();\n this.syncActionMenu();\n this.virtualController.invalidateMeasures();\n }\n\n @Watch('totalCount')\n handleTotalCountChange(): void {\n this.scheduleModelIssueWarning();\n }\n\n @Watch('caption')\n handleCaptionChange(): void {\n this.scheduleModelIssueWarning();\n }\n\n @Watch('loadIdentity')\n handleLoadIdentityChange(): void {\n this.incrementalWindowActive = this.hasIncrementalState;\n this.loadController.identityChanged();\n this.virtualController.resetToTop();\n }\n\n @Watch('sort')\n @Watch('grouping')\n handleVirtualIndexReset(): void {\n this.virtualController.resetToTop();\n }\n\n @Watch('loadMoreMode')\n @Watch('loadMoreThreshold')\n handleLazyConfigurationChange(): void {\n this.loadController.configurationChanged();\n this.groupLoadController.configurationChanged();\n }\n\n @Watch('dataMode')\n handleDataModeChange(): void {\n this.incrementalWindowActive = this.hasIncrementalState;\n this.scheduleModelIssueWarning();\n this.handleLazyConfigurationChange();\n this.virtualController.resetToTop();\n }\n\n @Watch('pagination')\n handlePaginationChange(\n pagination: TablePaginationState | null,\n previous: TablePaginationState | null\n ): void {\n if (pagination?.pageSizeMode === 'fit' && pagination.fitIdentity !== previous?.fitIdentity) {\n this.fitMeasurementPending = true;\n }\n }\n\n @Watch('loadingMore')\n handleLoadingMoreChange(loading: boolean): void {\n if (loading) this.incrementalWindowActive = true;\n if (!this.grouped) this.loadController.loadingChanged(loading);\n }\n\n @Watch('loadMoreError')\n handleLoadMoreErrorChange(error: string | undefined): void {\n if (error?.trim()) this.incrementalWindowActive = true;\n if (!this.grouped) this.loadController.errorChanged(error);\n }\n\n @Watch('hasMore')\n handleHasMoreChange(hasMore: boolean, hadMore: boolean): void {\n if (hasMore) this.incrementalWindowActive = true;\n if (!this.grouped) this.loadController.hasMoreChanged(hasMore, hadMore);\n }\n\n @Watch('columnCustomizer')\n @Watch('captionVisibility')\n handleColumnCustomizerAvailability(): void {\n if (!this.showsColumnCustomizer) {\n this.closeColumnCustomizer();\n this.columnCustomizerSurfaceOpen = false;\n }\n }\n\n @Watch('dataModeSwitcher')\n @Watch('captionVisibility')\n handleDataModeSwitcherAvailability(): void {\n if (!this.showsDataModeSwitcher) {\n this.closeDataModeSwitcher();\n this.dataModeSwitcherSurfaceOpen = false;\n }\n }\n\n @Watch('chromeLoading')\n handleChromeLoadingChange(loading: boolean): void {\n if (!loading) return;\n this.closeColumnCustomizer();\n this.closeDataModeSwitcher();\n this.columnCustomizerSurfaceOpen = false;\n this.dataModeSwitcherSurfaceOpen = false;\n }\n\n private get grouped(): boolean {\n return this.grouping !== null;\n }\n\n private get hasIncrementalState(): boolean {\n return (\n this.dataMode === 'infinite' &&\n (this.hasMore || this.loadingMore || !!this.loadMoreError?.trim())\n );\n }\n\n private get selectable(): boolean {\n return this.selectionMode === 'multiple';\n }\n\n private get visibleColumns(): TableColumn[] {\n const cached = this.visibleColumnsCache;\n if (\n cached &&\n cached.columns === this.columns &&\n cached.columnCustomizer === this.columnCustomizer &&\n cached.hiddenFieldIds === this.hiddenFieldIds &&\n cached.fieldOrder === this.fieldOrder\n ) {\n return cached.value;\n }\n const value = resolveTableVisibleColumns(this.columns, {\n columnCustomizer: this.columnCustomizer,\n hiddenFieldIds: this.hiddenFieldIds,\n fieldOrder: this.fieldOrder,\n });\n this.visibleColumnsCache = {\n columns: this.columns,\n columnCustomizer: this.columnCustomizer,\n hiddenFieldIds: this.hiddenFieldIds,\n fieldOrder: this.fieldOrder,\n value,\n };\n return value;\n }\n\n private get showsColumnCustomizer(): boolean {\n return this.columnCustomizer && this.captionVisibility === 'visible';\n }\n\n private get showsDataModeSwitcher(): boolean {\n return this.dataModeSwitcher && this.captionVisibility === 'visible';\n }\n\n private get showsCaptionTrailing(): boolean {\n return (\n this.showsDataModeSwitcher ||\n (this.showsColumnCustomizer && !this.hideColumnCustomizerTrigger)\n );\n }\n\n private get documentStickyHeader(): boolean {\n return this.stickyHeader && !this.containedScroll;\n }\n\n private get fixedHeight(): boolean {\n return !this.fitViewport && this.resolvedHeight !== undefined;\n }\n\n private get boundedComposition(): boolean {\n return this.fitViewport || this.fixedHeight;\n }\n\n private get containedScroll(): boolean {\n return this.boundedComposition || this.resolvedMaxHeight !== undefined;\n }\n\n private connectFitObserver(): void {\n if (typeof ResizeObserver === 'undefined') return;\n if (this.fitObservedViewport === this.viewportEl && this.fitObservedTable === this.tableEl) {\n return;\n }\n this.disconnectFitObserver();\n this.fitObservedViewport = this.viewportEl;\n this.fitObservedTable = this.tableEl;\n this.fitResizeObserver = new ResizeObserver(this.syncFitPageSize);\n if (this.fitObservedViewport) this.fitResizeObserver.observe(this.fitObservedViewport);\n if (this.fitObservedTable) this.fitResizeObserver.observe(this.fitObservedTable);\n }\n\n private disconnectFitObserver(): void {\n this.fitResizeObserver?.disconnect();\n this.fitResizeObserver = null;\n this.fitObservedViewport = null;\n this.fitObservedTable = null;\n }\n\n private connectCaptionCompactObserver(): void {\n if (this.captionCompactDisconnect) return;\n this.captionCompactDisconnect = observeTableCaptionCompact(this.el, compact => {\n if (this.captionCompact !== compact) this.captionCompact = compact;\n });\n }\n\n private disconnectCaptionCompactObserver(): void {\n this.captionCompactDisconnect?.();\n this.captionCompactDisconnect = undefined;\n }\n\n private readonly syncFitPageSize = (): void => {\n const pagination = this.dataMode === 'pagination' ? this.pagination : null;\n if (\n !pagination?.fitToPage ||\n pagination.fitToPageInactive ||\n !this.containedScroll ||\n !this.viewportEl ||\n !this.tableEl\n ) {\n if (this.fitPageSize !== undefined) this.fitPageSize = undefined;\n this.fitMeasurementPending = false;\n return;\n }\n const header = this.tableEl.querySelector<HTMLElement>('.ds-table__head .ds-table__header-row');\n const item = this.tableEl.querySelector<HTMLElement>(\n this.grouped ? '.ds-table__group-row' : '.ds-table__body .ds-table__row'\n );\n const itemBlockSize =\n item?.getBoundingClientRect().height ||\n resolveCssLengthPx(\n 'var(--ds-table-row-min-block-size, var(--dimension-size-500))',\n 0,\n this.rootEl ?? this.el\n );\n const next = resolveTableFitPageSize({\n viewportBlockSize: this.viewportEl.clientHeight,\n headerBlockSize: header?.getBoundingClientRect().height ?? 0,\n itemBlockSize,\n });\n if (next !== this.fitPageSize) this.fitPageSize = next;\n if (!this.fitMeasurementPending || pagination.pageSizeMode !== 'fit' || next === undefined) {\n return;\n }\n this.fitMeasurementPending = false;\n if (next === pagination.pageSize && pagination.pageIndex === 0) return;\n this.dsPaginationChange.emit({\n pageIndex: 0,\n pageSize: next,\n pageSizeMode: 'fit',\n totalItems: pagination.totalItems,\n pageSizeOptions: pagination.pageSizeOptions ?? [25, 50, 100, 200],\n fitToPage: true,\n fitToPageInactive: false,\n fitPageSize: next,\n fitPageSizeLabel: pagination.fitPageSizeLabel ?? 'Fit to page',\n fitPageSizeTriggerLabel: pagination.fitPageSizeTriggerLabel ?? 'Fit',\n itemLabel: pagination.itemLabel ?? 'items',\n pageSizeLabel: pagination.pageSizeLabel ?? 'Items',\n ariaLabel: pagination.ariaLabel ?? `${this.caption} pagination`,\n showFirstLastButtons: pagination.showFirstLastButtons ?? false,\n previousPageIndex: pagination.pageIndex,\n previousPageSize: pagination.pageSize,\n previousPageSizeMode: 'fit',\n reason: 'fit',\n });\n };\n\n @Listen('keydown', { target: 'window' })\n handleWindowPaginationKeyDown(event: KeyboardEvent): void {\n this.handlePaginationKeyDown(event, true);\n }\n\n private handlePaginationKeyDown(event: KeyboardEvent, fromWindow = false): void {\n const pagination = this.dataMode === 'pagination' ? this.pagination : null;\n if (\n !pagination ||\n this.loading ||\n event.defaultPrevented ||\n event.altKey ||\n event.ctrlKey ||\n event.metaKey ||\n event.shiftKey ||\n (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')\n ) {\n return;\n }\n\n if (paginationShortcutBlockedByPath(event.composedPath())) return;\n\n if (fromWindow) {\n const origin = event.composedPath()[0];\n const originNode = origin instanceof Text ? origin.parentNode : origin;\n if (\n !shouldHandleContainingPagePaginationShortcut({\n origin: originNode,\n table: this.el,\n eventPath: event.composedPath(),\n })\n ) {\n return;\n }\n }\n\n const state = resolvePaginationState(pagination);\n const pageIndex = event.key === 'ArrowLeft' ? state.pageIndex - 1 : state.pageIndex + 1;\n if (pageIndex < 0 || pageIndex >= state.totalPages) return;\n\n event.preventDefault();\n this.dsPaginationChange.emit({\n pageIndex,\n pageSize: state.pageSize,\n pageSizeMode: state.pageSizeMode,\n totalItems: state.totalItems,\n pageSizeOptions: state.pageSizeOptions,\n fitToPage: pagination.fitToPage ?? false,\n fitToPageInactive: pagination.fitToPageInactive ?? false,\n fitPageSize: this.fitPageSize ?? pagination.fitPageSize,\n fitPageSizeLabel: pagination.fitPageSizeLabel ?? 'Fit to page',\n fitPageSizeTriggerLabel: pagination.fitPageSizeTriggerLabel ?? 'Fit',\n itemLabel: pagination.itemLabel ?? 'items',\n pageSizeLabel: pagination.pageSizeLabel ?? 'Items',\n ariaLabel: pagination.ariaLabel ?? `${this.caption} pagination`,\n showFirstLastButtons: pagination.showFirstLastButtons ?? false,\n previousPageIndex: state.pageIndex,\n previousPageSize: state.pageSize,\n previousPageSizeMode: state.pageSizeMode,\n reason: 'page',\n });\n }\n\n private currentLoadedRows(): TableRow[] {\n return tableRows(this.rows, this.groups, this.grouped);\n }\n\n private currentLoadedRowCount(): number {\n return this.grouped\n ? this.groups.reduce((count, group) => count + group.rows.length, 0)\n : this.rows.length;\n }\n\n private createRenderModel(): TableRenderModel {\n const columns = this.visibleColumns;\n const grouped = this.grouped;\n const virtualCounts = this.dataMode === 'virtual';\n const cached = this.renderModelCache;\n if (\n cached &&\n cached.columns === columns &&\n cached.rows === this.rows &&\n cached.groups === this.groups &&\n cached.grouped === grouped &&\n cached.selectionMode === this.selectionMode &&\n cached.selectedRowIds === this.selectedRowIds &&\n cached.collapsedGroupIds === this.collapsedGroupIds &&\n cached.virtualCounts === virtualCounts\n ) {\n return cached.value;\n }\n const value = createTableRenderModel({\n columns,\n rows: this.rows,\n groups: this.groups,\n grouped,\n selectionMode: this.selectionMode,\n selectedRowIds: this.selectedRowIds,\n collapsedGroupIds: this.collapsedGroupIds,\n groupCountPresentation: virtualCounts ? 'total' : 'loaded-progress',\n });\n this.renderModelCache = {\n columns,\n rows: this.rows,\n groups: this.groups,\n grouped,\n selectionMode: this.selectionMode,\n selectedRowIds: this.selectedRowIds,\n collapsedGroupIds: this.collapsedGroupIds,\n virtualCounts,\n value,\n };\n return value;\n }\n\n private get highlightMatcher(): TableHighlightMatcher {\n if (this.highlightMatcherCache?.terms === this.highlightTerms) {\n return this.highlightMatcherCache.value;\n }\n const value = createTableHighlightMatcher(this.highlightTerms);\n this.highlightMatcherCache = { terms: this.highlightTerms, value };\n return value;\n }\n\n private get rowWindowingEnabled(): boolean {\n if (!this.hasVirtualViewportBounds) return false;\n if (this.dataMode === 'virtual') return true;\n return !this.grouped && this.currentLoadedRowCount() > TABLE_NON_VIRTUAL_WINDOW_THRESHOLD;\n }\n\n private get virtualViewportMissing(): boolean {\n return this.dataMode === 'virtual' && !this.hasVirtualViewportBounds;\n }\n\n private get hasVirtualViewportBounds(): boolean {\n return this.containedScroll && this.estimateVirtualViewportSize() > 0;\n }\n\n private estimateVirtualViewportSize(): number {\n const measured =\n this.virtualController.currentViewportSize() || this.viewportEl?.clientHeight || 0;\n const chrome =\n (this.captionVisibility === 'visible' ? 48 : 0) + (this.hasResultFooter ? 48 : 0);\n if (this.fixedHeight && this.resolvedHeight) {\n if (measured > 0) return measured;\n return Math.max(\n 0,\n resolveCssLengthPx(this.resolvedHeight, 0, this.rootEl ?? this.el) - chrome\n );\n }\n if (this.fitViewport) return Math.max(0, measured);\n if (this.resolvedMaxHeight) {\n if (this.resolvedMaxHeight.trim().toLowerCase() === 'none') return 0;\n return resolveCssLengthPx(this.resolvedMaxHeight, 0, this.rootEl ?? this.el);\n }\n return 0;\n }\n\n private virtualPinnedRowIds(): Set<string> {\n const pinned = new Set<string>();\n if (this.focusedRowId) pinned.add(this.focusedRowId);\n if (this.actionMenu?.rowId) pinned.add(this.actionMenu.rowId);\n return pinned;\n }\n\n private findRenderedRow(rowId: string): HTMLElement | null {\n return (\n [...this.el.querySelectorAll<HTMLElement>('[data-row-id]')].find(\n candidate => candidate.dataset.rowId === rowId\n ) ?? null\n );\n }\n\n private revealRenderedRow(row: HTMLElement): void {\n if (!this.containedScroll || !this.viewportEl) {\n row.scrollIntoView({ block: 'nearest', inline: 'nearest' });\n return;\n }\n\n const viewportRect = this.viewportEl.getBoundingClientRect();\n const rowRect = row.getBoundingClientRect();\n let visibleStart = viewportRect.top;\n if (this.stickyHeader) {\n visibleStart = Math.max(\n visibleStart,\n this.tableEl?.tHead?.getBoundingClientRect().bottom ?? visibleStart\n );\n }\n const groupRow = row.closest('tbody')?.querySelector<HTMLElement>('.ds-table__group-row');\n if (groupRow && getComputedStyle(groupRow).position === 'sticky') {\n visibleStart = Math.max(visibleStart, groupRow.getBoundingClientRect().bottom);\n }\n\n if (rowRect.top < visibleStart) {\n this.viewportEl.scrollTop += rowRect.top - visibleStart;\n } else if (rowRect.bottom > viewportRect.bottom) {\n this.viewportEl.scrollTop += rowRect.bottom - viewportRect.bottom;\n }\n }\n\n private nextAnimationFrame(): Promise<void> {\n return new Promise(resolve => requestAnimationFrame(() => resolve()));\n }\n\n private syncVirtualItems(model: TableRenderModel): void {\n if (!this.rowWindowingEnabled) {\n this.virtualItems = [];\n this.virtualItemsCache = null;\n this.bodyRenderer.resetVirtualState();\n return;\n }\n const columns = this.visibleColumns;\n const cached = this.virtualItemsCache;\n if (\n cached &&\n cached.columns === columns &&\n cached.rows === this.rows &&\n cached.groups === this.groups &&\n cached.grouped === model.grouped &&\n cached.collapsedGroupIds === this.collapsedGroupIds\n ) {\n return;\n }\n this.virtualItems = flattenTableVirtualItems({\n grouped: model.grouped,\n rows: this.rows,\n groups: this.groups,\n collapsedGroupIds: this.collapsedGroupIds,\n columns,\n });\n this.virtualItemsCache = {\n columns,\n rows: this.rows,\n groups: this.groups,\n grouped: model.grouped,\n collapsedGroupIds: this.collapsedGroupIds,\n };\n }\n\n private shouldWindowRows(model: TableRenderModel): boolean {\n return (\n this.rowWindowingEnabled && (model.hasData || (model.grouped && model.groups.length > 0))\n );\n }\n\n private get resolvedMaxHeight(): string | undefined {\n if (this.maxHeight == null || this.maxHeight === '') return undefined;\n return typeof this.maxHeight === 'number' ? `${Math.max(0, this.maxHeight)}px` : this.maxHeight;\n }\n\n private get resolvedHeight(): string | undefined {\n if (this.height == null || this.height === '') return undefined;\n return typeof this.height === 'number' ? `${Math.max(0, this.height)}px` : this.height;\n }\n\n private warnModelIssues(): void {\n const issues = tableModelIssues(this.columns, this.rows, this.groups, this.grouped);\n const visibleColumns = this.visibleColumns;\n if (!this.caption?.trim()) issues.unshift('A non-empty caption is required.');\n if (this.grouping) {\n const groupingColumn = this.columns.find(column => column.id === this.grouping!.fieldId);\n if (!groupingColumn) {\n issues.push(`Grouping references unknown column id: ${this.grouping.fieldId}`);\n } else if (!visibleColumns.some(column => column.id === groupingColumn.id)) {\n issues.push(`Grouping references hidden column id: ${this.grouping.fieldId}`);\n }\n }\n if (this.sort) {\n const sortColumn = this.columns.find(\n column =>\n column.id === this.sort!.fieldId ||\n column.segments?.some(segment => segment.sortKey === this.sort!.fieldId)\n );\n if (!sortColumn) {\n issues.push(`Sorting references unknown column id: ${this.sort.fieldId}`);\n } else if (!visibleColumns.some(column => column.id === sortColumn.id)) {\n issues.push(`Sorting references hidden column id: ${this.sort.fieldId}`);\n }\n }\n if (\n this.dataMode === 'virtual' &&\n Number.isFinite(this.totalCount) &&\n Math.max(0, Math.trunc(this.totalCount!)) !== this.currentLoadedRowCount()\n ) {\n issues.push('Virtual mode requires totalCount to match the complete supplied row count.');\n }\n const stickyStart = visibleColumns.filter(column => column.sticky === 'start');\n const stickyEnd = visibleColumns.filter(column => column.sticky === 'end');\n if (stickyStart.length > 1 || (this.selectable && stickyStart.length > 0)) {\n issues.push(\n 'Only one sticky start column is supported, and row selection already owns that lane.'\n );\n }\n if (stickyEnd.length > 1) issues.push('Only one sticky end column is supported.');\n for (const column of [...stickyStart, ...stickyEnd]) {\n if (!tableColumnSize(column))\n issues.push(`Sticky column ${column.id} requires an explicit size.`);\n }\n const message = issues.join(' ');\n if (!message) {\n this.previousModelWarning = '';\n return;\n }\n if (message === this.previousModelWarning) return;\n this.previousModelWarning = message;\n console.warn(`[ds-table] ${message}`);\n }\n\n private scheduleModelIssueWarning(): void {\n if (this.modelWarningQueued) return;\n this.modelWarningQueued = true;\n queueMicrotask(() => {\n this.modelWarningQueued = false;\n this.warnModelIssues();\n });\n }\n\n private scheduleInitialModelIssueWarning(): void {\n if (this.initialModelWarningFrame !== undefined) {\n cancelAnimationFrame(this.initialModelWarningFrame);\n }\n this.initialModelWarningFrame = requestAnimationFrame(() => {\n this.initialModelWarningFrame = undefined;\n this.warnModelIssues();\n });\n }\n\n private emitSort(column: TableColumn, sortKey = column.id): void {\n if (!column.sortable) return;\n this.dsSortChange.emit({ sort: nextDataSortState(this.sort, sortKey) });\n }\n\n private sortButtonLabel(column: TableColumn, sortKey = column.id, label = column.label): string {\n if (this.sort?.fieldId !== sortKey) return `Sort ${label} ascending`;\n if (this.sort.direction === 'asc') return `Sort ${label} descending. Currently ascending.`;\n return `Sort ${label} ascending. Currently descending.`;\n }\n\n private emitRowSelection(row: TableRow): void {\n const next = toggleTableRowSelection(this.selectedRowIds, row);\n const selected = next.includes(row.id);\n this.dsSelectionChange.emit({\n selectedRowIds: next,\n scope: 'row',\n changedRowId: row.id,\n selected,\n });\n }\n\n private emitAllSelection(): void {\n const loadedRows = this.currentLoadedRows();\n const selection = deriveTableSelectionState(loadedRows, this.selectedRowIds);\n this.dsSelectionChange.emit({\n selectedRowIds: toggleAllLoadedTableRows(this.selectedRowIds, loadedRows),\n scope: 'all-loaded',\n selected: !selection.allSelected,\n });\n }\n\n private emitGroupSelection(group: DataGroup): void {\n const state = deriveTableSelectionState(group.rows, this.selectedRowIds);\n this.dsSelectionChange.emit({\n selectedRowIds: toggleTableGroupSelection(this.selectedRowIds, group.rows),\n scope: 'group',\n groupId: group.id,\n selected: !state.allSelected,\n });\n }\n\n private rowEventOwnsActivation(event: Event): boolean {\n const currentTarget = event.currentTarget;\n return !event.composedPath().some(target => {\n if (!(target instanceof HTMLElement) || target === currentTarget) return false;\n return target.matches(\n 'button, a, input, select, textarea, [role=\"button\"], [role=\"checkbox\"], [popover], ds-button-unfilled, ds-menu'\n );\n });\n }\n\n private groupRowEventOwnsCollapse(event: Event): boolean {\n const currentTarget = event.currentTarget;\n return !event.composedPath().some(target => {\n if (!(target instanceof HTMLElement) || target === currentTarget) return false;\n return target.matches(\n 'input, select, textarea, [role=\"checkbox\"], .ds-table__selection-control, .ds-table__group-selection, ds-tooltip'\n );\n });\n }\n\n private handleGroupRowClick(group: DataGroup, event: Event): void {\n if (!this.groupRowEventOwnsCollapse(event)) return;\n this.emitGroupCollapse(group);\n }\n\n private emitRowActivation(row: TableRow, event: Event): void {\n if (row.disabled || !this.rowEventOwnsActivation(event)) return;\n if (this.selectionMode === 'multiple' && this.selectedRowIds.length > 0) {\n if (row.selectable !== false) this.emitRowSelection(row);\n return;\n }\n if (row.interactive) this.dsRowActivate.emit({ rowId: row.id });\n }\n\n private syncActionMenu(): void {\n if (!this.actionMenu) return;\n const row = tableRows(this.rows, this.groups, this.grouped).find(\n candidate => candidate.id === this.actionMenu?.rowId\n );\n const value = row && !row.disabled ? row.cells[this.actionMenu.columnId] : undefined;\n if (!isTableCellAction(value) || !isRenderableTableActionMenu(value)) {\n this.actionMenu = null;\n }\n }\n\n private closeActionMenu(): void {\n this.actionMenu = null;\n }\n\n private handleActionMenuSelect(item: MenuItemData): void {\n const open = this.actionMenu;\n const actionId = item.value;\n if (!open || !actionId || item.isInactive) return;\n this.dsCellAction.emit({\n actionId,\n rowId: open.rowId,\n columnId: open.columnId,\n });\n const triggerId = tableActionTriggerId(this.actionMenuElementId, open.rowId, open.columnId);\n this.closeActionMenu();\n requestAnimationFrame(() => {\n const trigger = this.el.querySelector<HTMLElement & { setFocus?: () => void }>(\n `#${CSS.escape(triggerId)}`\n );\n trigger?.setFocus?.();\n });\n }\n\n private toggleActionMenu(row: TableRow, column: TableColumn, event: MouseEvent): void {\n if (row.disabled) return;\n const open = this.actionMenu?.rowId === row.id && this.actionMenu?.columnId === column.id;\n this.actionMenuInitialFocusVisible = event.detail === 0;\n this.actionMenu = open ? null : { rowId: row.id, columnId: column.id };\n if (this.actionMenu) this.actionMenuSurface = this.actionMenu;\n }\n\n private renderOverflowActionMenu() {\n const open = this.actionMenu ?? this.actionMenuSurface;\n const row = open\n ? tableRows(this.rows, this.groups, this.grouped).find(\n candidate => candidate.id === open.rowId\n )\n : undefined;\n const value = row && open ? row.cells[open.columnId] : undefined;\n const menu = isTableCellAction(value) && isRenderableTableActionMenu(value) ? value : undefined;\n const sections = menu ? tableActionMenuSections(menu.items) : [];\n const triggerId = open\n ? tableActionTriggerId(this.actionMenuElementId, open.rowId, open.columnId)\n : undefined;\n\n return (\n <ds-menu\n id={this.actionMenuElementId}\n open={Boolean(this.actionMenu && sections.length)}\n anchorId={triggerId}\n align=\"end\"\n side=\"bottom\"\n menuLabel={menu?.ariaLabel ?? 'More actions'}\n initialFocusVisible={this.actionMenuInitialFocusVisible}\n sections={sections}\n onDsClose={() => this.closeActionMenu()}\n onDsAfterClose={() => {\n if (!this.actionMenu) this.actionMenuSurface = null;\n }}\n onDsSelect={event => this.handleActionMenuSelect(event.detail)}\n />\n );\n }\n\n private connectTruncateTooltip(): void {\n if (this.truncateTooltipBound) return;\n this.el.addEventListener('pointerover', this.onTruncatePointerOver);\n this.el.addEventListener('pointerout', this.onTruncatePointerOut);\n this.el.addEventListener('focusin', this.onTruncateFocusIn);\n this.el.addEventListener('focusout', this.onTruncateFocusOut);\n this.truncateTooltipBound = true;\n }\n\n private disconnectTruncateTooltip(): void {\n if (!this.truncateTooltipBound) return;\n this.el.removeEventListener('pointerover', this.onTruncatePointerOver);\n this.el.removeEventListener('pointerout', this.onTruncatePointerOut);\n this.el.removeEventListener('focusin', this.onTruncateFocusIn);\n this.el.removeEventListener('focusout', this.onTruncateFocusOut);\n this.truncateTooltipBound = false;\n this.dismissTruncateTooltip();\n }\n\n private onTruncatePointerOver = (event: PointerEvent) => {\n if (event.pointerType === 'touch') return;\n const track = resolveTableTruncateTrack(event.target);\n if (!track) return;\n this.presentOrDismissTruncateTrack(track);\n };\n\n private onTruncatePointerOut = (event: PointerEvent) => {\n if (event.pointerType === 'touch') return;\n if (resolveTableTruncateTrack(event.relatedTarget)) return;\n const next = event.relatedTarget;\n const cell = this.truncateAnchor?.closest('.ds-table__cell');\n if (cell && next instanceof Node && cell.contains(next)) return;\n this.dismissTruncateTooltip();\n };\n\n private onTruncateFocusIn = (event: FocusEvent) => {\n const track = resolveTableTruncateTrack(event.target);\n if (!track) {\n this.dismissTruncateTooltip();\n return;\n }\n this.presentOrDismissTruncateTrack(track);\n };\n\n private onTruncateFocusOut = (event: FocusEvent) => {\n if (resolveTableTruncateTrack(event.relatedTarget)) return;\n this.dismissTruncateTooltip();\n };\n\n private presentOrDismissTruncateTrack(track: HTMLElement): void {\n if (!isElementTruncated(track)) {\n this.dismissTruncateTooltip();\n return;\n }\n const label = tableTruncateLabel(track);\n if (!label) {\n this.dismissTruncateTooltip();\n return;\n }\n this.truncateAnchor = track;\n this.truncateTooltipLabel = label;\n void this.truncateTooltipEl?.presentFrom(track, label);\n }\n\n private dismissTruncateTooltip(): void {\n this.truncateAnchor = null;\n if (!this.truncateTooltipLabel && !this.truncateTooltipEl) return;\n this.truncateTooltipLabel = '';\n void this.truncateTooltipEl?.dismiss();\n }\n\n private renderTruncateTooltip() {\n return (\n <ds-tooltip\n ref={element => {\n this.truncateTooltipEl = element;\n }}\n label={this.truncateTooltipLabel}\n describedBy={false}\n wrapLabel={true}\n size=\"sm\"\n delay={0}\n side=\"top\"\n />\n );\n }\n\n private handleRowKeydown(row: TableRow, event: KeyboardEvent): void {\n if (event.target !== event.currentTarget || (event.key !== 'Enter' && event.key !== ' '))\n return;\n event.preventDefault();\n this.emitRowActivation(row, event);\n }\n\n private onVirtualFocusIn = (event: FocusEvent): void => {\n const target = event.target;\n if (!(target instanceof HTMLElement)) return;\n const row = target.closest('[data-row-id]');\n const id = row?.getAttribute('data-row-id') ?? null;\n if (id === this.focusedRowId) return;\n this.focusedRowId = id;\n if (this.rowWindowingEnabled) this.virtualController.schedule();\n };\n\n private renderSelectionControl(\n label: string,\n checked: boolean,\n indeterminate: boolean,\n disabled: boolean,\n onActivate: () => void\n ) {\n return (\n <button\n class=\"ds-table__selection-control ds-focus-ring ds-interaction-fill__content\"\n type=\"button\"\n role=\"checkbox\"\n aria-label={label}\n aria-checked={indeterminate ? 'mixed' : String(checked)}\n disabled={disabled}\n onClick={onActivate}\n >\n <ds-checkbox\n label=\"\"\n size=\"md\"\n checked={checked}\n indeterminate={indeterminate}\n presentation={true}\n />\n </button>\n );\n }\n\n private renderStickyEdge(sticky: TableColumn['sticky']) {\n if (!sticky) return null;\n return (\n <span class={`ds-table__sticky-edge ds-table__sticky-edge--${sticky}`} aria-hidden=\"true\" />\n );\n }\n\n private renderCollapseAllButton() {\n return (\n <ds-button-unfilled\n class=\"ds-table__collapse-all\"\n variant=\"icon\"\n icon=\"ChevronDownUp\"\n size=\"xs\"\n style={{ width: 'var(--dimension-size-300)' }}\n aria-label=\"Collapse all groups\"\n hasBorder={false}\n activeFill={false}\n pressScale={false}\n onDsClick={event => {\n event.stopPropagation();\n this.emitAllGroupsCollapse();\n }}\n />\n );\n }\n\n private renderFloatingCollapseAll(model: TableRenderModel) {\n if (model.collapseAllHost?.mode !== 'floating') return null;\n return (\n <span\n class=\"ds-table__collapse-all-overlay\"\n ref={element => {\n this.collapseAllOverlayEl = element ?? null;\n }}\n >\n <span class=\"ds-table__collapse-all-surface ds-control-elevation ds-control-elevation--md\">\n {this.renderCollapseAllButton()}\n </span>\n </span>\n );\n }\n\n private renderColumnHeader(\n column: TableColumn,\n model: TableRenderModel,\n interactive = true,\n presentational = false\n ) {\n const groupedColumn = this.grouping?.fieldId === column.id;\n const segments = column.segments?.length\n ? column.segments\n : [{ label: column.label, sortKey: column.id }];\n const activeMemberSegment = segments.find(segment => segment.sortKey === this.sort?.fieldId);\n const activeMemberSort = !!activeMemberSegment;\n const activeSort = activeMemberSort;\n const align = column.align ?? 'start';\n const direction = activeMemberSort ? this.sort!.direction : undefined;\n const memberAriaSort = activeMemberSort\n ? this.sort!.direction === 'asc'\n ? 'ascending'\n : 'descending'\n : undefined;\n\n const help = column.help?.trim();\n const labels = (\n <span\n class=\"ds-table__header-labels\"\n tabIndex={interactive && help && !column.sortable ? 0 : undefined}\n data-header-help={help ? '' : undefined}\n >\n {segments.map((segment, index) => {\n const segmentActive = activeMemberSegment?.sortKey === segment.sortKey;\n const segmentInteractive = interactive && !!column.sortable;\n const label = (\n <ds-text\n class=\"ds-table__header-label-box ds-control-label-box\"\n as=\"span\"\n variant=\"text-caption\"\n emphasis={segmentActive}\n color=\"inherit\"\n decoration={help ? 'dotted-underline' : undefined}\n lineTruncation={1}\n >\n {segment.label}\n </ds-text>\n );\n const control = segmentInteractive ? (\n <button\n class=\"ds-table__header-label ds-table__header-label--interactive ds-focus-ring\"\n type=\"button\"\n aria-label={this.sortButtonLabel(\n column,\n segment.sortKey,\n segment.dataLabel?.trim() || segment.label\n )}\n data-sort-control=\"label\"\n data-sort-key={segment.sortKey}\n data-sort-active={segmentActive ? 'true' : undefined}\n onClick={() => this.emitSort(column, segment.sortKey)}\n >\n {label}\n </button>\n ) : (\n <span class=\"ds-table__header-label ds-table__header-static\">{label}</span>\n );\n\n return (\n <span class=\"ds-table__header-segment\" key={segment.sortKey}>\n {control}\n {index < segments.length - 1 && (\n <ds-text\n class=\"ds-table__header-separator\"\n as=\"span\"\n variant=\"text-caption\"\n color=\"tertiary\"\n aria-hidden=\"true\"\n >\n {segment.separator ?? '/'}\n </ds-text>\n )}\n </span>\n );\n })}\n </span>\n );\n const labelControl =\n interactive && help ? (\n <ds-tooltip label={help} side=\"top\" size=\"sm\" wrapLabel={true}>\n {labels}\n </ds-tooltip>\n ) : (\n labels\n );\n const sortControl = interactive ? (\n <span class=\"ds-table__sort-slot\">\n {activeSort && (\n <ds-button-unfilled\n class=\"ds-table__sort-direction\"\n variant=\"icon\"\n icon={direction === 'asc' ? 'ArrowUp' : 'ArrowDown'}\n size=\"xs\"\n aria-label={this.sortButtonLabel(\n column,\n activeMemberSegment?.sortKey ?? column.id,\n activeMemberSegment?.label ?? column.label\n )}\n hasBorder={false}\n activeFill={false}\n isActive={true}\n pressScale={false}\n data-sort-control=\"direction\"\n onDsClick={() => this.emitSort(column, activeMemberSegment?.sortKey ?? column.id)}\n />\n )}\n </span>\n ) : null;\n const collapseHost = model.collapseAllHost;\n const actionCollapseHost =\n collapseHost?.mode === 'action' && collapseHost.columnId === column.id;\n const blankActionCollapseHost =\n actionCollapseHost && !column.label.trim() && !column.segments?.length;\n const collapseControl =\n interactive && actionCollapseHost ? (\n <span class=\"ds-table__collapse-slot\">{this.renderCollapseAllButton()}</span>\n ) : null;\n\n return (\n <th\n key={column.id}\n class={{\n 'ds-table__header-cell': true,\n [`ds-table__cell--align-${align}`]: true,\n 'ds-table__cell--sticky-start': column.sticky === 'start',\n 'ds-table__cell--sticky-end': column.sticky === 'end',\n 'ds-table__header-cell--collapse-all': actionCollapseHost,\n }}\n scope={presentational ? undefined : 'col'}\n aria-sort={presentational ? undefined : memberAriaSort}\n data-column-id={column.id}\n data-grouped={groupedColumn ? 'true' : undefined}\n data-sort-active={activeSort ? 'true' : undefined}\n >\n {!column.label.trim() && column.accessibleLabel?.trim() && (\n <span class=\"ds-visually-hidden\">{column.accessibleLabel}</span>\n )}\n {blankActionCollapseHost ? (\n <span class=\"ds-table__header-content ds-table__header-content--collapse-all\">\n {collapseControl}\n </span>\n ) : (\n <span class=\"ds-table__header-content\">\n {align === 'end' && sortControl}\n {align === 'center' && (\n <span\n class=\"ds-table__sort-slot ds-table__sort-slot--balance\"\n data-sort-balance=\"true\"\n aria-hidden=\"true\"\n />\n )}\n {labelControl}\n {align !== 'end' && sortControl}\n {collapseControl}\n </span>\n )}\n {this.renderStickyEdge(column.sticky)}\n </th>\n );\n }\n\n private renderColgroup(model: TableRenderModel) {\n const beforeSpacer =\n model.elasticSpacerIndex == null\n ? this.visibleColumns\n : this.visibleColumns.slice(0, model.elasticSpacerIndex);\n const afterSpacer =\n model.elasticSpacerIndex == null ? [] : this.visibleColumns.slice(model.elasticSpacerIndex);\n const renderColumn = (column: TableColumn) => {\n const width = tableColumnSize(column);\n return (\n <col\n key={column.id}\n class={{ 'ds-table__action-column': column.kind === 'action' }}\n style={width ? { width } : undefined}\n />\n );\n };\n return (\n <colgroup>\n {model.selectable && <col class=\"ds-table__selection-column\" />}\n {beforeSpacer.map(renderColumn)}\n {model.elasticSpacerIndex != null && <col class=\"ds-table__elastic-spacer-column\" />}\n {afterSpacer.map(renderColumn)}\n </colgroup>\n );\n }\n\n private renderHeader(\n model: TableRenderModel,\n interactive = true,\n presentational = false,\n ariaRowIndex?: number\n ) {\n const selection = model.selection;\n const beforeSpacer =\n model.elasticSpacerIndex == null\n ? this.visibleColumns\n : this.visibleColumns.slice(0, model.elasticSpacerIndex);\n const afterSpacer =\n model.elasticSpacerIndex == null ? [] : this.visibleColumns.slice(model.elasticSpacerIndex);\n return (\n <thead\n class={{\n 'ds-table__head': true,\n 'ds-table__head--semantic-copy': !interactive,\n }}\n ref={element => {\n if (interactive) this.interactiveHeadEl = element ?? null;\n }}\n >\n <tr class=\"ds-table__header-row\" aria-rowindex={ariaRowIndex}>\n {model.selectable && (\n <th\n class=\"ds-table__header-cell ds-table__selection-cell ds-table__cell--sticky-start\"\n scope={presentational ? undefined : 'col'}\n >\n {interactive ? (\n this.renderSelectionControl(\n selection.allSelected ? 'Deselect all loaded rows' : 'Select all loaded rows',\n selection.allSelected,\n selection.indeterminate,\n selection.selectableRowIds.length === 0,\n () => this.emitAllSelection()\n )\n ) : (\n <span class=\"ds-visually-hidden\">Select rows</span>\n )}\n {this.renderStickyEdge('start')}\n </th>\n )}\n {beforeSpacer.map(column =>\n this.renderColumnHeader(column, model, interactive, presentational)\n )}\n {model.elasticSpacerIndex != null && (\n <th\n class=\"ds-table__header-cell ds-table__elastic-spacer-cell\"\n aria-hidden=\"true\"\n role=\"presentation\"\n data-elastic-spacer=\"true\"\n />\n )}\n {afterSpacer.map(column =>\n this.renderColumnHeader(column, model, interactive, presentational)\n )}\n </tr>\n </thead>\n );\n }\n\n private renderRow(\n row: TableRow,\n model: TableRenderModel,\n ariaRowIndex?: number,\n variableVirtualSize = false,\n rowKey = row.id\n ) {\n return renderTableRowView({\n row,\n model,\n visibleColumns: this.visibleColumns,\n emptyCellLabel: this.emptyCellLabel,\n actionMenuElementId: this.actionMenuElementId,\n actionMenu: this.actionMenu,\n actionMenuSurface: this.actionMenuSurface,\n surfaceOpenRowId: this.surfaceOpenRowId,\n highlightMatcher: this.highlightMatcher,\n highlightFieldIds: this.highlightFieldIds,\n ariaRowIndex,\n variableVirtualSize,\n intrinsicBlockSize:\n this.dataMode !== 'virtual'\n ? estimateTableRowBlockSize(row, this.visibleColumns)\n : undefined,\n rowKey,\n renderSelectionControl: (label, checked, indeterminate, disabled, onActivate) =>\n this.renderSelectionControl(label, checked, indeterminate, disabled, onActivate),\n renderStickyEdge: sticky => this.renderStickyEdge(sticky),\n onRowActivate: (targetRow, event) => this.emitRowActivation(targetRow, event),\n onRowKeyDown: (targetRow, event) => this.handleRowKeydown(targetRow, event),\n onRowSelection: targetRow => this.emitRowSelection(targetRow),\n onCellAction: detail => this.dsCellAction.emit(detail),\n onActionMenuToggle: (targetRow, column, event) =>\n this.toggleActionMenu(targetRow, column, event),\n });\n }\n\n private emitGroupCollapse(group: DataGroup) {\n const collapsedGroupIds = toggleTableGroupCollapsed(this.collapsedGroupIds, group.id);\n this.dsGroupCollapseChange.emit({\n scope: 'group',\n groupId: group.id,\n collapsed: collapsedGroupIds.includes(group.id),\n collapsedGroupIds,\n });\n }\n\n private emitAllGroupsCollapse() {\n const groupIds = this.groups.map(group => group.id);\n const collapsedGroupIds = nextTableGroupsCollapsed(this.collapsedGroupIds, groupIds);\n this.dsGroupCollapseChange.emit({\n scope: 'all',\n collapsed: collapsedGroupIds.length > 0,\n collapsedGroupIds,\n });\n }\n\n private renderGroupAccessory(\n item: TableRenderModel['groups'][number]['accessories'][number],\n index: number\n ) {\n const text = (\n <ds-text\n key={`acc-${index}`}\n class=\"ds-table__group-accessory\"\n as=\"span\"\n variant=\"text-body-small\"\n color=\"secondary\"\n decoration={item.help ? 'dotted-underline' : undefined}\n lineTruncation={1}\n >\n {item.text}\n </ds-text>\n );\n if (!item.help) return text;\n return (\n <ds-tooltip\n key={`acc-${index}`}\n label={item.help}\n side=\"bottom\"\n align=\"start\"\n size=\"sm\"\n wrapLabel={true}\n >\n <span class=\"ds-table__group-accessory-tip\" tabIndex={0}>\n {text}\n </span>\n </ds-tooltip>\n );\n }\n\n private renderGroupContent(groupModel: TableRenderModel['groups'][number]) {\n const {\n group,\n countLabel,\n collapsed: isCollapsed,\n labelColor,\n selection: groupSelection,\n accessories,\n hero,\n } = groupModel;\n const heroTracks = hero?.kind === 'score' && hero.tracks === 2 ? 2 : 1;\n const heroPresentation = TABLE_GROUP_HERO_SCORE_PRESENTATION[heroTracks];\n return (\n <span\n class={{\n 'ds-table__group-content': true,\n 'ds-table__group-content--multi': accessories.length > 0 || heroTracks === 2,\n 'ds-table__group-content--hero': Boolean(hero),\n 'ds-table__group-content--hero-two-track': Boolean(hero) && heroTracks === 2,\n }}\n onClick={event => this.handleGroupRowClick(group, event)}\n >\n {groupSelection && (\n <span class=\"ds-table__group-selection\">\n {this.renderSelectionControl(\n groupSelection.allSelected\n ? `Deselect loaded rows in ${group.label} group`\n : `Select loaded rows in ${group.label} group`,\n groupSelection.allSelected,\n groupSelection.indeterminate,\n groupSelection.selectableRowIds.length === 0,\n () => this.emitGroupSelection(group)\n )}\n </span>\n )}\n {hero?.kind === 'score' && (\n <span class=\"ds-table__group-hero\">\n <ds-score\n size={heroPresentation.size}\n variant={heroPresentation.variant}\n value={hero.value}\n level={hero.level}\n label={hero.label ?? 'Safety score'}\n isLoading={hero.isLoading === true}\n />\n </span>\n )}\n <span class=\"ds-table__group-copy\">\n <span class=\"ds-table__group-primary\">\n <ds-text\n class=\"ds-table__group-label\"\n as=\"span\"\n variant=\"text-body-medium\"\n emphasis={true}\n color={labelColor}\n >\n {group.label}\n </ds-text>\n <span class=\"ds-visually-hidden\">{countLabel}</span>\n </span>\n {accessories.length > 0 && (\n <span class=\"ds-table__group-accessories\">\n {accessories.flatMap((item, index) => [\n ...(index > 0\n ? [\n <ds-text\n key={`sep-${index}`}\n class=\"ds-table__group-accessory-separator\"\n as=\"span\"\n variant=\"text-body-small\"\n color=\"secondary\"\n aria-hidden=\"true\"\n >\n ·\n </ds-text>,\n ]\n : []),\n this.renderGroupAccessory(item, index),\n ])}\n </span>\n )}\n </span>\n <span class=\"ds-table__group-trailing\">\n <ds-text\n class=\"ds-table__group-count\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n aria-hidden=\"true\"\n >\n {groupModel.visibleCountText}\n </ds-text>\n <span class=\"ds-table__group-action\">\n <ds-button-unfilled\n class=\"ds-table__group-toggle\"\n variant=\"icon\"\n size=\"md\"\n isInset={true}\n insetDepth=\"double\"\n icon={isCollapsed ? 'ChevronDown' : 'ChevronUp'}\n expanded={!isCollapsed}\n aria-label={\n isCollapsed ? `Expand ${group.label} group` : `Collapse ${group.label} group`\n }\n hasBorder={false}\n pressScale={false}\n />\n </span>\n </span>\n </span>\n );\n }\n\n private formatGroupLoadLabel(template: string, group: DataGroup): string {\n return template.split('{group}').join(group.label);\n }\n\n private renderGroupLoadRow(group: DataGroup, totalColumns: number) {\n const error = group.loadMoreError?.trim();\n if (!error && !group.loadingMore && !group.hasMore) return null;\n const manualFallback =\n this.loadMoreMode === 'manual' || !this.groupLoadController.intersectionSupported;\n\n return (\n <tr\n class=\"ds-table__load-row ds-table__group-load-row\"\n data-group-id={group.id}\n ref={element => {\n if (element) this.groupSentinelEls.set(group.id, element);\n else this.groupSentinelEls.delete(group.id);\n }}\n >\n <td class=\"ds-table__load-cell\" colSpan={totalColumns}>\n <div class=\"ds-table__viewport-band ds-table__load-band\">\n {renderTableLoadContent({\n error,\n loading: !!group.loadingMore,\n hasMore: !!group.hasMore,\n manualFallback,\n retryLabel: this.formatGroupLoadLabel(this.groupRetryLabel, group),\n loadingLabel: this.formatGroupLoadLabel(this.groupLoadingMoreLabel, group),\n loadMoreLabel: this.groupLoadMoreLabel,\n loadMoreAriaLabel: this.formatGroupLoadLabel(this.groupLoadMoreAriaLabel, group),\n onRetry: () => this.groupLoadController.request(group.id, 'retry'),\n onLoadMore: () => this.groupLoadController.request(group.id, 'manual'),\n })}\n </div>\n </td>\n </tr>\n );\n }\n\n private renderStickyGroup(model: TableRenderModel) {\n if (!this.documentStickyHeader || !this.activeStickyGroupId) return null;\n const groupModel = model.groups.find(item => item.group.id === this.activeStickyGroupId);\n if (!groupModel) return null;\n return (\n <div\n class={{\n 'ds-table__sticky-group': true,\n 'ds-table__sticky-group--selectable': model.selectable,\n [groupModel.intentClass ?? '']: !!groupModel.intentClass,\n }}\n data-group-id={groupModel.group.id}\n data-group-intent={groupModel.intent}\n >\n {this.renderGroupContent(groupModel)}\n </div>\n );\n }\n\n private renderDataBodies(model: TableRenderModel, plan: TableVirtualPlan | null) {\n return this.bodyRenderer.render({\n model,\n plan,\n rows: this.rows,\n stickyHeader: this.stickyHeader,\n documentStickyHeader: this.documentStickyHeader,\n activeStickyGroupId: this.activeStickyGroupId,\n renderRow: (row, renderModel, ariaRowIndex, variableVirtualSize, rowKey) =>\n this.renderRow(row, renderModel, ariaRowIndex, variableVirtualSize, rowKey),\n renderGroupContent: groupModel => this.renderGroupContent(groupModel),\n renderGroupLoadRow: (group, totalColumns) => this.renderGroupLoadRow(group, totalColumns),\n });\n }\n\n private renderSkeletonBody(model: TableRenderModel) {\n return renderTableSkeletonBody({\n model,\n visibleColumns: this.visibleColumns,\n skeletonRows: this.skeletonRows,\n renderStickyEdge: sticky => this.renderStickyEdge(sticky),\n });\n }\n\n private renderStateBody(kind: 'empty' | 'error' | 'virtual-viewport', totalColumns: number) {\n const error = kind !== 'empty';\n const heading =\n kind === 'virtual-viewport'\n ? TABLE_VIRTUAL_VIEWPORT_REQUIRED_HEADING\n : error\n ? this.errorHeading\n : this.emptyHeading;\n const body =\n kind === 'virtual-viewport'\n ? TABLE_VIRTUAL_VIEWPORT_REQUIRED_BODY\n : error\n ? this.errorBody\n : this.emptyBody;\n return (\n <tbody class=\"ds-table__body ds-table__state-body\">\n <tr class=\"ds-table__state-row\">\n <td class=\"ds-table__state-cell\" colSpan={totalColumns}>\n <div class=\"ds-table__viewport-band ds-table__state-band\">\n <ds-empty-state\n icon={error ? 'ExclamationTriangle' : 'Inbox'}\n heading={heading}\n body={body}\n />\n </div>\n </td>\n </tr>\n </tbody>\n );\n }\n\n private renderLazyBody(model: TableRenderModel) {\n if (\n this.dataMode !== 'infinite' ||\n !this.incrementalWindowActive ||\n !model.hasData ||\n model.grouped\n )\n return null;\n const error = this.loadMoreError?.trim();\n const manualFallback =\n this.loadMoreMode === 'manual' || !this.loadController.intersectionSupported;\n\n return (\n <tbody class=\"ds-table__body ds-table__load-body\">\n <tr\n class=\"ds-table__load-row\"\n ref={element => {\n this.sentinelEl = element ?? null;\n }}\n >\n <td class=\"ds-table__load-cell\" colSpan={model.totalColumns}>\n <div class=\"ds-table__viewport-band ds-table__load-band\">\n {renderTableLoadContent({\n error,\n loading: this.loadingMore,\n hasMore: this.hasMore,\n manualFallback,\n retryLabel: this.retryLabel,\n loadingLabel: this.loadingMoreLabel,\n loadMoreLabel: this.loadMoreLabel,\n endOfResultsLabel: this.endOfResultsLabel,\n onRetry: () => this.loadController.request('retry'),\n onLoadMore: () => this.loadController.request('manual'),\n })}\n </div>\n </td>\n </tr>\n </tbody>\n );\n }\n\n private renderDocumentStickyHeader(model: TableRenderModel) {\n if (!this.documentStickyHeader) return null;\n return (\n <div class=\"ds-table__document-sticky-header\">\n <table\n class={{\n 'ds-table__table': true,\n 'ds-table__sticky-header-table': true,\n 'ds-table__table--selectable': model.selectable,\n 'ds-table__table--grouped': model.grouped,\n }}\n style={model.tableStyle}\n role=\"presentation\"\n ref={element => {\n this.stickyHeaderTableEl = element ?? null;\n }}\n >\n {this.renderColgroup(model)}\n {this.renderHeader(model, true, true)}\n </table>\n {this.renderFloatingCollapseAll(model)}\n </div>\n );\n }\n\n private get resultSummary(): string | null {\n if (this.dataMode === 'pagination') return null;\n if (this.dataMode === 'virtual') {\n return formatTableTotalSummary(this.currentLoadedRowCount(), this.resultTotalSummaryLabel);\n }\n return formatTableResultSummary(this.displayedCount, this.totalCount, this.resultSummaryLabel);\n }\n\n private get hasResultFooter(): boolean {\n return (\n (this.dataMode === 'pagination' && !!this.pagination) ||\n !!this.resultSummary ||\n this.footerSlotPresence !== 0\n );\n }\n\n private renderResultFooter() {\n const summary = this.resultSummary;\n const hasLeading = (this.footerSlotPresence & TABLE_FOOTER_SLOT_LEADING) !== 0;\n const hasCopy = (this.footerSlotPresence & TABLE_FOOTER_SLOT_COPY) !== 0;\n const hasTrailing = (this.footerSlotPresence & TABLE_FOOTER_SLOT_TRAILING) !== 0;\n const pagination = this.dataMode === 'pagination' ? this.pagination : null;\n if (!this.hasResultFooter) return null;\n return (\n <div class=\"ds-table__footer ds-table__bar ds-chrome-header ds-control--md\">\n <div class=\"ds-table__bar-copy ds-chrome-header__copy ds-control--md\">\n {hasLeading && (\n <div class=\"ds-table__bar-status ds-chrome-header__heading\">\n <slot name=\"footer-leading\" onSlotchange={this.syncFooterSlotPresence} />\n </div>\n )}\n {hasCopy ? (\n <slot name=\"footer\" onSlotchange={this.syncFooterSlotPresence} />\n ) : summary ? (\n <ds-text\n class=\"ds-table__footer-summary ds-table__bar-text\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n lineTruncation={1}\n >\n {summary}\n </ds-text>\n ) : null}\n </div>\n {(pagination || hasTrailing) && (\n <div class=\"ds-table__bar-trailing ds-chrome-header__trailing\">\n {pagination && (\n <ds-pagination\n pageIndex={pagination.pageIndex}\n pageSize={pagination.pageSize}\n pageSizeMode={pagination.pageSizeMode ?? 'fixed'}\n totalItems={pagination.totalItems}\n pageSizeOptions={pagination.pageSizeOptions ?? [25, 50, 100, 200]}\n fitToPage={pagination.fitToPage ?? false}\n {...{ fitToPageInactive: pagination.fitToPageInactive ?? false }}\n fitPageSize={this.fitPageSize ?? pagination.fitPageSize}\n fitPageSizeLabel={pagination.fitPageSizeLabel ?? 'Fit to page'}\n fitPageSizeTriggerLabel={pagination.fitPageSizeTriggerLabel ?? 'Fit'}\n itemLabel={pagination.itemLabel ?? 'items'}\n pageSizeLabel={pagination.pageSizeLabel ?? 'Items'}\n label={pagination.ariaLabel ?? `${this.caption} pagination`}\n showFirstLastButtons={pagination.showFirstLastButtons ?? false}\n loading={this.loading}\n onDsChange={(event: CustomEvent<PaginationChangeDetail>) =>\n this.dsPaginationChange.emit(event.detail)\n }\n />\n )}\n <slot name=\"footer-trailing\" onSlotchange={this.syncFooterSlotPresence} />\n </div>\n )}\n </div>\n );\n }\n\n private renderCaptionBar() {\n if (this.captionVisibility !== 'visible') return null;\n return (\n <div class=\"ds-table__caption-bar ds-table__bar ds-control--md\">\n <div\n class={{\n 'ds-table__caption-content': true,\n 'ds-table__caption-content--trailing': this.showsCaptionTrailing,\n }}\n >\n <div\n class={{\n 'ds-table__caption-leading': true,\n 'ds-table__caption-leading--toolbar': this.headerUsesToolbar,\n }}\n >\n <slot name=\"header\" onSlotchange={this.syncHeaderSlotPresence} />\n {!this.headerPresent ? (\n <ds-text\n class=\"ds-table__caption-title ds-table__bar-text\"\n as=\"div\"\n variant=\"text-title-small\"\n emphasis={true}\n color=\"primary\"\n aria-hidden=\"true\"\n >\n {this.caption}\n </ds-text>\n ) : null}\n </div>\n {this.renderCaptionTrailing()}\n </div>\n <slot name=\"header-after\" />\n </div>\n );\n }\n\n private renderCaptionTrailing() {\n if (!this.showsCaptionTrailing) return null;\n return (\n <div class=\"ds-table__caption-trailing\">\n {this.renderColumnCustomizerTrigger()}\n {this.showsColumnCustomizer &&\n !this.hideColumnCustomizerTrigger &&\n this.showsDataModeSwitcher ? (\n <ds-divider orientation=\"vertical\" length=\"32px\" />\n ) : null}\n {this.renderDataModeSwitcherTrigger()}\n </div>\n );\n }\n\n private renderDataModeSwitcherTrigger() {\n if (!this.showsDataModeSwitcher) return null;\n if (this.chromeLoading) {\n return <ds-skeleton variant=\"control\" controlSize=\"md\" width=\"var(--dimension-size-400)\" />;\n }\n return (\n <span class=\"ds-table__caption-mode-switcher\">\n <ds-tooltip label={this.dataModeSwitcherLabel} side=\"top\" size=\"sm\">\n <ds-button-unfilled\n hasBorder={!this.captionControlsBorderless}\n id={`${this.dataModeSwitcherElementId}-trigger`}\n variant=\"icon\"\n size=\"md\"\n icon=\"Ellipses\"\n aria-label={this.dataModeSwitcherLabel}\n hasMenu={true}\n expanded={this.dataModeSwitcherOpen}\n surfaceOpen={this.dataModeSwitcherSurfaceOpen}\n controls={this.dataModeSwitcherElementId}\n activeFill={false}\n pressScale={false}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n this.toggleDataModeSwitcher(event.detail.detail === 0);\n }}\n />\n </ds-tooltip>\n </span>\n );\n }\n\n private renderDataModeSwitcherMenu() {\n if (!this.showsDataModeSwitcher || this.chromeLoading) return null;\n return (\n <ds-menu\n id={this.dataModeSwitcherElementId}\n open={this.dataModeSwitcherOpen}\n anchorId={`${this.dataModeSwitcherElementId}-trigger`}\n align=\"end\"\n side=\"bottom\"\n menuLabel={this.dataModeMenuLabel}\n initialFocusVisible={this.dataModeSwitcherInitialFocusVisible}\n items={tableDataModeMenuItems(this.dataMode, {\n infinite: this.infiniteModeLabel,\n pagination: this.paginationModeLabel,\n virtual: this.virtualModeLabel,\n })}\n onDsClose={() => this.closeDataModeSwitcher()}\n onDsAfterClose={() => {\n if (!this.dataModeSwitcherOpen) this.dataModeSwitcherSurfaceOpen = false;\n }}\n onDsSelect={event => this.handleDataModeSwitcherSelect(event.detail)}\n />\n );\n }\n\n private toggleDataModeSwitcher(fromKeyboard = false): void {\n if (this.dataModeSwitcherOpen) this.closeDataModeSwitcher();\n else this.openDataModeSwitcher(fromKeyboard);\n }\n\n private openDataModeSwitcher(fromKeyboard = false): void {\n if (!this.showsDataModeSwitcher || this.dataModeSwitcherOpen) return;\n this.closeColumnCustomizer();\n this.dataModeSwitcherInitialFocusVisible = fromKeyboard;\n this.dataModeSwitcherOpen = true;\n this.dataModeSwitcherSurfaceOpen = true;\n }\n\n private closeDataModeSwitcher(): void {\n this.dataModeSwitcherOpen = false;\n }\n\n private handleDataModeSwitcherSelect(item: MenuItemData): void {\n const dataMode = tableDataModeFromMenuItem(item);\n if (!dataMode) return;\n this.closeDataModeSwitcher();\n if (dataMode !== this.dataMode) this.dsDataModeChange.emit({ dataMode });\n requestAnimationFrame(() => {\n this.el\n .querySelector<\n HTMLElement & { setFocus?: () => void }\n >(`#${CSS.escape(`${this.dataModeSwitcherElementId}-trigger`)}`)\n ?.setFocus?.();\n });\n }\n\n private renderColumnCustomizerTrigger() {\n if (this.hideColumnCustomizerTrigger) return null;\n if (!this.showsColumnCustomizer) return null;\n return (\n <div\n class={{\n 'ds-table__caption-customizer': true,\n 'ds-table__caption-customizer--loading': this.chromeLoading,\n }}\n aria-hidden={this.chromeLoading ? 'true' : undefined}\n >\n <ds-tooltip label={this.captionCompact ? 'Customize' : ''} side=\"top\" size=\"sm\">\n <ds-button-unfilled\n hasBorder={!this.captionControlsBorderless}\n id={`${this.columnCustomizerElementId}-trigger`}\n variant={this.captionCompact ? 'icon' : 'icon-label'}\n size=\"md\"\n icon=\"Table\"\n label=\"Customize\"\n labelEmphasis={false}\n pressScale={false}\n aria-label=\"Customize table\"\n haspopup=\"menu\"\n expanded={this.columnCustomizerOpen}\n surfaceOpen={this.columnCustomizerSurfaceOpen}\n controls={this.columnCustomizerElementId}\n onDsClick={(event: CustomEvent<MouseEvent>) => {\n if (this.chromeLoading) return;\n this.toggleColumnCustomizer(event.detail.detail === 0);\n }}\n />\n </ds-tooltip>\n {this.chromeLoading ? (\n <ds-skeleton variant=\"control\" controlSize=\"md\" width=\"100%\" />\n ) : null}\n </div>\n );\n }\n\n private renderColumnCustomizerMenu() {\n if (!this.showsColumnCustomizer || this.chromeLoading) return null;\n return (\n <ds-menu\n id={this.columnCustomizerElementId}\n open={this.columnCustomizerOpen}\n anchorId={`${this.columnCustomizerElementId}-trigger`}\n align=\"end\"\n side=\"bottom\"\n menuLabel=\"Customize table\"\n initialFocusVisible={this.columnCustomizerInitialFocusVisible}\n sections={[\n {\n header: 'Columns',\n items: tableColumnCustomizerMenuItems(\n this.columns,\n this.hiddenFieldIds,\n this.fieldOrder\n ),\n },\n ...(this.customizeOptions.length\n ? [{ header: 'Options', items: this.customizeOptions }]\n : []),\n ]}\n onDsClose={() => this.closeColumnCustomizer()}\n onDsAfterClose={() => {\n if (!this.columnCustomizerOpen) this.columnCustomizerSurfaceOpen = false;\n }}\n onDsSelect={event => {\n if (this.customizeOptions.some(option => option.value === event.detail.value)) {\n this.dsCustomizeOptionChange.emit(event.detail.value!);\n } else this.handleColumnCustomizerSelect(event.detail);\n }}\n onDsReorder={event => this.handleColumnCustomizerReorder(event.detail)}\n />\n );\n }\n\n private toggleColumnCustomizer(fromKeyboard = false): void {\n if (this.columnCustomizerOpen) this.closeColumnCustomizer();\n else this.openColumnCustomizer(fromKeyboard);\n }\n\n private openColumnCustomizer(fromKeyboard = false): void {\n if (!this.showsColumnCustomizer || this.columnCustomizerOpen) return;\n this.closeDataModeSwitcher();\n this.columnCustomizerInitialFocusVisible = fromKeyboard;\n this.columnCustomizerOpen = true;\n this.columnCustomizerSurfaceOpen = true;\n }\n\n private closeColumnCustomizer(): void {\n this.columnCustomizerOpen = false;\n }\n\n private emitColumnsConfigChange(hiddenFieldIds: string[], fieldOrder: string[]): void {\n this.dsFieldsConfigChange.emit({\n hiddenFieldIds: resolveTableHiddenColumnIds(this.columns, hiddenFieldIds),\n fieldOrder: resolveTableColumnOrder(this.columns, fieldOrder),\n });\n }\n\n private handleColumnCustomizerReorder(detail: MenuReorderDetail): void {\n const order = detail.items\n .filter(item => item.reorderable)\n .map(item => item.value)\n .filter((id): id is string => !!id);\n this.emitColumnsConfigChange(this.hiddenFieldIds, order);\n }\n\n private handleColumnCustomizerSelect(item: MenuItemData): void {\n const fieldId = item.value;\n if (!fieldId || item.isInactive) return;\n this.emitColumnsConfigChange(\n toggleTableColumnHidden(this.columns, this.hiddenFieldIds, fieldId),\n resolveTableColumnOrder(this.columns, this.fieldOrder)\n );\n }\n\n render() {\n this.groupSentinelEls.clear();\n const model = this.createRenderModel();\n this.syncVirtualItems(model);\n this.renderedModel = model;\n const regionLabel = this.scrollLabel?.trim() || `${this.caption} scroll area`;\n const initialLoading = this.loading && !model.hasData;\n const initialError = !model.hasData && this.error;\n const hasGroupedStructure = model.grouped && model.groups.length > 0;\n const windowRows = this.shouldWindowRows(model);\n const virtualViewportMissing =\n this.virtualViewportMissing && !initialLoading && (model.hasData || hasGroupedStructure);\n const virtualPlan = windowRows\n ? (this.virtualWindow ??\n resolveTableVirtualPlan({\n items: this.virtualItems,\n sizes: this.virtualItems.map(item => this.virtualController.sizeFor(item)),\n scrollOffset: this.viewportEl?.scrollTop ?? 0,\n viewportSize: this.estimateVirtualViewportSize(),\n pinnedRowIds: this.virtualPinnedRowIds(),\n }))\n : null;\n const groupLoadingMore = hasGroupedStructure && this.groups.some(group => group.loadingMore);\n const viewportStyle =\n !this.boundedComposition && this.resolvedMaxHeight\n ? { '--ds-table-max-block-size': this.resolvedMaxHeight }\n : undefined;\n const hostStyle = this.fixedHeight\n ? { '--_table-fixed-block-size': this.resolvedHeight }\n : undefined;\n\n return (\n <Host\n class={{\n 'table-host': true,\n 'table-host--bounded': this.boundedComposition,\n 'table-host--fixed-height': this.fixedHeight,\n 'table-host--viewport-fit': this.fitViewport,\n }}\n style={hostStyle}\n onKeyDown={(event: KeyboardEvent) => this.handlePaginationKeyDown(event)}\n onFocusin={this.onVirtualFocusIn}\n >\n <div\n class={{\n 'ds-table': true,\n 'ds-table--bounded': this.boundedComposition,\n 'ds-table--contained-scroll': this.containedScroll,\n 'ds-table--fixed-height': this.fixedHeight,\n 'ds-table--viewport-fit': this.fitViewport,\n 'ds-table--viewport-fit-pending': this.fitViewport && !this.viewportFitSettled,\n 'ds-table--viewport-fit-settled': this.fitViewport && this.viewportFitSettled,\n 'ds-table--sticky-header': this.stickyHeader,\n 'ds-table--document-sticky-header': this.documentStickyHeader,\n 'ds-table--contained-sticky-header': this.stickyHeader && !this.documentStickyHeader,\n 'ds-table--caption-visible': this.captionVisibility === 'visible',\n 'ds-table--footer-visible': this.hasResultFooter,\n 'ds-table--state-fill':\n !initialLoading &&\n (initialError || virtualViewportMissing || !(model.hasData || hasGroupedStructure)),\n }}\n ref={element => {\n this.rootEl = element ?? null;\n }}\n >\n {this.renderCaptionBar()}\n <div\n class={{\n 'ds-table__frame': true,\n 'ds-table__frame--overflow-start': this.overflowStart,\n 'ds-table__frame--overflow-end': this.overflowEnd,\n }}\n ref={element => {\n this.frameEl = element ?? null;\n }}\n >\n {this.renderDocumentStickyHeader(model)}\n {this.renderStickyGroup(model)}\n {!this.documentStickyHeader && this.renderFloatingCollapseAll(model)}\n <div\n class={{\n 'ds-table__viewport': true,\n 'ds-focus-ring': this.scrollable,\n }}\n style={viewportStyle}\n ref={element => {\n this.viewportEl = element ?? null;\n }}\n role={this.scrollable ? 'region' : undefined}\n aria-label={this.scrollable ? regionLabel : undefined}\n tabIndex={this.scrollable ? 0 : undefined}\n >\n <table\n class={{\n 'ds-table__table': true,\n 'ds-table__table--selectable': model.selectable,\n 'ds-table__table--grouped': model.grouped,\n 'ds-table__table--deferred-rows': this.dataMode !== 'virtual',\n 'ds-table__table--windowed': windowRows,\n 'ds-table__table--virtual': this.dataMode === 'virtual' && windowRows,\n 'ds-table__table--native-group-sticky':\n (model.grouped && this.stickyHeader && !this.documentStickyHeader) ||\n windowRows,\n }}\n style={model.tableStyle}\n aria-rowcount={windowRows ? 1 + this.virtualItems.length : undefined}\n aria-busy={\n initialLoading ||\n (this.dataMode === 'infinite' && (this.loadingMore || groupLoadingMore))\n ? 'true'\n : undefined\n }\n ref={element => {\n this.tableEl = element ?? null;\n }}\n >\n <caption class=\"ds-table__caption ds-visually-hidden\">\n <ds-text as=\"span\" variant=\"text-title-small\" emphasis={true} color=\"primary\">\n {this.caption}\n </ds-text>\n </caption>\n {this.renderColgroup(model)}\n {this.renderHeader(\n model,\n !this.documentStickyHeader,\n false,\n windowRows ? 1 : undefined\n )}\n {initialLoading\n ? this.renderSkeletonBody(model)\n : initialError\n ? this.renderStateBody('error', model.totalColumns)\n : virtualViewportMissing\n ? this.renderStateBody('virtual-viewport', model.totalColumns)\n : model.hasData || hasGroupedStructure\n ? this.renderDataBodies(model, virtualPlan)\n : this.renderStateBody('empty', model.totalColumns)}\n {!initialLoading &&\n !initialError &&\n this.dataMode !== 'virtual' &&\n this.renderLazyBody(model)}\n </table>\n </div>\n </div>\n {this.renderResultFooter()}\n {this.renderOverflowActionMenu()}\n {this.renderDataModeSwitcherMenu()}\n {this.renderColumnCustomizerMenu()}\n {this.renderTruncateTooltip()}\n <div class=\"ds-visually-hidden\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n {this.announcement}\n </div>\n </div>\n </Host>\n );\n }\n}\n",
1435
1435
  "type": "registry:ui"
1436
1436
  },
1437
1437
  {
1438
1438
  "path": "src/wc/components/Table/table-action-menu.ts",
1439
- "content": "import type { MenuItemData, MenuItemsSection } from '../Menu/menu-types';\nimport type { TableCellAction, TableCellActionMenuEntry } from './table-types';\n\nlet actionMenuSeq = 0;\n\nexport function nextTableActionMenuElementId(): string {\n actionMenuSeq += 1;\n return `ds-table-action-menu-${actionMenuSeq}`;\n}\n\nexport function isTableCellActionMenu(\n value: TableCellAction\n): value is TableCellAction & { items: TableCellActionMenuEntry[]; ariaLabel: string } {\n return Array.isArray(value.items);\n}\n\nexport function isRenderableTableActionMenu(\n value: TableCellAction\n): value is TableCellAction & { items: TableCellActionMenuEntry[]; ariaLabel: string } {\n return isTableCellActionMenu(value) && tableActionMenuSections(value.items).length > 0;\n}\n\n/** Split divider entries into Menu sections. Empty runs (leading/repeated dividers) are dropped. */\nexport function tableActionMenuSections(items: TableCellActionMenuEntry[]): MenuItemsSection[] {\n const sections: MenuItemsSection[] = [];\n let current: MenuItemData[] = [];\n const flush = () => {\n if (!current.length) return;\n sections.push({ items: current });\n current = [];\n };\n\n for (const entry of items) {\n if ('kind' in entry) {\n flush();\n continue;\n }\n current.push({\n value: entry.actionId,\n label: entry.label,\n ...(entry.isInactive ? { isInactive: true } : {}),\n ...(entry.isDestructive ? { isDestructive: true } : {}),\n });\n }\n flush();\n return sections;\n}\n\nexport function tableActionTriggerId(menuId: string, rowId: string, columnId: string): string {\n return `${menuId}-${sanitizeId(rowId)}-${sanitizeId(columnId)}`;\n}\n\nfunction sanitizeId(value: string): string {\n return value.replace(/[^A-Za-z0-9_-]+/g, '-');\n}\n",
1439
+ "content": "import type { MenuItemData, MenuItemsSection } from '../Menu/menu-types';\nimport type { TableCellAction, TableCellActionMenuEntry } from './table-types';\n\nlet actionMenuSeq = 0;\n\nexport function nextTableActionMenuElementId(): string {\n actionMenuSeq += 1;\n return `ds-table-action-menu-${actionMenuSeq}`;\n}\n\nexport function isTableCellActionMenu(\n value: TableCellAction\n): value is TableCellAction & { items: TableCellActionMenuEntry[]; ariaLabel: string } {\n return Array.isArray(value.items);\n}\n\nexport function isRenderableTableActionMenu(\n value: TableCellAction\n): value is TableCellAction & { items: TableCellActionMenuEntry[]; ariaLabel: string } {\n return isTableCellActionMenu(value) && tableActionMenuSections(value.items).length > 0;\n}\n\n/** Split divider entries into Menu sections. Empty runs (leading/repeated dividers) are dropped. */\nexport function tableActionMenuSections(items: TableCellActionMenuEntry[]): MenuItemsSection[] {\n const sections: MenuItemsSection[] = [];\n let current: MenuItemData[] = [];\n const flush = () => {\n if (!current.length) return;\n sections.push({ items: current });\n current = [];\n };\n\n for (const entry of items) {\n if ('kind' in entry) {\n flush();\n continue;\n }\n current.push({\n value: entry.actionId,\n label: entry.label,\n ...(entry.isInactive ? { isInactive: true } : {}),\n ...(entry.isDestructive ? { isDestructive: true } : {}),\n });\n }\n flush();\n return sections;\n}\n\nexport function tableActionTriggerId(menuId: string, rowId: string, fieldId: string): string {\n return `${menuId}-${sanitizeId(rowId)}-${sanitizeId(fieldId)}`;\n}\n\nfunction sanitizeId(value: string): string {\n return value.replace(/[^A-Za-z0-9_-]+/g, '-');\n}\n",
1440
1440
  "type": "registry:ui"
1441
1441
  },
1442
1442
  {
1443
1443
  "path": "src/wc/components/Table/table-body-renderer.tsx",
1444
- "content": "import { h } from '@stencil/core';\nimport type { TableGroupRenderModel, TableRenderModel } from './table-render-model';\nimport type { TableGroup, TableRow } from './table-types';\nimport type { TableVirtualNode, TableVirtualPlan } from './table-virtual-model';\n\nexport interface TableVirtualRowPoolState {\n slotsByRowId: Map<string, number>;\n nextSlot: number;\n}\n\nexport interface TableBodyRenderOptions {\n model: TableRenderModel;\n plan: TableVirtualPlan | null;\n rows: TableRow[];\n stickyHeader: boolean;\n documentStickyHeader: boolean;\n activeStickyGroupId: string | null;\n renderRow: (\n row: TableRow,\n model: TableRenderModel,\n ariaRowIndex?: number,\n variableVirtualSize?: boolean,\n rowKey?: string\n ) => unknown;\n renderGroupContent: (group: TableGroupRenderModel) => unknown;\n renderGroupLoadRow: (group: TableGroup, totalColumns: number) => unknown;\n}\n\n/**\n * Assign stable, bounded DOM keys to the rows currently occupying one virtual scope.\n * Departed rows release their slots so long scrolls recycle DOM instead of growing it.\n */\nexport function assignTableVirtualRowPoolKeys(\n nodes: readonly TableVirtualNode[],\n state: TableVirtualRowPoolState\n): Map<number, string> {\n const rows = nodes.filter(\n (node): node is Extract<TableVirtualNode, { kind: 'row' }> => node.kind === 'row'\n );\n const desiredIds = new Set(rows.map(node => node.item.rowId ?? node.item.id));\n const freeSlots: number[] = [];\n for (const [rowId, slot] of state.slotsByRowId) {\n if (desiredIds.has(rowId)) continue;\n state.slotsByRowId.delete(rowId);\n freeSlots.push(slot);\n }\n freeSlots.sort((left, right) => left - right);\n\n const keys = new Map<number, string>();\n for (const node of rows) {\n const rowId = node.item.rowId ?? node.item.id;\n let slot = state.slotsByRowId.get(rowId);\n if (slot == null) {\n slot = freeSlots.shift() ?? state.nextSlot++;\n state.slotsByRowId.set(rowId, slot);\n }\n keys.set(node.index, `virtual-row-slot-${slot}`);\n }\n return keys;\n}\n\n/** Owns grouped and virtual tbody composition while Table remains the behavior orchestrator. */\nexport class TableBodyRenderer {\n private virtualLookupCache: {\n rows: TableRow[];\n groups: TableGroupRenderModel[];\n rowsById: Map<string, TableRow>;\n groupsById: Map<string, TableGroupRenderModel>;\n } | null = null;\n\n private readonly virtualRowPoolStates = new Map<string, TableVirtualRowPoolState>();\n\n resetVirtualState(): void {\n this.virtualLookupCache = null;\n this.virtualRowPoolStates.clear();\n }\n\n render(options: TableBodyRenderOptions) {\n if (options.plan) return this.renderVirtualBodies(options);\n\n const { model } = options;\n if (!model.grouped) {\n return (\n <tbody class=\"ds-table__body\">\n {options.rows.map(row => options.renderRow(row, model))}\n </tbody>\n );\n }\n\n return model.groups.map(groupModel => {\n const { group, collapsed: isCollapsed, intent, intentClass } = groupModel;\n const stickySourceHidden =\n options.documentStickyHeader && options.activeStickyGroupId === group.id;\n return (\n <tbody\n class=\"ds-table__body ds-table__group\"\n role=\"rowgroup\"\n data-group-id={group.id}\n data-group-intent={intent}\n data-collapsed={isCollapsed ? 'true' : undefined}\n aria-busy={!isCollapsed && group.loadingMore ? 'true' : undefined}\n key={group.id}\n >\n <tr\n role=\"row\"\n aria-hidden={stickySourceHidden ? 'true' : undefined}\n class={{\n 'ds-table__group-row': true,\n 'ds-table__group-row--native-sticky':\n options.stickyHeader && !options.documentStickyHeader,\n }}\n >\n <th\n class={{\n 'ds-table__group-cell': true,\n 'ds-table__group-cell--sticky-source-hidden': stickySourceHidden,\n [intentClass ?? '']: !!intentClass,\n }}\n role=\"rowheader\"\n scope=\"rowgroup\"\n colSpan={model.totalColumns}\n >\n {options.renderGroupContent(groupModel)}\n </th>\n </tr>\n {!isCollapsed && group.rows.map(row => options.renderRow(row, model))}\n {!isCollapsed && options.renderGroupLoadRow(group, model.totalColumns)}\n </tbody>\n );\n });\n }\n\n private renderVirtualSpacer(\n node: Extract<TableVirtualNode, { kind: 'spacer' }>,\n totalColumns: number\n ) {\n if (node.size <= 0) return null;\n return (\n <tr class=\"ds-table__virtual-spacer-row\" key={node.key} aria-hidden=\"true\">\n <td\n class=\"ds-table__virtual-spacer-cell\"\n colSpan={totalColumns}\n style={\n { '--_table-virtual-spacer-block-size': `${node.size}px` } as Record<string, string>\n }\n />\n </tr>\n );\n }\n\n private renderVirtualRow(\n node: Extract<TableVirtualNode, { kind: 'row' }>,\n options: TableBodyRenderOptions,\n rowsById: Map<string, TableRow>,\n rowKey: string\n ) {\n const row = node.item.rowId ? rowsById.get(node.item.rowId) : undefined;\n if (!row) return null;\n return options.renderRow(row, options.model, node.index + 2, node.item.variableSize, rowKey);\n }\n\n private virtualRowPoolKeys(nodes: readonly TableVirtualNode[], scope: string) {\n const state = this.virtualRowPoolStates.get(scope) ?? {\n slotsByRowId: new Map<string, number>(),\n nextSlot: 0,\n };\n this.virtualRowPoolStates.set(scope, state);\n return assignTableVirtualRowPoolKeys(nodes, state);\n }\n\n private renderVirtualGroup(\n node: Extract<TableVirtualNode, { kind: 'group' }>,\n options: TableBodyRenderOptions,\n rowsById: Map<string, TableRow>,\n groupsById: Map<string, TableGroupRenderModel>\n ) {\n const groupModel = groupsById.get(node.groupId);\n if (!groupModel) return null;\n const { group, collapsed: isCollapsed, intent, intentClass } = groupModel;\n const rowKeys = this.virtualRowPoolKeys(node.nodes, `group:${group.id}`);\n return (\n <tbody\n class=\"ds-table__body ds-table__group\"\n role=\"rowgroup\"\n data-group-id={group.id}\n data-group-intent={intent}\n data-collapsed={isCollapsed ? 'true' : undefined}\n key={group.id}\n >\n <tr\n role=\"row\"\n data-virtual-id={`group:${group.id}`}\n aria-rowindex={node.headerIndex + 2}\n class={{\n 'ds-table__group-row': true,\n 'ds-table__group-row--native-sticky':\n options.stickyHeader && !options.documentStickyHeader,\n }}\n >\n <th\n class={{\n 'ds-table__group-cell': true,\n [intentClass ?? '']: !!intentClass,\n }}\n role=\"rowheader\"\n scope=\"rowgroup\"\n colSpan={options.model.totalColumns}\n >\n {options.renderGroupContent(groupModel)}\n </th>\n </tr>\n {node.nodes.map(child =>\n child.kind === 'spacer'\n ? this.renderVirtualSpacer(child, options.model.totalColumns)\n : this.renderVirtualRow(\n child,\n options,\n rowsById,\n rowKeys.get(child.index) ?? `virtual-row-${child.index}`\n )\n )}\n </tbody>\n );\n }\n\n private renderVirtualBodies(options: TableBodyRenderOptions) {\n const { model, plan } = options;\n if (!plan) return null;\n let lookup = this.virtualLookupCache;\n if (!lookup || lookup.rows !== model.loadedRows || lookup.groups !== model.groups) {\n lookup = {\n rows: model.loadedRows,\n groups: model.groups,\n rowsById: new Map(model.loadedRows.map(row => [row.id, row])),\n groupsById: new Map(model.groups.map(groupModel => [groupModel.group.id, groupModel])),\n };\n this.virtualLookupCache = lookup;\n }\n const { rowsById, groupsById } = lookup;\n if (!model.grouped) {\n for (const scope of this.virtualRowPoolStates.keys()) {\n if (scope !== 'flat') this.virtualRowPoolStates.delete(scope);\n }\n const rowKeys = this.virtualRowPoolKeys(plan.nodes, 'flat');\n return (\n <tbody class=\"ds-table__body\">\n {plan.nodes.map(node =>\n node.kind === 'spacer'\n ? this.renderVirtualSpacer(node, model.totalColumns)\n : node.kind === 'row'\n ? this.renderVirtualRow(\n node,\n options,\n rowsById,\n rowKeys.get(node.index) ?? `virtual-row-${node.index}`\n )\n : null\n )}\n </tbody>\n );\n }\n\n const activeGroupScopes = new Set(\n plan.nodes\n .filter(\n (node): node is Extract<TableVirtualNode, { kind: 'group' }> => node.kind === 'group'\n )\n .map(node => `group:${node.groupId}`)\n );\n for (const scope of this.virtualRowPoolStates.keys()) {\n if (!activeGroupScopes.has(scope)) this.virtualRowPoolStates.delete(scope);\n }\n return plan.nodes.map(node => {\n if (node.kind === 'spacer') {\n return (\n <tbody class=\"ds-table__body ds-table__virtual-pad\" aria-hidden=\"true\" key={node.key}>\n {this.renderVirtualSpacer(node, model.totalColumns)}\n </tbody>\n );\n }\n if (node.kind === 'group') {\n return this.renderVirtualGroup(node, options, rowsById, groupsById);\n }\n return null;\n });\n }\n}\n",
1444
+ "content": "import { h } from '@stencil/core';\nimport type { TableGroupRenderModel, TableRenderModel } from './table-render-model';\nimport type { DataGroup, TableRow } from './table-types';\nimport type { TableVirtualNode, TableVirtualPlan } from './table-virtual-model';\n\nexport interface TableVirtualRowPoolState {\n slotsByRowId: Map<string, number>;\n nextSlot: number;\n}\n\nexport interface TableBodyRenderOptions {\n model: TableRenderModel;\n plan: TableVirtualPlan | null;\n rows: TableRow[];\n stickyHeader: boolean;\n documentStickyHeader: boolean;\n activeStickyGroupId: string | null;\n renderRow: (\n row: TableRow,\n model: TableRenderModel,\n ariaRowIndex?: number,\n variableVirtualSize?: boolean,\n rowKey?: string\n ) => unknown;\n renderGroupContent: (group: TableGroupRenderModel) => unknown;\n renderGroupLoadRow: (group: DataGroup, totalColumns: number) => unknown;\n}\n\n/**\n * Assign stable, bounded DOM keys to the rows currently occupying one virtual scope.\n * Departed rows release their slots so long scrolls recycle DOM instead of growing it.\n */\nexport function assignTableVirtualRowPoolKeys(\n nodes: readonly TableVirtualNode[],\n state: TableVirtualRowPoolState\n): Map<number, string> {\n const rows = nodes.filter(\n (node): node is Extract<TableVirtualNode, { kind: 'row' }> => node.kind === 'row'\n );\n const desiredIds = new Set(rows.map(node => node.item.rowId ?? node.item.id));\n const freeSlots: number[] = [];\n for (const [rowId, slot] of state.slotsByRowId) {\n if (desiredIds.has(rowId)) continue;\n state.slotsByRowId.delete(rowId);\n freeSlots.push(slot);\n }\n freeSlots.sort((left, right) => left - right);\n\n const keys = new Map<number, string>();\n for (const node of rows) {\n const rowId = node.item.rowId ?? node.item.id;\n let slot = state.slotsByRowId.get(rowId);\n if (slot == null) {\n slot = freeSlots.shift() ?? state.nextSlot++;\n state.slotsByRowId.set(rowId, slot);\n }\n keys.set(node.index, `virtual-row-slot-${slot}`);\n }\n return keys;\n}\n\n/** Owns grouped and virtual tbody composition while Table remains the behavior orchestrator. */\nexport class TableBodyRenderer {\n private virtualLookupCache: {\n rows: TableRow[];\n groups: TableGroupRenderModel[];\n rowsById: Map<string, TableRow>;\n groupsById: Map<string, TableGroupRenderModel>;\n } | null = null;\n\n private readonly virtualRowPoolStates = new Map<string, TableVirtualRowPoolState>();\n\n resetVirtualState(): void {\n this.virtualLookupCache = null;\n this.virtualRowPoolStates.clear();\n }\n\n render(options: TableBodyRenderOptions) {\n if (options.plan) return this.renderVirtualBodies(options);\n\n const { model } = options;\n if (!model.grouped) {\n return (\n <tbody class=\"ds-table__body\">\n {options.rows.map(row => options.renderRow(row, model))}\n </tbody>\n );\n }\n\n return model.groups.map(groupModel => {\n const { group, collapsed: isCollapsed, intent, intentClass } = groupModel;\n const stickySourceHidden =\n options.documentStickyHeader && options.activeStickyGroupId === group.id;\n return (\n <tbody\n class=\"ds-table__body ds-table__group\"\n role=\"rowgroup\"\n data-group-id={group.id}\n data-group-intent={intent}\n data-collapsed={isCollapsed ? 'true' : undefined}\n aria-busy={!isCollapsed && group.loadingMore ? 'true' : undefined}\n key={group.id}\n >\n <tr\n role=\"row\"\n aria-hidden={stickySourceHidden ? 'true' : undefined}\n class={{\n 'ds-table__group-row': true,\n 'ds-table__group-row--native-sticky':\n options.stickyHeader && !options.documentStickyHeader,\n }}\n >\n <th\n class={{\n 'ds-table__group-cell': true,\n 'ds-table__group-cell--sticky-source-hidden': stickySourceHidden,\n [intentClass ?? '']: !!intentClass,\n }}\n role=\"rowheader\"\n scope=\"rowgroup\"\n colSpan={model.totalColumns}\n >\n {options.renderGroupContent(groupModel)}\n </th>\n </tr>\n {!isCollapsed && group.rows.map(row => options.renderRow(row, model))}\n {!isCollapsed && options.renderGroupLoadRow(group, model.totalColumns)}\n </tbody>\n );\n });\n }\n\n private renderVirtualSpacer(\n node: Extract<TableVirtualNode, { kind: 'spacer' }>,\n totalColumns: number\n ) {\n if (node.size <= 0) return null;\n return (\n <tr class=\"ds-table__virtual-spacer-row\" key={node.key} aria-hidden=\"true\">\n <td\n class=\"ds-table__virtual-spacer-cell\"\n colSpan={totalColumns}\n style={\n { '--_table-virtual-spacer-block-size': `${node.size}px` } as Record<string, string>\n }\n />\n </tr>\n );\n }\n\n private renderVirtualRow(\n node: Extract<TableVirtualNode, { kind: 'row' }>,\n options: TableBodyRenderOptions,\n rowsById: Map<string, TableRow>,\n rowKey: string\n ) {\n const row = node.item.rowId ? rowsById.get(node.item.rowId) : undefined;\n if (!row) return null;\n return options.renderRow(row, options.model, node.index + 2, node.item.variableSize, rowKey);\n }\n\n private virtualRowPoolKeys(nodes: readonly TableVirtualNode[], scope: string) {\n const state = this.virtualRowPoolStates.get(scope) ?? {\n slotsByRowId: new Map<string, number>(),\n nextSlot: 0,\n };\n this.virtualRowPoolStates.set(scope, state);\n return assignTableVirtualRowPoolKeys(nodes, state);\n }\n\n private renderVirtualGroup(\n node: Extract<TableVirtualNode, { kind: 'group' }>,\n options: TableBodyRenderOptions,\n rowsById: Map<string, TableRow>,\n groupsById: Map<string, TableGroupRenderModel>\n ) {\n const groupModel = groupsById.get(node.groupId);\n if (!groupModel) return null;\n const { group, collapsed: isCollapsed, intent, intentClass } = groupModel;\n const rowKeys = this.virtualRowPoolKeys(node.nodes, `group:${group.id}`);\n return (\n <tbody\n class=\"ds-table__body ds-table__group\"\n role=\"rowgroup\"\n data-group-id={group.id}\n data-group-intent={intent}\n data-collapsed={isCollapsed ? 'true' : undefined}\n key={group.id}\n >\n <tr\n role=\"row\"\n data-virtual-id={`group:${group.id}`}\n aria-rowindex={node.headerIndex + 2}\n class={{\n 'ds-table__group-row': true,\n 'ds-table__group-row--native-sticky':\n options.stickyHeader && !options.documentStickyHeader,\n }}\n >\n <th\n class={{\n 'ds-table__group-cell': true,\n [intentClass ?? '']: !!intentClass,\n }}\n role=\"rowheader\"\n scope=\"rowgroup\"\n colSpan={options.model.totalColumns}\n >\n {options.renderGroupContent(groupModel)}\n </th>\n </tr>\n {node.nodes.map(child =>\n child.kind === 'spacer'\n ? this.renderVirtualSpacer(child, options.model.totalColumns)\n : this.renderVirtualRow(\n child,\n options,\n rowsById,\n rowKeys.get(child.index) ?? `virtual-row-${child.index}`\n )\n )}\n </tbody>\n );\n }\n\n private renderVirtualBodies(options: TableBodyRenderOptions) {\n const { model, plan } = options;\n if (!plan) return null;\n let lookup = this.virtualLookupCache;\n if (!lookup || lookup.rows !== model.loadedRows || lookup.groups !== model.groups) {\n lookup = {\n rows: model.loadedRows,\n groups: model.groups,\n rowsById: new Map(model.loadedRows.map(row => [row.id, row])),\n groupsById: new Map(model.groups.map(groupModel => [groupModel.group.id, groupModel])),\n };\n this.virtualLookupCache = lookup;\n }\n const { rowsById, groupsById } = lookup;\n if (!model.grouped) {\n for (const scope of this.virtualRowPoolStates.keys()) {\n if (scope !== 'flat') this.virtualRowPoolStates.delete(scope);\n }\n const rowKeys = this.virtualRowPoolKeys(plan.nodes, 'flat');\n return (\n <tbody class=\"ds-table__body\">\n {plan.nodes.map(node =>\n node.kind === 'spacer'\n ? this.renderVirtualSpacer(node, model.totalColumns)\n : node.kind === 'row'\n ? this.renderVirtualRow(\n node,\n options,\n rowsById,\n rowKeys.get(node.index) ?? `virtual-row-${node.index}`\n )\n : null\n )}\n </tbody>\n );\n }\n\n const activeGroupScopes = new Set(\n plan.nodes\n .filter(\n (node): node is Extract<TableVirtualNode, { kind: 'group' }> => node.kind === 'group'\n )\n .map(node => `group:${node.groupId}`)\n );\n for (const scope of this.virtualRowPoolStates.keys()) {\n if (!activeGroupScopes.has(scope)) this.virtualRowPoolStates.delete(scope);\n }\n return plan.nodes.map(node => {\n if (node.kind === 'spacer') {\n return (\n <tbody class=\"ds-table__body ds-table__virtual-pad\" aria-hidden=\"true\" key={node.key}>\n {this.renderVirtualSpacer(node, model.totalColumns)}\n </tbody>\n );\n }\n if (node.kind === 'group') {\n return this.renderVirtualGroup(node, options, rowsById, groupsById);\n }\n return null;\n });\n }\n}\n",
1445
1445
  "type": "registry:ui"
1446
1446
  },
1447
1447
  {
@@ -1451,7 +1451,7 @@
1451
1451
  },
1452
1452
  {
1453
1453
  "path": "src/wc/components/Table/table-column-customizer.ts",
1454
- "content": "import type { MenuItemData } from '../Menu/menu-types';\nimport type { TableColumn } from './table-types';\n\nlet columnCustomizerSeq = 0;\n\nexport function nextTableColumnCustomizerElementId(): string {\n columnCustomizerSeq += 1;\n return `ds-table-column-customizer-${columnCustomizerSeq}`;\n}\n\nexport function isTableActionColumn(column: TableColumn): boolean {\n return column.kind === 'action';\n}\n\nexport function tableColumnCustomizerLabel(column: TableColumn): string {\n return column.header.trim() || column.headerLabel?.trim() || column.id;\n}\n\nexport function tableDataColumns(columns: TableColumn[]): TableColumn[] {\n return columns.filter(column => !isTableActionColumn(column));\n}\n\n/** Data-column ids in display order. Unknown, duplicate, and action ids are ignored. */\nexport function resolveTableColumnOrder(\n columns: TableColumn[],\n columnOrder: string[] | undefined\n): string[] {\n const catalogIds = tableDataColumns(columns).map(column => column.id);\n const catalog = new Set(catalogIds);\n const seen = new Set<string>();\n const ordered: string[] = [];\n\n for (const id of columnOrder ?? []) {\n if (!catalog.has(id) || seen.has(id)) continue;\n seen.add(id);\n ordered.push(id);\n }\n\n for (const id of catalogIds) {\n if (seen.has(id)) continue;\n ordered.push(id);\n }\n\n return ordered;\n}\n\n/**\n * Hidden data-column ids. Action and unknown ids are dropped. At least one data\n * column remains visible when the catalog has any data columns.\n */\nexport function resolveTableHiddenColumnIds(\n columns: TableColumn[],\n hiddenColumnIds: string[] | undefined\n): string[] {\n const dataIds = tableDataColumns(columns).map(column => column.id);\n const data = new Set(dataIds);\n const hidden = [...new Set((hiddenColumnIds ?? []).filter(id => data.has(id)))];\n if (dataIds.length === 0 || hidden.length < dataIds.length) return hidden;\n\n const keep = dataIds[0];\n return hidden.filter(id => id !== keep);\n}\n\nexport function resolveTableVisibleColumns(\n columns: TableColumn[],\n options: {\n columnCustomizer?: boolean;\n hiddenColumnIds?: string[];\n columnOrder?: string[];\n } = {}\n): TableColumn[] {\n if (!options.columnCustomizer) return columns;\n\n const byId = new Map(columns.map(column => [column.id, column]));\n const hidden = new Set(resolveTableHiddenColumnIds(columns, options.hiddenColumnIds));\n const visibleData = resolveTableColumnOrder(columns, options.columnOrder)\n .map(id => byId.get(id))\n .filter((column): column is TableColumn => !!column && !hidden.has(column.id));\n\n return [...visibleData, ...columns.filter(isTableActionColumn)];\n}\n\nexport function canToggleTableColumnHidden(\n columns: TableColumn[],\n hiddenColumnIds: string[] | undefined,\n columnId: string\n): boolean {\n const column = columns.find(candidate => candidate.id === columnId);\n if (!column || isTableActionColumn(column)) return false;\n\n const hidden = new Set(resolveTableHiddenColumnIds(columns, hiddenColumnIds));\n if (hidden.has(columnId)) return true;\n\n const visibleData = tableDataColumns(columns).filter(candidate => !hidden.has(candidate.id));\n return visibleData.length > 1;\n}\n\nexport function toggleTableColumnHidden(\n columns: TableColumn[],\n hiddenColumnIds: string[] | undefined,\n columnId: string\n): string[] {\n const resolved = resolveTableHiddenColumnIds(columns, hiddenColumnIds);\n if (!canToggleTableColumnHidden(columns, resolved, columnId)) return resolved;\n\n const hidden = new Set(resolved);\n if (hidden.has(columnId)) hidden.delete(columnId);\n else hidden.add(columnId);\n return resolveTableHiddenColumnIds(columns, [...hidden]);\n}\n\nexport function moveTableColumnOrder(order: string[], fromId: string, toId: string): string[] {\n const from = order.indexOf(fromId);\n const to = order.indexOf(toId);\n if (from < 0 || to < 0 || from === to) return order;\n\n const next = [...order];\n next.splice(from, 1);\n next.splice(to, 0, fromId);\n return next;\n}\n\nexport function moveTableColumnInOrder(\n order: string[],\n columnId: string,\n offset: number\n): string[] {\n const from = order.indexOf(columnId);\n if (from < 0 || offset === 0) return order;\n return moveTableColumnOrder(order, columnId, order[from + offset] ?? columnId);\n}\n\nexport interface TableColumnCustomizerItem {\n column: TableColumn;\n label: string;\n visible: boolean;\n hideable: boolean;\n reorderable: boolean;\n}\n\nexport function tableColumnCustomizerItems(\n columns: TableColumn[],\n hiddenColumnIds: string[] | undefined,\n columnOrder: string[] | undefined\n): TableColumnCustomizerItem[] {\n const byId = new Map(columns.map(column => [column.id, column]));\n const resolvedHidden = resolveTableHiddenColumnIds(columns, hiddenColumnIds);\n const hidden = new Set(resolvedHidden);\n const dataItems = resolveTableColumnOrder(columns, columnOrder).flatMap(id => {\n const column = byId.get(id);\n if (!column) return [];\n return [\n {\n column,\n label: tableColumnCustomizerLabel(column),\n visible: !hidden.has(id),\n hideable: canToggleTableColumnHidden(columns, resolvedHidden, id),\n reorderable: true,\n },\n ];\n });\n return dataItems;\n}\n\n/** Menu switch rows for the table-owned column customizer. */\nexport function tableColumnCustomizerMenuItems(\n columns: TableColumn[],\n hiddenColumnIds: string[] | undefined,\n columnOrder: string[] | undefined\n): MenuItemData[] {\n return tableColumnCustomizerItems(columns, hiddenColumnIds, columnOrder).map(item => ({\n label: item.label,\n value: item.column.id,\n showSwitch: true,\n switchValue: item.visible,\n isInactive: !item.hideable,\n reorderable: item.reorderable,\n }));\n}\n",
1454
+ "content": "import type { MenuItemData } from '../Menu/menu-types';\nimport {\n canToggleListHidden,\n listCustomizerMenuItems,\n moveListOrder,\n moveListOrderBy,\n resolveListHiddenIds,\n resolveListOrder,\n toggleListHidden,\n type ListCustomizerOptions,\n} from '../../utils/list-customizer';\nimport type { TableColumn } from './table-types';\n\n/**\n * Table's customizer policy: the last visible data column cannot be hidden, and\n * rows reorder. Everything below is a thin adapter over the domain-neutral\n * helpers in utils/list-customizer so other catalogs can reuse the same state\n * machine with different policy.\n */\nconst TABLE_COLUMN_CUSTOMIZER: ListCustomizerOptions = { minVisible: 1, reorderable: true };\n\nlet columnCustomizerSeq = 0;\n\nexport function nextTableColumnCustomizerElementId(): string {\n columnCustomizerSeq += 1;\n return `ds-table-column-customizer-${columnCustomizerSeq}`;\n}\n\nexport function isTableActionColumn(column: TableColumn): boolean {\n return column.kind === 'action';\n}\n\nexport function tableColumnCustomizerLabel(column: TableColumn): string {\n return column.label.trim() || column.accessibleLabel?.trim() || column.id;\n}\n\nexport function tableDataColumns(columns: TableColumn[]): TableColumn[] {\n return columns.filter(column => !isTableActionColumn(column));\n}\n\n/** Data-column ids in display order. Unknown, duplicate, and action ids are ignored. */\nexport function resolveTableColumnOrder(\n columns: TableColumn[],\n fieldOrder: string[] | undefined\n): string[] {\n return resolveListOrder(\n tableDataColumns(columns).map(column => column.id),\n fieldOrder\n );\n}\n\n/**\n * Hidden data-column ids. Action and unknown ids are dropped. At least one data\n * column remains visible when the catalog has any data columns.\n */\nexport function resolveTableHiddenColumnIds(\n columns: TableColumn[],\n hiddenFieldIds: string[] | undefined\n): string[] {\n return resolveListHiddenIds(\n tableDataColumns(columns).map(column => column.id),\n hiddenFieldIds,\n TABLE_COLUMN_CUSTOMIZER\n );\n}\n\nexport function resolveTableVisibleColumns(\n columns: TableColumn[],\n options: {\n columnCustomizer?: boolean;\n hiddenFieldIds?: string[];\n fieldOrder?: string[];\n } = {}\n): TableColumn[] {\n if (!options.columnCustomizer) return columns;\n\n const byId = new Map(columns.map(column => [column.id, column]));\n const hidden = new Set(resolveTableHiddenColumnIds(columns, options.hiddenFieldIds));\n const visibleData = resolveTableColumnOrder(columns, options.fieldOrder)\n .map(id => byId.get(id))\n .filter((column): column is TableColumn => !!column && !hidden.has(column.id));\n\n return [...visibleData, ...columns.filter(isTableActionColumn)];\n}\n\nexport function canToggleTableColumnHidden(\n columns: TableColumn[],\n hiddenFieldIds: string[] | undefined,\n fieldId: string\n): boolean {\n const column = columns.find(candidate => candidate.id === fieldId);\n if (!column || isTableActionColumn(column)) return false;\n\n return canToggleListHidden(\n tableDataColumns(columns).map(candidate => candidate.id),\n hiddenFieldIds,\n fieldId,\n TABLE_COLUMN_CUSTOMIZER\n );\n}\n\nexport function toggleTableColumnHidden(\n columns: TableColumn[],\n hiddenFieldIds: string[] | undefined,\n fieldId: string\n): string[] {\n const column = columns.find(candidate => candidate.id === fieldId);\n if (!column || isTableActionColumn(column)) {\n return resolveTableHiddenColumnIds(columns, hiddenFieldIds);\n }\n\n return toggleListHidden(\n tableDataColumns(columns).map(candidate => candidate.id),\n hiddenFieldIds,\n fieldId,\n TABLE_COLUMN_CUSTOMIZER\n );\n}\n\nexport function moveTableColumnOrder(order: string[], fromId: string, toId: string): string[] {\n return moveListOrder(order, fromId, toId);\n}\n\nexport function moveTableColumnInOrder(order: string[], fieldId: string, offset: number): string[] {\n return moveListOrderBy(order, fieldId, offset);\n}\n\nexport interface TableColumnCustomizerItem {\n column: TableColumn;\n label: string;\n visible: boolean;\n hideable: boolean;\n reorderable: boolean;\n}\n\nexport function tableColumnCustomizerItems(\n columns: TableColumn[],\n hiddenFieldIds: string[] | undefined,\n fieldOrder: string[] | undefined\n): TableColumnCustomizerItem[] {\n const byId = new Map(columns.map(column => [column.id, column]));\n const resolvedHidden = resolveTableHiddenColumnIds(columns, hiddenFieldIds);\n const hidden = new Set(resolvedHidden);\n const dataItems = resolveTableColumnOrder(columns, fieldOrder).flatMap(id => {\n const column = byId.get(id);\n if (!column) return [];\n return [\n {\n column,\n label: tableColumnCustomizerLabel(column),\n visible: !hidden.has(id),\n hideable: canToggleTableColumnHidden(columns, resolvedHidden, id),\n reorderable: true,\n },\n ];\n });\n return dataItems;\n}\n\n/** Menu switch rows for the table-owned column customizer. */\nexport function tableColumnCustomizerMenuItems(\n columns: TableColumn[],\n hiddenFieldIds: string[] | undefined,\n fieldOrder: string[] | undefined\n): MenuItemData[] {\n return listCustomizerMenuItems(\n tableDataColumns(columns).map(column => ({\n id: column.id,\n label: tableColumnCustomizerLabel(column),\n })),\n hiddenFieldIds,\n fieldOrder,\n TABLE_COLUMN_CUSTOMIZER\n );\n}\n",
1455
1455
  "type": "registry:ui"
1456
1456
  },
1457
1457
  {
@@ -1461,7 +1461,7 @@
1461
1461
  },
1462
1462
  {
1463
1463
  "path": "src/wc/components/Table/table-group-load-controller.ts",
1464
- "content": "import type {\n TableGroup,\n TableGroupLoadMoreDetail,\n TableLoadMoreMode,\n TableLoadMoreReason,\n} from './table-types';\n\nexport interface TableGroupLoadControllerState {\n enabled: boolean;\n loadMoreMode: TableLoadMoreMode;\n loadMoreThreshold: number;\n containedScroll: boolean;\n groups: TableGroup[];\n viewport: HTMLElement | null;\n sentinels: ReadonlyMap<string, HTMLElement>;\n loadingMoreLabel: string;\n endOfResultsLabel: string;\n rowsLoadedLabel: string;\n}\n\nexport interface TableGroupLoadControllerOptions {\n state: () => TableGroupLoadControllerState;\n announce: (message: string) => void;\n request: (detail: TableGroupLoadMoreDetail) => void;\n}\n\ninterface GroupSnapshot {\n count: number;\n hasMore: boolean;\n loading: boolean;\n error: string;\n identity: string | number;\n}\n\nfunction groupIdentity(group: TableGroup): string | number {\n return group.loadIdentity ?? group.id;\n}\n\nfunction formatGroupLabel(template: string, group: TableGroup, count?: number): string {\n return template\n .split('{group}')\n .join(group.label)\n .split('{count}')\n .join(String(count ?? group.rows.length))\n .split('{loaded}')\n .join(String(group.rows.length))\n .split('{total}')\n .join(String(group.totalCount ?? group.rows.length));\n}\n\n/** Owns request guards, announcements, and observation for independently loaded groups. */\nexport class TableGroupLoadController {\n readonly intersectionSupported = typeof IntersectionObserver !== 'undefined';\n\n private connected = false;\n private intersectionObserver: IntersectionObserver | null = null;\n private snapshots = new Map<string, GroupSnapshot>();\n private pending = new Map<string, { identity: string | number; count: number }>();\n\n constructor(private readonly options: TableGroupLoadControllerOptions) {}\n\n initialize(): void {\n this.snapshots = this.createSnapshots();\n }\n\n connect(): void {\n this.connected = true;\n this.refresh();\n }\n\n disconnect(): void {\n this.connected = false;\n this.disconnectObserver();\n }\n\n refresh(): void {\n if (!this.connected) return;\n const state = this.options.state();\n if (\n !state.enabled ||\n state.loadMoreMode !== 'auto' ||\n !this.intersectionSupported ||\n !state.viewport\n ) {\n this.disconnectObserver();\n return;\n }\n\n this.disconnectObserver();\n const threshold = Number.isFinite(state.loadMoreThreshold)\n ? Math.max(0, state.loadMoreThreshold)\n : 0;\n this.intersectionObserver = new IntersectionObserver(\n entries => {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue;\n const groupId = (entry.target as HTMLElement).dataset.groupId;\n if (groupId) this.request(groupId, 'auto');\n }\n },\n {\n root: state.containedScroll ? state.viewport : null,\n rootMargin: `0px 0px ${threshold}px 0px`,\n }\n );\n\n for (const [groupId, sentinel] of state.sentinels) {\n const group = state.groups.find(item => item.id === groupId);\n if (!group?.hasMore || group.loadingMore || group.loadMoreError?.trim()) continue;\n this.intersectionObserver.observe(sentinel);\n }\n }\n\n structureChanged(): void {\n this.pending.clear();\n this.snapshots = this.createSnapshots();\n this.disconnectObserver();\n }\n\n dataChanged(): void {\n const state = this.options.state();\n const nextSnapshots = new Map<string, GroupSnapshot>();\n\n for (const group of state.groups) {\n const next = this.snapshot(group);\n const previous = this.snapshots.get(group.id);\n const pending = this.pending.get(group.id);\n\n if (!previous || previous.identity !== next.identity) {\n this.pending.delete(group.id);\n } else {\n if (next.count > previous.count) {\n this.options.announce(\n formatGroupLabel(state.rowsLoadedLabel, group, next.count - previous.count)\n );\n }\n if (!previous.error && next.error) {\n this.pending.delete(group.id);\n this.options.announce(next.error);\n }\n if (previous.hasMore && !next.hasMore) {\n this.pending.delete(group.id);\n this.options.announce(formatGroupLabel(state.endOfResultsLabel, group));\n }\n if (pending && next.count > pending.count) this.pending.delete(group.id);\n if (pending && previous.loading && !next.loading && next.count === pending.count) {\n this.pending.delete(group.id);\n }\n }\n\n nextSnapshots.set(group.id, next);\n }\n\n this.snapshots = nextSnapshots;\n for (const groupId of this.pending.keys()) {\n if (!nextSnapshots.has(groupId)) this.pending.delete(groupId);\n }\n this.disconnectObserver();\n }\n\n configurationChanged(): void {\n this.disconnectObserver();\n }\n\n request(groupId: string, reason: TableLoadMoreReason): void {\n const state = this.options.state();\n const group = state.groups.find(item => item.id === groupId);\n if (\n !state.enabled ||\n !group?.hasMore ||\n group.loadingMore ||\n (reason !== 'retry' && !!group.loadMoreError?.trim()) ||\n this.pending.has(groupId)\n )\n return;\n\n const identity = groupIdentity(group);\n this.pending.set(groupId, { identity, count: group.rows.length });\n this.options.announce(formatGroupLabel(state.loadingMoreLabel, group));\n this.options.request({\n groupId,\n reason,\n loadIdentity: identity,\n loadedRowCount: group.rows.length,\n });\n }\n\n private createSnapshots(): Map<string, GroupSnapshot> {\n return new Map(this.options.state().groups.map(group => [group.id, this.snapshot(group)]));\n }\n\n private snapshot(group: TableGroup): GroupSnapshot {\n return {\n count: group.rows.length,\n hasMore: !!group.hasMore,\n loading: !!group.loadingMore,\n error: group.loadMoreError?.trim() ?? '',\n identity: groupIdentity(group),\n };\n }\n\n private disconnectObserver(): void {\n this.intersectionObserver?.disconnect();\n this.intersectionObserver = null;\n }\n}\n",
1464
+ "content": "import type {\n DataGroup,\n TableGroupLoadMoreDetail,\n TableLoadMoreMode,\n TableLoadMoreReason,\n} from './table-types';\n\nexport interface TableGroupLoadControllerState {\n enabled: boolean;\n loadMoreMode: TableLoadMoreMode;\n loadMoreThreshold: number;\n containedScroll: boolean;\n groups: DataGroup[];\n viewport: HTMLElement | null;\n sentinels: ReadonlyMap<string, HTMLElement>;\n loadingMoreLabel: string;\n endOfResultsLabel: string;\n rowsLoadedLabel: string;\n}\n\nexport interface TableGroupLoadControllerOptions {\n state: () => TableGroupLoadControllerState;\n announce: (message: string) => void;\n request: (detail: TableGroupLoadMoreDetail) => void;\n}\n\ninterface GroupSnapshot {\n count: number;\n hasMore: boolean;\n loading: boolean;\n error: string;\n identity: string | number;\n}\n\nfunction groupIdentity(group: DataGroup): string | number {\n return group.loadIdentity ?? group.id;\n}\n\nfunction formatGroupLabel(template: string, group: DataGroup, count?: number): string {\n return template\n .split('{group}')\n .join(group.label)\n .split('{count}')\n .join(String(count ?? group.rows.length))\n .split('{loaded}')\n .join(String(group.rows.length))\n .split('{total}')\n .join(String(group.totalCount ?? group.rows.length));\n}\n\n/** Owns request guards, announcements, and observation for independently loaded groups. */\nexport class TableGroupLoadController {\n readonly intersectionSupported = typeof IntersectionObserver !== 'undefined';\n\n private connected = false;\n private intersectionObserver: IntersectionObserver | null = null;\n private snapshots = new Map<string, GroupSnapshot>();\n private pending = new Map<string, { identity: string | number; count: number }>();\n\n constructor(private readonly options: TableGroupLoadControllerOptions) {}\n\n initialize(): void {\n this.snapshots = this.createSnapshots();\n }\n\n connect(): void {\n this.connected = true;\n this.refresh();\n }\n\n disconnect(): void {\n this.connected = false;\n this.disconnectObserver();\n }\n\n refresh(): void {\n if (!this.connected) return;\n const state = this.options.state();\n if (\n !state.enabled ||\n state.loadMoreMode !== 'auto' ||\n !this.intersectionSupported ||\n !state.viewport\n ) {\n this.disconnectObserver();\n return;\n }\n\n this.disconnectObserver();\n const threshold = Number.isFinite(state.loadMoreThreshold)\n ? Math.max(0, state.loadMoreThreshold)\n : 0;\n this.intersectionObserver = new IntersectionObserver(\n entries => {\n for (const entry of entries) {\n if (!entry.isIntersecting) continue;\n const groupId = (entry.target as HTMLElement).dataset.groupId;\n if (groupId) this.request(groupId, 'auto');\n }\n },\n {\n root: state.containedScroll ? state.viewport : null,\n rootMargin: `0px 0px ${threshold}px 0px`,\n }\n );\n\n for (const [groupId, sentinel] of state.sentinels) {\n const group = state.groups.find(item => item.id === groupId);\n if (!group?.hasMore || group.loadingMore || group.loadMoreError?.trim()) continue;\n this.intersectionObserver.observe(sentinel);\n }\n }\n\n structureChanged(): void {\n this.pending.clear();\n this.snapshots = this.createSnapshots();\n this.disconnectObserver();\n }\n\n dataChanged(): void {\n const state = this.options.state();\n const nextSnapshots = new Map<string, GroupSnapshot>();\n\n for (const group of state.groups) {\n const next = this.snapshot(group);\n const previous = this.snapshots.get(group.id);\n const pending = this.pending.get(group.id);\n\n if (!previous || previous.identity !== next.identity) {\n this.pending.delete(group.id);\n } else {\n if (next.count > previous.count) {\n this.options.announce(\n formatGroupLabel(state.rowsLoadedLabel, group, next.count - previous.count)\n );\n }\n if (!previous.error && next.error) {\n this.pending.delete(group.id);\n this.options.announce(next.error);\n }\n if (previous.hasMore && !next.hasMore) {\n this.pending.delete(group.id);\n this.options.announce(formatGroupLabel(state.endOfResultsLabel, group));\n }\n if (pending && next.count > pending.count) this.pending.delete(group.id);\n if (pending && previous.loading && !next.loading && next.count === pending.count) {\n this.pending.delete(group.id);\n }\n }\n\n nextSnapshots.set(group.id, next);\n }\n\n this.snapshots = nextSnapshots;\n for (const groupId of this.pending.keys()) {\n if (!nextSnapshots.has(groupId)) this.pending.delete(groupId);\n }\n this.disconnectObserver();\n }\n\n configurationChanged(): void {\n this.disconnectObserver();\n }\n\n request(groupId: string, reason: TableLoadMoreReason): void {\n const state = this.options.state();\n const group = state.groups.find(item => item.id === groupId);\n if (\n !state.enabled ||\n !group?.hasMore ||\n group.loadingMore ||\n (reason !== 'retry' && !!group.loadMoreError?.trim()) ||\n this.pending.has(groupId)\n )\n return;\n\n const identity = groupIdentity(group);\n this.pending.set(groupId, { identity, count: group.rows.length });\n this.options.announce(formatGroupLabel(state.loadingMoreLabel, group));\n this.options.request({\n groupId,\n reason,\n loadIdentity: identity,\n loadedRowCount: group.rows.length,\n });\n }\n\n private createSnapshots(): Map<string, GroupSnapshot> {\n return new Map(this.options.state().groups.map(group => [group.id, this.snapshot(group)]));\n }\n\n private snapshot(group: DataGroup): GroupSnapshot {\n return {\n count: group.rows.length,\n hasMore: !!group.hasMore,\n loading: !!group.loadingMore,\n error: group.loadMoreError?.trim() ?? '',\n identity: groupIdentity(group),\n };\n }\n\n private disconnectObserver(): void {\n this.intersectionObserver?.disconnect();\n this.intersectionObserver = null;\n }\n}\n",
1465
1465
  "type": "registry:ui"
1466
1466
  },
1467
1467
  {
@@ -1481,12 +1481,12 @@
1481
1481
  },
1482
1482
  {
1483
1483
  "path": "src/wc/components/Table/table-load-view.tsx",
1484
- "content": "import { h } from '@stencil/core';\n\ninterface TableLoadContentOptions {\n error?: string;\n loading: boolean;\n hasMore: boolean;\n manualFallback: boolean;\n retryLabel: string;\n loadingLabel: string;\n loadMoreLabel: string;\n loadMoreAriaLabel?: string;\n endOfResultsLabel?: string;\n onRetry: () => void;\n onLoadMore: () => void;\n}\n\n/** Shared visible state machine for top-level and per-group incremental loading bands. */\nexport function renderTableLoadContent(options: TableLoadContentOptions) {\n if (options.error) {\n return (\n <span class=\"ds-table__load-content ds-table__load-content--error\">\n <span class=\"ds-table__load-copy\">\n <ds-icon name=\"ErrorTriangle\" size=\"md\" color=\"secondary\" aria-hidden=\"true\" />\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"secondary\">\n {options.error}\n </ds-text>\n </span>\n <ds-button-unfilled label={options.retryLabel} size=\"md\" onDsClick={options.onRetry} />\n </span>\n );\n }\n\n if (options.loading) {\n return (\n <span class=\"ds-table__load-content\">\n <ds-loader size=\"md\" color=\"secondary\" />\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"secondary\">\n {options.loadingLabel}\n </ds-text>\n </span>\n );\n }\n\n if (options.hasMore && options.manualFallback) {\n return (\n <span class=\"ds-table__load-content\">\n <ds-button-unfilled\n label={options.loadMoreLabel}\n aria-label={options.loadMoreAriaLabel}\n size=\"md\"\n onDsClick={options.onLoadMore}\n />\n </span>\n );\n }\n\n if (options.hasMore) return <span class=\"ds-table__auto-sentinel\" aria-hidden=\"true\" />;\n if (!options.endOfResultsLabel) return null;\n return (\n <ds-text class=\"ds-table__load-content\" as=\"span\" variant=\"text-body-medium\" color=\"secondary\">\n {options.endOfResultsLabel}\n </ds-text>\n );\n}\n",
1484
+ "content": "import { h } from '@stencil/core';\n\ninterface TableLoadContentOptions {\n error?: string;\n loading: boolean;\n hasMore: boolean;\n manualFallback: boolean;\n retryLabel: string;\n loadingLabel: string;\n loadMoreLabel: string;\n loadMoreAriaLabel?: string;\n endOfResultsLabel?: string;\n onRetry: () => void;\n onLoadMore: () => void;\n}\n\n/** Shared visible state machine for top-level and per-group incremental loading bands. */\nexport function renderTableLoadContent(options: TableLoadContentOptions) {\n if (options.error) {\n return (\n <span class=\"ds-table__load-content ds-table__load-content--error\">\n <span class=\"ds-table__load-copy\">\n <ds-icon name=\"ExclamationTriangle\" size=\"md\" color=\"secondary\" aria-hidden=\"true\" />\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"secondary\">\n {options.error}\n </ds-text>\n </span>\n <ds-button-unfilled label={options.retryLabel} size=\"md\" onDsClick={options.onRetry} />\n </span>\n );\n }\n\n if (options.loading) {\n return (\n <span class=\"ds-table__load-content\">\n <ds-loader size=\"md\" color=\"secondary\" />\n <ds-text as=\"span\" variant=\"text-body-medium\" color=\"secondary\">\n {options.loadingLabel}\n </ds-text>\n </span>\n );\n }\n\n if (options.hasMore && options.manualFallback) {\n return (\n <span class=\"ds-table__load-content\">\n <ds-button-unfilled\n label={options.loadMoreLabel}\n aria-label={options.loadMoreAriaLabel}\n size=\"md\"\n onDsClick={options.onLoadMore}\n />\n </span>\n );\n }\n\n if (options.hasMore) return <span class=\"ds-table__auto-sentinel\" aria-hidden=\"true\" />;\n if (!options.endOfResultsLabel) return null;\n return (\n <ds-text class=\"ds-table__load-content\" as=\"span\" variant=\"text-body-medium\" color=\"secondary\">\n {options.endOfResultsLabel}\n </ds-text>\n );\n}\n",
1485
1485
  "type": "registry:ui"
1486
1486
  },
1487
1487
  {
1488
1488
  "path": "src/wc/components/Table/table-model.ts",
1489
- "content": "import type {\n TableCellBlank,\n TableCellAction,\n TableCellEmpty,\n TableCellIcon,\n TableCellIconText,\n TableCellScoreText,\n TableCellImage,\n TableCellPrimaryText,\n TableCellText,\n TableCellTag,\n TableCellTags,\n TableCellValue,\n TableColumn,\n TableGroup,\n TableGroupAccessory,\n TableGroupHero,\n TableRow,\n TableSortState,\n} from './table-types';\nimport { isSafetyScoreLevel } from '../Score/score-model';\n\nexport interface TableSelectionState {\n selectableRowIds: string[];\n selectedLoadedCount: number;\n allSelected: boolean;\n indeterminate: boolean;\n}\n\nconst TABLE_COLUMN_WIDTH_TOKENS = {\n xs: '--dimension-table-column-width-xs',\n sm: '--dimension-table-column-width-sm',\n md: '--dimension-table-column-width-md',\n lg: '--dimension-table-column-width-lg',\n xl: '--dimension-table-column-width-xl',\n} as const;\n\nconst TABLE_IMAGE_COLUMN_WIDTH_VARIABLES = {\n 1: '--_table-image-column-inline-size',\n 2: '--_table-image-column-inline-size-multi',\n 3: '--_table-image-column-inline-size-triple',\n} as const;\n\nexport function isTableCellText(value: TableCellValue): value is TableCellText {\n return typeof value === 'object' && value !== null && 'primary' in value && !('kind' in value);\n}\n\nexport function isTableCellPrimaryText(value: TableCellValue): value is TableCellPrimaryText {\n return (\n typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'primary-text'\n );\n}\n\nexport function isTableCellTag(value: TableCellValue): value is TableCellTag {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'tag';\n}\n\nexport function isTableCellTags(value: TableCellValue): value is TableCellTags {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'tags';\n}\n\nexport function isTableCellIcon(value: TableCellValue): value is TableCellIcon {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'icon';\n}\n\nexport function isTableCellIconText(value: TableCellValue): value is TableCellIconText {\n return (\n typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'icon-text'\n );\n}\n\nexport function isTableCellScoreText(value: TableCellValue): value is TableCellScoreText {\n return (\n typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'score-text'\n );\n}\n\nexport function isTableCellImage(value: TableCellValue): value is TableCellImage {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'image';\n}\n\nexport function isTableCellAction(value: TableCellValue): value is TableCellAction {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'action';\n}\n\nexport function isTableCellEmpty(value: TableCellValue): value is TableCellEmpty {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'empty';\n}\n\nexport function isTableCellBlank(value: TableCellValue): value is TableCellBlank {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'blank';\n}\n\nexport function tableCellPrimary(value: TableCellValue): string | number | null {\n if (value == null) return null;\n if (isTableCellTag(value)) return value.label;\n if (isTableCellTags(value)) return value.items.map(item => item.label).join(', ');\n if (isTableCellIcon(value)) return null;\n if (isTableCellIconText(value)) return value.primary;\n if (isTableCellScoreText(value)) return value.primary;\n if (isTableCellImage(value)) return value.alt;\n if (isTableCellAction(value)) return value.label ?? value.ariaLabel;\n if (isTableCellPrimaryText(value)) return value.primary;\n if (isTableCellEmpty(value) || isTableCellBlank(value)) return null;\n return isTableCellText(value) ? value.primary : value;\n}\n\nexport function tableRowSelectionLabel(row: TableRow, columns: TableColumn[]): string {\n const explicit = row.selectionLabel?.trim();\n if (explicit) return explicit;\n\n for (const column of columns) {\n const primary = tableCellPrimary(row.cells[column.id]);\n if (primary != null && String(primary).trim()) return String(primary);\n }\n\n return row.id;\n}\n\nexport function nextTableSortState(\n current: TableSortState | null | undefined,\n columnId: string\n): TableSortState {\n if (current?.columnId !== columnId) return { columnId, direction: 'asc' };\n if (current.direction === 'asc') return { columnId, direction: 'desc' };\n return { columnId, direction: 'asc' };\n}\n\nexport function toggleTableGroupCollapsed(collapsedGroupIds: string[], groupId: string): string[] {\n const next = new Set(collapsedGroupIds);\n if (next.has(groupId)) next.delete(groupId);\n else next.add(groupId);\n return [...next];\n}\n\nexport function nextTableGroupsCollapsed(\n collapsedGroupIds: string[],\n groupIds: string[]\n): string[] {\n if (groupIds.length === 0) return [];\n const collapsed = new Set(collapsedGroupIds);\n const allCollapsed = groupIds.every(id => collapsed.has(id));\n return allCollapsed ? [] : [...groupIds];\n}\n\n/** Host for grouped collapse-all: the trailing action lane or a scrollport-owned overlay. */\nexport type TableCollapseAllHost = { columnId: string; mode: 'action' } | { mode: 'floating' };\n\nexport function tableCollapseAllHost(columns: TableColumn[]): TableCollapseAllHost | undefined {\n if (columns.length === 0) return undefined;\n for (let index = columns.length - 1; index >= 0; index -= 1) {\n const column = columns[index]!;\n if (column.kind === 'action') return { columnId: column.id, mode: 'action' };\n }\n return { mode: 'floating' };\n}\n\nexport function clampTableColumnSize(column: TableColumn): number | undefined {\n if (typeof column.size !== 'number' || !Number.isFinite(column.size) || column.size <= 0) {\n return undefined;\n }\n\n const minimum =\n Number.isFinite(column.minSize) && (column.minSize ?? 0) > 0 ? column.minSize! : 0;\n const maximum =\n Number.isFinite(column.maxSize) && (column.maxSize ?? 0) > 0\n ? Math.max(column.maxSize!, minimum)\n : Number.POSITIVE_INFINITY;\n\n return Math.min(Math.max(column.size!, minimum), maximum);\n}\n\nexport function tableColumnSize(column: TableColumn): string | undefined {\n if (typeof column.size === 'string') {\n return `var(${TABLE_COLUMN_WIDTH_TOKENS[column.size]})`;\n }\n const width = clampTableColumnSize(column);\n if (width != null) return `${width}px`;\n if (column.imageTracks) {\n return `var(${TABLE_IMAGE_COLUMN_WIDTH_VARIABLES[column.imageTracks]})`;\n }\n return column.kind === 'action' ? 'var(--dimension-size-500)' : undefined;\n}\n\n/**\n * Fully sized tables add one internal lane that absorbs spare inline space and\n * collapses to zero at the explicit overflow width. Keep trailing action and\n * sticky-end lanes after it so those controls remain at the visible edge.\n */\nexport function tableElasticSpacerIndex(columns: TableColumn[]): number | undefined {\n if (columns.length === 0 || columns.some(column => !tableColumnSize(column))) return undefined;\n let index = columns.length;\n while (index > 0) {\n const column = columns[index - 1]!;\n if (column.kind !== 'action' && column.sticky !== 'end') break;\n index -= 1;\n }\n return index;\n}\n\nexport function tableExplicitMinWidth(columns: TableColumn[]): string | undefined {\n const widths = columns.map(tableColumnSize).filter((width): width is string => !!width);\n if (widths.length === 0) return undefined;\n return widths.length === 1 ? widths[0] : `calc(${widths.join(' + ')})`;\n}\n\nexport function tableRows(rows: TableRow[], groups: TableGroup[], grouped: boolean): TableRow[] {\n return grouped ? groups.flatMap(group => group.rows) : rows;\n}\n\nfunction canSelectRow(row: TableRow): boolean {\n return row.selectable !== false && !row.disabled;\n}\n\nexport function deriveTableSelectionState(\n rows: TableRow[],\n selectedRowIds: readonly string[]\n): TableSelectionState {\n const selected = new Set(selectedRowIds);\n const selectableRowIds = rows.filter(canSelectRow).map(row => row.id);\n const selectedLoadedCount = selectableRowIds.reduce(\n (count, id) => count + (selected.has(id) ? 1 : 0),\n 0\n );\n\n return {\n selectableRowIds,\n selectedLoadedCount,\n allSelected: selectableRowIds.length > 0 && selectedLoadedCount === selectableRowIds.length,\n indeterminate: selectedLoadedCount > 0 && selectedLoadedCount < selectableRowIds.length,\n };\n}\n\nexport function toggleTableRowSelection(\n selectedRowIds: readonly string[],\n row: TableRow\n): string[] {\n if (!canSelectRow(row)) return [...selectedRowIds];\n const selected = new Set(selectedRowIds);\n if (selected.has(row.id)) selected.delete(row.id);\n else selected.add(row.id);\n return [...selected];\n}\n\nexport function toggleAllLoadedTableRows(\n selectedRowIds: readonly string[],\n loadedRows: TableRow[]\n): string[] {\n const selected = new Set(selectedRowIds);\n const state = deriveTableSelectionState(loadedRows, selectedRowIds);\n\n for (const id of state.selectableRowIds) {\n if (state.allSelected) selected.delete(id);\n else selected.add(id);\n }\n\n return [...selected];\n}\n\n/** Toggle every selectable loaded row that belongs to a group. */\nexport function toggleTableGroupSelection(\n selectedRowIds: readonly string[],\n groupRows: TableRow[]\n): string[] {\n return toggleAllLoadedTableRows(selectedRowIds, groupRows);\n}\n\nexport function resolvedTableGroupCount(group: TableGroup): number {\n const total = Number.isFinite(group.totalCount) ? Math.max(0, group.totalCount!) : 0;\n return Math.max(total, group.rows.length);\n}\n\n/** Accessory items shown on a group header's second track. */\nexport const TABLE_GROUP_ACCESSORY_LIMIT = 4;\n\n/** Trim, drop empty copy, and cap accessories at the second-track limit. */\nexport function tableGroupAccessories(group: TableGroup): TableGroupAccessory[] {\n const resolved: TableGroupAccessory[] = [];\n for (const item of group.accessories ?? []) {\n const text = item?.text?.trim() ?? '';\n if (!text) continue;\n const help = item.help?.trim();\n resolved.push(help ? { text, help } : { text });\n if (resolved.length === TABLE_GROUP_ACCESSORY_LIMIT) break;\n }\n return resolved;\n}\n\n/** Score recipes for a leading group-header hero spanning one or two tracks. */\nexport const TABLE_GROUP_HERO_SCORE_PRESENTATION = {\n 1: { size: 'sm', variant: 'default' },\n 2: { size: 'lg', variant: 'default' },\n} as const;\n\n/** Resolve a supported group-header hero, or undefined when none is supplied. */\nexport function tableGroupHero(group: TableGroup): TableGroupHero | undefined {\n const hero = group.hero;\n if (!hero || hero.kind !== 'score') return undefined;\n const label = hero.label?.trim();\n return {\n kind: 'score',\n value: hero.value,\n ...(isSafetyScoreLevel(hero.level) ? { level: hero.level } : {}),\n ...(hero.tracks === 2 ? { tracks: 2 as const } : {}),\n ...(label ? { label } : {}),\n ...(hero.isLoading ? { isLoading: true } : {}),\n };\n}\n\nexport const TABLE_GROUP_INTENTS = [\n 'brand',\n 'neutral',\n 'negative',\n 'warning',\n 'caution',\n 'positive',\n] as const;\n\nexport function isTableGroupIntent(value: unknown): value is (typeof TABLE_GROUP_INTENTS)[number] {\n return typeof value === 'string' && (TABLE_GROUP_INTENTS as readonly string[]).includes(value);\n}\n\nexport function tableGroupIntentClass(\n intent: (typeof TABLE_GROUP_INTENTS)[number] | undefined\n): string | undefined {\n return isTableGroupIntent(intent) ? `ds-table__group-cell--intent-${intent}` : undefined;\n}\n\n/** Title color for an intentful group label; defaults to primary when unset. */\nexport function tableGroupLabelColor(\n intent: (typeof TABLE_GROUP_INTENTS)[number] | undefined\n): 'primary' | 'brand' | 'negative' | 'warning' | 'caution' | 'positive' | `var(--${string})` {\n if (!isTableGroupIntent(intent)) return 'primary';\n if (intent === 'neutral') return 'var(--color-foreground-bold-neutral)';\n return intent;\n}\n\n/** Formats the optional table result footer. Returns null when either count is missing. */\nexport function formatTableResultSummary(\n displayed: number | null | undefined,\n total: number | null | undefined,\n label = 'Displaying {displayed} of {total}',\n locale?: string\n): string | null {\n if (!Number.isFinite(displayed) || !Number.isFinite(total)) return null;\n const normalizedTotal = Math.max(0, Math.trunc(total as number));\n const normalizedDisplayed = Math.min(\n normalizedTotal,\n Math.max(0, Math.trunc(displayed as number))\n );\n const formatter = new Intl.NumberFormat(locale);\n return label\n .replace('{displayed}', formatter.format(normalizedDisplayed))\n .replace('{total}', formatter.format(normalizedTotal));\n}\n\n/** Formats the virtual-mode total-only result footer. */\nexport function formatTableTotalSummary(\n total: number | null | undefined,\n label = '{total} items',\n locale?: string\n): string | null {\n if (!Number.isFinite(total)) return null;\n const normalizedTotal = Math.max(0, Math.trunc(total as number));\n return label.replace('{total}', new Intl.NumberFormat(locale).format(normalizedTotal));\n}\n\n/**\n * True when a footer slot belongs to the table itself: a light-DOM child of the\n * host, or a top-level node Stencil relocated into this table's footer. A\n * slotted ancestor marks a nested component boundary, so dialog action slots\n * inside footer-leading/footer-trailing content cannot replace the table copy.\n */\nexport function isOwnedTableFooterSlot(node: Element, host: Element): boolean {\n if (node.parentElement === host) return true;\n let insideTableFooter = false;\n for (\n let ancestor = node.parentElement;\n ancestor && ancestor !== host;\n ancestor = ancestor.parentElement\n ) {\n if (ancestor.hasAttribute('slot')) return false;\n if (ancestor.classList.contains('ds-table__footer')) insideTableFooter = true;\n }\n return insideTableFooter;\n}\n\nexport function hasOwnedTableFooterSlot(host: Element, slotName: string): boolean {\n return [...host.querySelectorAll(`[slot=\"${slotName}\"]`)].some(node =>\n isOwnedTableFooterSlot(node, host)\n );\n}\n\nexport function tableModelIssues(\n columns: TableColumn[],\n rows: TableRow[],\n groups: TableGroup[],\n grouped: boolean\n): string[] {\n const issues: string[] = [];\n const columnIds = new Set<string>();\n for (const column of columns) {\n if (!column.id.trim()) issues.push('Every column requires a non-empty id.');\n else if (columnIds.has(column.id)) issues.push(`Duplicate column id: ${column.id}`);\n if (!column.header.trim() && !column.headerLabel?.trim()) {\n issues.push(\n `Column ${column.id || '(missing id)'} requires a visible header or headerLabel.`\n );\n }\n columnIds.add(column.id);\n }\n\n const groupIds = new Set<string>();\n if (grouped) {\n for (const group of groups) {\n if (!group.id.trim()) issues.push('Every group requires a non-empty id.');\n else if (groupIds.has(group.id)) issues.push(`Duplicate group id: ${group.id}`);\n groupIds.add(group.id);\n if (group.totalCount != null && group.totalCount < group.rows.length) {\n issues.push(`Group ${group.id} totalCount is smaller than its loaded row count.`);\n }\n if (group.intent != null && !isTableGroupIntent(group.intent)) {\n issues.push(`Group ${group.id} has an unsupported intent.`);\n }\n const accessoryCount = (group.accessories ?? []).filter(\n item => typeof item?.text === 'string' && item.text.trim()\n ).length;\n if (accessoryCount > TABLE_GROUP_ACCESSORY_LIMIT) {\n issues.push(`Group ${group.id} has more than ${TABLE_GROUP_ACCESSORY_LIMIT} accessories.`);\n }\n if (group.hero != null && group.hero.kind !== 'score') {\n issues.push(`Group ${group.id} has an unsupported hero.`);\n }\n }\n }\n\n const rowIds = new Set<string>();\n for (const row of tableRows(rows, groups, grouped)) {\n if (!row.id.trim()) issues.push('Every row requires a non-empty id.');\n else if (rowIds.has(row.id)) issues.push(`Duplicate row id: ${row.id}`);\n rowIds.add(row.id);\n }\n\n return [...new Set(issues)];\n}\n",
1489
+ "content": "import type {\n TableCellBlank,\n TableCellAction,\n TableCellEmpty,\n TableCellIcon,\n TableCellIconText,\n TableCellScoreText,\n TableCellImage,\n TableCellPrimaryText,\n TableCellText,\n TableCellTag,\n TableCellTags,\n TableCellValue,\n TableColumn,\n DataGroup,\n TableGroupAccessory,\n TableGroupHero,\n TableRow,\n DataSortState,\n} from './table-types';\nimport { isSafetyScoreLevel } from '../Score/score-model';\n\nexport interface TableSelectionState {\n selectableRowIds: string[];\n selectedLoadedCount: number;\n allSelected: boolean;\n indeterminate: boolean;\n}\n\nconst TABLE_COLUMN_WIDTH_TOKENS = {\n xs: '--dimension-table-column-width-xs',\n sm: '--dimension-table-column-width-sm',\n md: '--dimension-table-column-width-md',\n lg: '--dimension-table-column-width-lg',\n xl: '--dimension-table-column-width-xl',\n} as const;\n\nconst TABLE_IMAGE_COLUMN_WIDTH_VARIABLES = {\n 1: '--_table-image-column-inline-size',\n 2: '--_table-image-column-inline-size-multi',\n 3: '--_table-image-column-inline-size-triple',\n} as const;\n\nexport function isTableCellText(value: TableCellValue): value is TableCellText {\n return typeof value === 'object' && value !== null && 'primary' in value && !('kind' in value);\n}\n\nexport function isTableCellPrimaryText(value: TableCellValue): value is TableCellPrimaryText {\n return (\n typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'primary-text'\n );\n}\n\nexport function isTableCellTag(value: TableCellValue): value is TableCellTag {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'tag';\n}\n\nexport function isTableCellTags(value: TableCellValue): value is TableCellTags {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'tags';\n}\n\nexport function isTableCellIcon(value: TableCellValue): value is TableCellIcon {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'icon';\n}\n\nexport function isTableCellIconText(value: TableCellValue): value is TableCellIconText {\n return (\n typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'icon-text'\n );\n}\n\nexport function isTableCellScoreText(value: TableCellValue): value is TableCellScoreText {\n return (\n typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'score-text'\n );\n}\n\nexport function isTableCellImage(value: TableCellValue): value is TableCellImage {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'image';\n}\n\nexport function isTableCellAction(value: TableCellValue): value is TableCellAction {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'action';\n}\n\nexport function isTableCellEmpty(value: TableCellValue): value is TableCellEmpty {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'empty';\n}\n\nexport function isTableCellBlank(value: TableCellValue): value is TableCellBlank {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'blank';\n}\n\nexport function tableCellPrimary(value: TableCellValue): string | number | null {\n if (value == null) return null;\n if (isTableCellTag(value)) return value.label;\n if (isTableCellTags(value)) return value.items.map(item => item.label).join(', ');\n if (isTableCellIcon(value)) return null;\n if (isTableCellIconText(value)) return value.primary;\n if (isTableCellScoreText(value)) return value.primary;\n if (isTableCellImage(value)) return value.alt;\n if (isTableCellAction(value)) return value.label ?? value.ariaLabel;\n if (isTableCellPrimaryText(value)) return value.primary;\n if (isTableCellEmpty(value) || isTableCellBlank(value)) return null;\n return isTableCellText(value) ? value.primary : value;\n}\n\nexport function tableRowSelectionLabel(row: TableRow, columns: TableColumn[]): string {\n const explicit = row.selectionLabel?.trim();\n if (explicit) return explicit;\n\n for (const column of columns) {\n const primary = tableCellPrimary(row.cells[column.id]);\n if (primary != null && String(primary).trim()) return String(primary);\n }\n\n return row.id;\n}\n\nexport function nextDataSortState(\n current: DataSortState | null | undefined,\n fieldId: string\n): DataSortState {\n if (current?.fieldId !== fieldId) return { fieldId, direction: 'asc' };\n if (current.direction === 'asc') return { fieldId, direction: 'desc' };\n return { fieldId, direction: 'asc' };\n}\n\nexport function toggleTableGroupCollapsed(collapsedGroupIds: string[], groupId: string): string[] {\n const next = new Set(collapsedGroupIds);\n if (next.has(groupId)) next.delete(groupId);\n else next.add(groupId);\n return [...next];\n}\n\nexport function nextTableGroupsCollapsed(\n collapsedGroupIds: string[],\n groupIds: string[]\n): string[] {\n if (groupIds.length === 0) return [];\n const collapsed = new Set(collapsedGroupIds);\n const allCollapsed = groupIds.every(id => collapsed.has(id));\n return allCollapsed ? [] : [...groupIds];\n}\n\n/** Host for grouped collapse-all: the trailing action lane or a scrollport-owned overlay. */\nexport type TableCollapseAllHost = { columnId: string; mode: 'action' } | { mode: 'floating' };\n\nexport function tableCollapseAllHost(columns: TableColumn[]): TableCollapseAllHost | undefined {\n if (columns.length === 0) return undefined;\n for (let index = columns.length - 1; index >= 0; index -= 1) {\n const column = columns[index]!;\n if (column.kind === 'action') return { columnId: column.id, mode: 'action' };\n }\n return { mode: 'floating' };\n}\n\nexport function clampTableColumnSize(column: TableColumn): number | undefined {\n if (typeof column.size !== 'number' || !Number.isFinite(column.size) || column.size <= 0) {\n return undefined;\n }\n\n const minimum =\n Number.isFinite(column.minSize) && (column.minSize ?? 0) > 0 ? column.minSize! : 0;\n const maximum =\n Number.isFinite(column.maxSize) && (column.maxSize ?? 0) > 0\n ? Math.max(column.maxSize!, minimum)\n : Number.POSITIVE_INFINITY;\n\n return Math.min(Math.max(column.size!, minimum), maximum);\n}\n\nexport function tableColumnSize(column: TableColumn): string | undefined {\n if (typeof column.size === 'string') {\n return `var(${TABLE_COLUMN_WIDTH_TOKENS[column.size]})`;\n }\n const width = clampTableColumnSize(column);\n if (width != null) return `${width}px`;\n if (column.imageTracks) {\n return `var(${TABLE_IMAGE_COLUMN_WIDTH_VARIABLES[column.imageTracks]})`;\n }\n return column.kind === 'action' ? 'var(--dimension-size-500)' : undefined;\n}\n\n/**\n * Fully sized tables add one internal lane that absorbs spare inline space and\n * collapses to zero at the explicit overflow width. Keep trailing action and\n * sticky-end lanes after it so those controls remain at the visible edge.\n */\nexport function tableElasticSpacerIndex(columns: TableColumn[]): number | undefined {\n if (columns.length === 0 || columns.some(column => !tableColumnSize(column))) return undefined;\n let index = columns.length;\n while (index > 0) {\n const column = columns[index - 1]!;\n if (column.kind !== 'action' && column.sticky !== 'end') break;\n index -= 1;\n }\n return index;\n}\n\nexport function tableExplicitMinWidth(columns: TableColumn[]): string | undefined {\n const widths = columns.map(tableColumnSize).filter((width): width is string => !!width);\n if (widths.length === 0) return undefined;\n return widths.length === 1 ? widths[0] : `calc(${widths.join(' + ')})`;\n}\n\nexport function tableRows(rows: TableRow[], groups: DataGroup[], grouped: boolean): TableRow[] {\n return grouped ? groups.flatMap(group => group.rows) : rows;\n}\n\nfunction canSelectRow(row: TableRow): boolean {\n return row.selectable !== false && !row.disabled;\n}\n\nexport function deriveTableSelectionState(\n rows: TableRow[],\n selectedRowIds: readonly string[]\n): TableSelectionState {\n const selected = new Set(selectedRowIds);\n const selectableRowIds = rows.filter(canSelectRow).map(row => row.id);\n const selectedLoadedCount = selectableRowIds.reduce(\n (count, id) => count + (selected.has(id) ? 1 : 0),\n 0\n );\n\n return {\n selectableRowIds,\n selectedLoadedCount,\n allSelected: selectableRowIds.length > 0 && selectedLoadedCount === selectableRowIds.length,\n indeterminate: selectedLoadedCount > 0 && selectedLoadedCount < selectableRowIds.length,\n };\n}\n\nexport function toggleTableRowSelection(\n selectedRowIds: readonly string[],\n row: TableRow\n): string[] {\n if (!canSelectRow(row)) return [...selectedRowIds];\n const selected = new Set(selectedRowIds);\n if (selected.has(row.id)) selected.delete(row.id);\n else selected.add(row.id);\n return [...selected];\n}\n\nexport function toggleAllLoadedTableRows(\n selectedRowIds: readonly string[],\n loadedRows: TableRow[]\n): string[] {\n const selected = new Set(selectedRowIds);\n const state = deriveTableSelectionState(loadedRows, selectedRowIds);\n\n for (const id of state.selectableRowIds) {\n if (state.allSelected) selected.delete(id);\n else selected.add(id);\n }\n\n return [...selected];\n}\n\n/** Toggle every selectable loaded row that belongs to a group. */\nexport function toggleTableGroupSelection(\n selectedRowIds: readonly string[],\n groupRows: TableRow[]\n): string[] {\n return toggleAllLoadedTableRows(selectedRowIds, groupRows);\n}\n\nexport function resolvedTableGroupCount(group: DataGroup): number {\n const total = Number.isFinite(group.totalCount) ? Math.max(0, group.totalCount!) : 0;\n return Math.max(total, group.rows.length);\n}\n\n/** Accessory items shown on a group header's second track. */\nexport const TABLE_GROUP_ACCESSORY_LIMIT = 4;\n\n/** Trim, drop empty copy, and cap accessories at the second-track limit. */\nexport function tableGroupAccessories(group: DataGroup): TableGroupAccessory[] {\n const resolved: TableGroupAccessory[] = [];\n for (const item of group.accessories ?? []) {\n const text = item?.text?.trim() ?? '';\n if (!text) continue;\n const help = item.help?.trim();\n resolved.push(help ? { text, help } : { text });\n if (resolved.length === TABLE_GROUP_ACCESSORY_LIMIT) break;\n }\n return resolved;\n}\n\n/** Score recipes for a leading group-header hero spanning one or two tracks. */\nexport const TABLE_GROUP_HERO_SCORE_PRESENTATION = {\n 1: { size: 'sm', variant: 'default' },\n 2: { size: 'lg', variant: 'default' },\n} as const;\n\n/** Resolve a supported group-header hero, or undefined when none is supplied. */\nexport function tableGroupHero(group: DataGroup): TableGroupHero | undefined {\n const hero = group.hero;\n if (!hero || hero.kind !== 'score') return undefined;\n const label = hero.label?.trim();\n return {\n kind: 'score',\n value: hero.value,\n ...(isSafetyScoreLevel(hero.level) ? { level: hero.level } : {}),\n ...(hero.tracks === 2 ? { tracks: 2 as const } : {}),\n ...(label ? { label } : {}),\n ...(hero.isLoading ? { isLoading: true } : {}),\n };\n}\n\nexport const TABLE_GROUP_INTENTS = [\n 'brand',\n 'neutral',\n 'negative',\n 'warning',\n 'caution',\n 'positive',\n] as const;\n\nexport function isTableGroupIntent(value: unknown): value is (typeof TABLE_GROUP_INTENTS)[number] {\n return typeof value === 'string' && (TABLE_GROUP_INTENTS as readonly string[]).includes(value);\n}\n\nexport function tableGroupIntentClass(\n intent: (typeof TABLE_GROUP_INTENTS)[number] | undefined\n): string | undefined {\n return isTableGroupIntent(intent) ? `ds-table__group-cell--intent-${intent}` : undefined;\n}\n\n/** Title color for an intentful group label; defaults to primary when unset. */\nexport function tableGroupLabelColor(\n intent: (typeof TABLE_GROUP_INTENTS)[number] | undefined\n): 'primary' | 'brand' | 'negative' | 'warning' | 'caution' | 'positive' | `var(--${string})` {\n if (!isTableGroupIntent(intent)) return 'primary';\n if (intent === 'neutral') return 'var(--color-foreground-bold-neutral)';\n return intent;\n}\n\n/** Formats the optional table result footer. Returns null when either count is missing. */\nexport function formatTableResultSummary(\n displayed: number | null | undefined,\n total: number | null | undefined,\n label = 'Displaying {displayed} of {total}',\n locale?: string\n): string | null {\n if (!Number.isFinite(displayed) || !Number.isFinite(total)) return null;\n const normalizedTotal = Math.max(0, Math.trunc(total as number));\n const normalizedDisplayed = Math.min(\n normalizedTotal,\n Math.max(0, Math.trunc(displayed as number))\n );\n const formatter = new Intl.NumberFormat(locale);\n return label\n .replace('{displayed}', formatter.format(normalizedDisplayed))\n .replace('{total}', formatter.format(normalizedTotal));\n}\n\n/** Formats the virtual-mode total-only result footer. */\nexport function formatTableTotalSummary(\n total: number | null | undefined,\n label = '{total} items',\n locale?: string\n): string | null {\n if (!Number.isFinite(total)) return null;\n const normalizedTotal = Math.max(0, Math.trunc(total as number));\n return label.replace('{total}', new Intl.NumberFormat(locale).format(normalizedTotal));\n}\n\n/**\n * True when a footer slot belongs to the table itself: a light-DOM child of the\n * host, or a top-level node Stencil relocated into this table's footer. A\n * slotted ancestor marks a nested component boundary, so dialog action slots\n * inside footer-leading/footer-trailing content cannot replace the table copy.\n */\nexport function isOwnedTableFooterSlot(node: Element, host: Element): boolean {\n if (node.parentElement === host) return true;\n let insideTableFooter = false;\n for (\n let ancestor = node.parentElement;\n ancestor && ancestor !== host;\n ancestor = ancestor.parentElement\n ) {\n if (ancestor.hasAttribute('slot')) return false;\n if (ancestor.classList.contains('ds-table__footer')) insideTableFooter = true;\n }\n return insideTableFooter;\n}\n\nexport function hasOwnedTableFooterSlot(host: Element, slotName: string): boolean {\n return [...host.querySelectorAll(`[slot=\"${slotName}\"]`)].some(node =>\n isOwnedTableFooterSlot(node, host)\n );\n}\n\nexport function tableModelIssues(\n columns: TableColumn[],\n rows: TableRow[],\n groups: DataGroup[],\n grouped: boolean\n): string[] {\n const issues: string[] = [];\n const columnIds = new Set<string>();\n for (const column of columns) {\n if (!column.id.trim()) issues.push('Every column requires a non-empty id.');\n else if (columnIds.has(column.id)) issues.push(`Duplicate column id: ${column.id}`);\n if (!column.label.trim() && !column.accessibleLabel?.trim()) {\n issues.push(\n `Column ${column.id || '(missing id)'} requires a visible label or accessibleLabel.`\n );\n }\n columnIds.add(column.id);\n }\n\n const groupIds = new Set<string>();\n if (grouped) {\n for (const group of groups) {\n if (!group.id.trim()) issues.push('Every group requires a non-empty id.');\n else if (groupIds.has(group.id)) issues.push(`Duplicate group id: ${group.id}`);\n groupIds.add(group.id);\n if (group.totalCount != null && group.totalCount < group.rows.length) {\n issues.push(`Group ${group.id} totalCount is smaller than its loaded row count.`);\n }\n if (group.intent != null && !isTableGroupIntent(group.intent)) {\n issues.push(`Group ${group.id} has an unsupported intent.`);\n }\n const accessoryCount = (group.accessories ?? []).filter(\n item => typeof item?.text === 'string' && item.text.trim()\n ).length;\n if (accessoryCount > TABLE_GROUP_ACCESSORY_LIMIT) {\n issues.push(`Group ${group.id} has more than ${TABLE_GROUP_ACCESSORY_LIMIT} accessories.`);\n }\n if (group.hero != null && group.hero.kind !== 'score') {\n issues.push(`Group ${group.id} has an unsupported hero.`);\n }\n }\n }\n\n const rowIds = new Set<string>();\n for (const row of tableRows(rows, groups, grouped)) {\n if (!row.id.trim()) issues.push('Every row requires a non-empty id.');\n else if (rowIds.has(row.id)) issues.push(`Duplicate row id: ${row.id}`);\n rowIds.add(row.id);\n }\n\n return [...new Set(issues)];\n}\n",
1490
1490
  "type": "registry:ui"
1491
1491
  },
1492
1492
  {
@@ -1501,12 +1501,12 @@
1501
1501
  },
1502
1502
  {
1503
1503
  "path": "src/wc/components/Table/table-render-model.ts",
1504
- "content": "import {\n deriveTableSelectionState,\n isTableGroupIntent,\n resolvedTableGroupCount,\n tableCollapseAllHost,\n tableColumnSize,\n tableElasticSpacerIndex,\n tableExplicitMinWidth,\n tableGroupAccessories,\n tableGroupHero,\n tableGroupIntentClass,\n tableGroupLabelColor,\n tableRows,\n type TableCollapseAllHost,\n type TableSelectionState,\n} from './table-model';\nimport type {\n TableColumn,\n TableGroup,\n TableGroupAccessory,\n TableGroupHero,\n TableGroupIntent,\n TableRow,\n TableSelectionMode,\n} from './table-types';\n\nexport type TableGroupCountPresentation = 'loaded-progress' | 'total';\n\nexport interface TableGroupRenderModel {\n group: TableGroup;\n count: number;\n loadedCount: number;\n visibleCountText: string;\n countLabel: string;\n intent: TableGroupIntent | undefined;\n intentClass: string | undefined;\n labelColor: ReturnType<typeof tableGroupLabelColor>;\n collapsed: boolean;\n accessories: TableGroupAccessory[];\n hero: TableGroupHero | undefined;\n selection: TableSelectionState | null;\n}\n\nexport interface TableRenderModel {\n grouped: boolean;\n selectable: boolean;\n loadedRows: TableRow[];\n hasData: boolean;\n selectedRowIds: ReadonlySet<string>;\n selection: TableSelectionState;\n collapsedGroupIds: ReadonlySet<string>;\n groups: TableGroupRenderModel[];\n totalColumns: number;\n allGroupsCollapsed: boolean;\n showCollapseAll: boolean;\n collapseAllHost: TableCollapseAllHost | undefined;\n elasticSpacerIndex: number | undefined;\n tableStyle: Record<string, string> | undefined;\n}\n\nexport interface TableRenderModelInput {\n columns: TableColumn[];\n rows: TableRow[];\n groups: TableGroup[];\n grouped: boolean;\n selectionMode: TableSelectionMode;\n selectedRowIds: string[];\n collapsedGroupIds: string[];\n groupCountPresentation?: TableGroupCountPresentation;\n}\n\n/** Create one immutable snapshot of all derived state consumed during a render. */\nexport function createTableRenderModel(input: TableRenderModelInput): TableRenderModel {\n const selectable = input.selectionMode === 'multiple';\n const loadedRows = tableRows(input.rows, input.groups, input.grouped);\n const selectedRowIds = new Set(input.selectedRowIds);\n const collapsedGroupIds = new Set(input.collapsedGroupIds);\n const allGroupsCollapsed =\n input.grouped &&\n input.groups.length > 0 &&\n input.groups.every(group => collapsedGroupIds.has(group.id));\n const showCollapseAll = input.grouped && input.groups.length > 0 && !allGroupsCollapsed;\n const explicitMinWidth = tableExplicitMinWidth(input.columns);\n const elasticSpacerIndex = tableElasticSpacerIndex(input.columns);\n const columnTracks = input.columns.map(column => tableColumnSize(column) ?? 'minmax(0, 1fr)');\n if (elasticSpacerIndex != null) columnTracks.splice(elasticSpacerIndex, 0, 'minmax(0, 1fr)');\n const gridTracks = [\n ...(selectable ? ['var(--_table-selection-column-inline-size)'] : []),\n ...columnTracks,\n ].join(' ');\n const tableStyle: Record<string, string> = {\n '--_table-grid-template-columns': gridTracks || 'minmax(0, 1fr)',\n };\n if (explicitMinWidth != null) {\n tableStyle['--ds-table-explicit-min-inline-size'] = explicitMinWidth;\n }\n\n return {\n grouped: input.grouped,\n selectable,\n loadedRows,\n hasData: loadedRows.length > 0,\n selectedRowIds,\n selection: deriveTableSelectionState(loadedRows, input.selectedRowIds),\n collapsedGroupIds,\n groups: input.groups.map(group => {\n const loadedCount = group.rows.length;\n const collapsed = collapsedGroupIds.has(group.id);\n const count = resolvedTableGroupCount(group);\n const showTotalOnly =\n input.groupCountPresentation === 'total' || (collapsed && loadedCount === 0);\n const totalLabel = group.countLabel ?? `${count} ${count === 1 ? 'item' : 'items'}`;\n const accessories = tableGroupAccessories(group);\n const hero = tableGroupHero(group);\n const intent = hero || !isTableGroupIntent(group.intent) ? undefined : group.intent;\n return {\n group,\n count,\n loadedCount,\n visibleCountText: showTotalOnly ? String(count) : `${loadedCount} of ${count}`,\n countLabel: showTotalOnly ? totalLabel : `${loadedCount} of ${totalLabel} loaded`,\n intent,\n intentClass: tableGroupIntentClass(intent),\n labelColor: tableGroupLabelColor(intent),\n collapsed,\n accessories,\n hero,\n selection: selectable ? deriveTableSelectionState(group.rows, input.selectedRowIds) : null,\n };\n }),\n totalColumns:\n input.columns.length + (selectable ? 1 : 0) + (elasticSpacerIndex == null ? 0 : 1),\n allGroupsCollapsed,\n showCollapseAll,\n collapseAllHost: showCollapseAll ? tableCollapseAllHost(input.columns) : undefined,\n elasticSpacerIndex,\n tableStyle,\n };\n}\n",
1504
+ "content": "import {\n deriveTableSelectionState,\n isTableGroupIntent,\n resolvedTableGroupCount,\n tableCollapseAllHost,\n tableColumnSize,\n tableElasticSpacerIndex,\n tableExplicitMinWidth,\n tableGroupAccessories,\n tableGroupHero,\n tableGroupIntentClass,\n tableGroupLabelColor,\n tableRows,\n type TableCollapseAllHost,\n type TableSelectionState,\n} from './table-model';\nimport type {\n TableColumn,\n DataGroup,\n TableGroupAccessory,\n TableGroupHero,\n TableGroupIntent,\n TableRow,\n TableSelectionMode,\n} from './table-types';\n\nexport type TableGroupCountPresentation = 'loaded-progress' | 'total';\n\nexport interface TableGroupRenderModel {\n group: DataGroup;\n count: number;\n loadedCount: number;\n visibleCountText: string;\n countLabel: string;\n intent: TableGroupIntent | undefined;\n intentClass: string | undefined;\n labelColor: ReturnType<typeof tableGroupLabelColor>;\n collapsed: boolean;\n accessories: TableGroupAccessory[];\n hero: TableGroupHero | undefined;\n selection: TableSelectionState | null;\n}\n\nexport interface TableRenderModel {\n grouped: boolean;\n selectable: boolean;\n loadedRows: TableRow[];\n hasData: boolean;\n selectedRowIds: ReadonlySet<string>;\n selection: TableSelectionState;\n collapsedGroupIds: ReadonlySet<string>;\n groups: TableGroupRenderModel[];\n totalColumns: number;\n allGroupsCollapsed: boolean;\n showCollapseAll: boolean;\n collapseAllHost: TableCollapseAllHost | undefined;\n elasticSpacerIndex: number | undefined;\n tableStyle: Record<string, string> | undefined;\n}\n\nexport interface TableRenderModelInput {\n columns: TableColumn[];\n rows: TableRow[];\n groups: DataGroup[];\n grouped: boolean;\n selectionMode: TableSelectionMode;\n selectedRowIds: string[];\n collapsedGroupIds: string[];\n groupCountPresentation?: TableGroupCountPresentation;\n}\n\n/** Create one immutable snapshot of all derived state consumed during a render. */\nexport function createTableRenderModel(input: TableRenderModelInput): TableRenderModel {\n const selectable = input.selectionMode === 'multiple';\n const loadedRows = tableRows(input.rows, input.groups, input.grouped);\n const selectedRowIds = new Set(input.selectedRowIds);\n const collapsedGroupIds = new Set(input.collapsedGroupIds);\n const allGroupsCollapsed =\n input.grouped &&\n input.groups.length > 0 &&\n input.groups.every(group => collapsedGroupIds.has(group.id));\n const showCollapseAll = input.grouped && input.groups.length > 0 && !allGroupsCollapsed;\n const explicitMinWidth = tableExplicitMinWidth(input.columns);\n const elasticSpacerIndex = tableElasticSpacerIndex(input.columns);\n const columnTracks = input.columns.map(column => tableColumnSize(column) ?? 'minmax(0, 1fr)');\n if (elasticSpacerIndex != null) columnTracks.splice(elasticSpacerIndex, 0, 'minmax(0, 1fr)');\n const gridTracks = [\n ...(selectable ? ['var(--_table-selection-column-inline-size)'] : []),\n ...columnTracks,\n ].join(' ');\n const tableStyle: Record<string, string> = {\n '--_table-grid-template-columns': gridTracks || 'minmax(0, 1fr)',\n };\n if (explicitMinWidth != null) {\n tableStyle['--ds-table-explicit-min-inline-size'] = explicitMinWidth;\n }\n\n return {\n grouped: input.grouped,\n selectable,\n loadedRows,\n hasData: loadedRows.length > 0,\n selectedRowIds,\n selection: deriveTableSelectionState(loadedRows, input.selectedRowIds),\n collapsedGroupIds,\n groups: input.groups.map(group => {\n const loadedCount = group.rows.length;\n const collapsed = collapsedGroupIds.has(group.id);\n const count = resolvedTableGroupCount(group);\n const showTotalOnly =\n input.groupCountPresentation === 'total' || (collapsed && loadedCount === 0);\n const totalLabel = group.countLabel ?? `${count} ${count === 1 ? 'item' : 'items'}`;\n const accessories = tableGroupAccessories(group);\n const hero = tableGroupHero(group);\n const intent = hero || !isTableGroupIntent(group.intent) ? undefined : group.intent;\n return {\n group,\n count,\n loadedCount,\n visibleCountText: showTotalOnly ? String(count) : `${loadedCount} of ${count}`,\n countLabel: showTotalOnly ? totalLabel : `${loadedCount} of ${totalLabel} loaded`,\n intent,\n intentClass: tableGroupIntentClass(intent),\n labelColor: tableGroupLabelColor(intent),\n collapsed,\n accessories,\n hero,\n selection: selectable ? deriveTableSelectionState(group.rows, input.selectedRowIds) : null,\n };\n }),\n totalColumns:\n input.columns.length + (selectable ? 1 : 0) + (elasticSpacerIndex == null ? 0 : 1),\n allGroupsCollapsed,\n showCollapseAll,\n collapseAllHost: showCollapseAll ? tableCollapseAllHost(input.columns) : undefined,\n elasticSpacerIndex,\n tableStyle,\n };\n}\n",
1505
1505
  "type": "registry:ui"
1506
1506
  },
1507
1507
  {
1508
1508
  "path": "src/wc/components/Table/table-row-view.tsx",
1509
- "content": "import { h } from '@stencil/core';\nimport {\n resolveTableCellPresentation,\n tableCellTrackStackBlockSize,\n tableCellTextOverflowProps,\n type TableCellPresentation,\n} from './table-cell-model';\nimport {\n isRenderableTableActionMenu,\n isTableCellActionMenu,\n tableActionTriggerId,\n} from './table-action-menu';\nimport { tableRowSelectionLabel } from './table-model';\nimport type { TableRenderModel } from './table-render-model';\nimport { TABLE_NO_HIGHLIGHT_MATCHER, type TableHighlightMatcher } from './table-highlight';\nimport type { TableCellActionDetail, TableCellTextRun, TableColumn, TableRow } from './table-types';\nimport { resolveSafeUrl } from '../../utils/safe-url';\n\nexport interface TableRowViewOptions {\n row: TableRow;\n model: TableRenderModel;\n visibleColumns: TableColumn[];\n emptyCellLabel: string;\n actionMenuElementId: string;\n actionMenu: { rowId: string; columnId: string } | null;\n actionMenuSurface?: { rowId: string; columnId: string } | null;\n highlightMatcher: TableHighlightMatcher;\n highlightFieldIds: string[];\n surfaceOpenRowId?: string;\n ariaRowIndex?: number;\n variableVirtualSize?: boolean;\n intrinsicBlockSize?: number;\n rowKey?: string;\n renderSelectionControl: (\n label: string,\n checked: boolean,\n indeterminate: boolean,\n disabled: boolean,\n onActivate: () => void\n ) => unknown;\n renderStickyEdge: (sticky: TableColumn['sticky']) => unknown;\n onRowActivate: (row: TableRow, event: Event) => void;\n onRowKeyDown: (row: TableRow, event: KeyboardEvent) => void;\n onRowSelection: (row: TableRow) => void;\n onCellAction: (detail: TableCellActionDetail) => void;\n onActionMenuToggle: (row: TableRow, column: TableColumn, event: MouseEvent) => void;\n}\n\n/** Render one semantic body row and the complete declarative table-cell vocabulary. */\nexport function renderTableRow({\n row,\n model,\n visibleColumns,\n emptyCellLabel,\n actionMenuElementId,\n actionMenu,\n actionMenuSurface,\n highlightMatcher,\n highlightFieldIds,\n surfaceOpenRowId,\n ariaRowIndex,\n variableVirtualSize = false,\n intrinsicBlockSize,\n rowKey = row.id,\n renderSelectionControl,\n renderStickyEdge,\n onRowActivate,\n onRowKeyDown,\n onRowSelection,\n onCellAction,\n onActionMenuToggle,\n}: TableRowViewOptions) {\n const selected = model.selectedRowIds.has(row.id);\n const surfaceOpen = row.id === surfaceOpenRowId && !!row.interactive && !row.disabled;\n const rowSelectable = row.selectable !== false && !row.disabled;\n const rowInteractive =\n !row.disabled &&\n (model.selectable && model.selectedRowIds.size > 0 ? rowSelectable : !!row.interactive);\n const beforeSpacer =\n model.elasticSpacerIndex == null\n ? visibleColumns\n : visibleColumns.slice(0, model.elasticSpacerIndex);\n const afterSpacer =\n model.elasticSpacerIndex == null ? [] : visibleColumns.slice(model.elasticSpacerIndex);\n const renderColumn = (column: TableColumn) =>\n renderTableCell({\n row,\n column,\n selected,\n surfaceOpen,\n emptyCellLabel,\n actionMenuElementId,\n actionMenu,\n actionMenuSurface,\n highlightMatcher,\n highlightFieldIds,\n renderStickyEdge,\n onCellAction,\n onActionMenuToggle,\n });\n\n return (\n <tr\n key={rowKey}\n class={{\n 'ds-table__row': true,\n 'ds-table__row--selected': selected,\n 'ds-table__row--disabled': !!row.disabled,\n 'ds-table__row--interactive': rowInteractive,\n 'ds-focus-ring': rowInteractive,\n }}\n data-row-id={row.id}\n data-virtual-id={ariaRowIndex != null ? `row:${row.id}` : undefined}\n data-virtual-pool-key={ariaRowIndex != null ? rowKey : undefined}\n data-virtual-measure={ariaRowIndex != null && variableVirtualSize ? 'true' : undefined}\n data-selected={selected ? 'true' : undefined}\n data-surface-open={surfaceOpen ? 'true' : undefined}\n style={\n intrinsicBlockSize != null\n ? { '--_table-row-intrinsic-block-size': `${intrinsicBlockSize}px` }\n : undefined\n }\n aria-rowindex={ariaRowIndex}\n aria-disabled={row.disabled ? 'true' : undefined}\n tabIndex={rowInteractive ? 0 : undefined}\n onClick={event => onRowActivate(row, event)}\n onKeyDown={event => onRowKeyDown(row, event)}\n >\n {model.selectable && (\n <td\n class={{\n 'ds-table__cell': true,\n 'ds-table__selection-cell': true,\n 'ds-table__cell--sticky-start': true,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--grouped': true,\n 'ds-interaction-fill--selected': selected,\n 'ds-interaction-fill--surface-open': surfaceOpen,\n }}\n >\n {renderSelectionControl(\n `${selected ? 'Deselect' : 'Select'} ${tableRowSelectionLabel(row, visibleColumns)}`,\n selected,\n false,\n !rowSelectable,\n () => onRowSelection(row)\n )}\n {renderStickyEdge('start')}\n </td>\n )}\n {beforeSpacer.map(renderColumn)}\n {model.elasticSpacerIndex != null && (\n <td\n class={{\n 'ds-table__cell': true,\n 'ds-table__elastic-spacer-cell': true,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--grouped': true,\n 'ds-interaction-fill--selected': selected,\n 'ds-interaction-fill--surface-open': surfaceOpen,\n }}\n aria-hidden=\"true\"\n role=\"presentation\"\n data-elastic-spacer=\"true\"\n />\n )}\n {afterSpacer.map(renderColumn)}\n </tr>\n );\n}\n\ninterface TableCellViewOptions {\n row: TableRow;\n column: TableColumn;\n selected: boolean;\n surfaceOpen: boolean;\n emptyCellLabel: string;\n actionMenuElementId: string;\n actionMenu: { rowId: string; columnId: string } | null;\n actionMenuSurface?: { rowId: string; columnId: string } | null;\n highlightMatcher: TableHighlightMatcher;\n highlightFieldIds: string[];\n renderStickyEdge: (sticky: TableColumn['sticky']) => unknown;\n onCellAction: (detail: TableCellActionDetail) => void;\n onActionMenuToggle: (row: TableRow, column: TableColumn, event: MouseEvent) => void;\n}\n\nfunction renderTableCell(options: TableCellViewOptions) {\n const { row, column, selected, surfaceOpen, renderStickyEdge } = options;\n const align = column.align ?? 'start';\n const cell = resolveTableCellPresentation(row.cells[column.id], column);\n const tagCell = cell.kind === 'tag';\n const tagsCell = cell.kind === 'tags';\n const iconCell = cell.kind === 'icon';\n const iconTextCell = cell.kind === 'icon-text';\n const scoreTextCell = cell.kind === 'score-text';\n const imageCell = cell.kind === 'image';\n const actionCell = cell.kind === 'action';\n const actionMenuCell = actionCell && isRenderableTableActionMenu(cell.value);\n const textCell = cell.kind === 'text';\n const primaryTextCell = textCell && cell.primaryText;\n const singleTextCell = textCell && cell.singleLine;\n const emptyCell = cell.kind === 'empty';\n const blankCell = cell.kind === 'blank';\n const tagVariant = tagCell ? cell.variant : undefined;\n const tagsVariant = tagsCell ? cell.variant : undefined;\n const textVariant = textCell ? cell.variant : undefined;\n const imageVariant = imageCell ? cell.variant : undefined;\n const iconTextVariant = iconTextCell ? cell.variant : undefined;\n const scoreTextVariant = scoreTextCell ? cell.variant : undefined;\n const wraps = (textCell || iconTextCell || scoreTextCell) && cell.wraps;\n\n return (\n <td\n key={`${row.id}:${column.id}`}\n class={{\n 'ds-table__cell': true,\n [`ds-table__cell--align-${align}`]: true,\n 'ds-table__cell--tag': tagCell,\n [`ds-table__cell--tag-${tagVariant}`]: tagCell,\n 'ds-table__cell--tags': tagsCell,\n [`ds-table__cell--tags-${tagsVariant}`]: tagsCell,\n 'ds-table__cell--icon': iconCell,\n 'ds-table__cell--icon-text': iconTextCell,\n [`ds-table__cell--icon-text-${iconTextVariant}`]: iconTextCell,\n 'ds-table__cell--icon-text-wrap': iconTextCell && wraps,\n 'ds-table__cell--score-text': scoreTextCell,\n [`ds-table__cell--score-text-${scoreTextVariant}`]: scoreTextCell,\n 'ds-table__cell--score-text-wrap': scoreTextCell && wraps,\n 'ds-table__cell--image': imageCell,\n [`ds-table__cell--image-${imageVariant}`]: imageCell,\n 'ds-table__cell--action': actionCell,\n 'ds-table__cell--action-menu': actionMenuCell,\n 'ds-table__cell--primary-text': primaryTextCell,\n 'ds-table__cell--text-single': singleTextCell,\n 'ds-table__cell--text-multi': textCell && !singleTextCell && textVariant !== 'triple',\n 'ds-table__cell--text-triple': textVariant === 'triple',\n 'ds-table__cell--text-wrap': textCell && wraps,\n 'ds-table__cell--empty': emptyCell,\n 'ds-table__cell--blank': blankCell,\n 'ds-table__cell--sticky-start': column.sticky === 'start',\n 'ds-table__cell--sticky-end': column.sticky === 'end',\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--grouped': true,\n 'ds-interaction-fill--selected': selected,\n 'ds-interaction-fill--surface-open': surfaceOpen,\n }}\n data-column-id={column.id}\n data-cell-type={cell.cellType}\n data-cell-variant={\n tagVariant ??\n tagsVariant ??\n textVariant ??\n imageVariant ??\n iconTextVariant ??\n scoreTextVariant\n }\n data-cell-tracks={tagsCell ? cell.tracks : undefined}\n >\n <span class=\"ds-table__cell-content ds-interaction-fill__content\">\n {renderTableCellValue(cell, options)}\n </span>\n {renderStickyEdge(column.sticky)}\n </td>\n );\n}\n\nfunction renderTableCellValue(cell: TableCellPresentation, options: TableCellViewOptions) {\n const {\n row,\n column,\n emptyCellLabel,\n actionMenuElementId,\n actionMenu,\n actionMenuSurface,\n onCellAction,\n onActionMenuToggle,\n } = options;\n if (cell.kind === 'blank') return null;\n\n if (cell.kind === 'empty') {\n return (\n <ds-text\n class=\"ds-table__cell-track ds-table__cell-track--text\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"tertiary\"\n >\n <span aria-hidden=\"true\">—</span>\n <span class=\"ds-visually-hidden\">{emptyCellLabel}</span>\n </ds-text>\n );\n }\n\n if (cell.kind === 'icon') {\n const value = cell.value;\n return (\n <ds-icon name={value.icon} size=\"md\" color={value.color ?? 'secondary'} label={value.label} />\n );\n }\n\n if (cell.kind === 'icon-text') {\n return (\n <span class=\"ds-table__cell-icon-text\">\n <span class=\"ds-table__cell-icon-text-icon\">\n <ds-icon\n name={cell.icon}\n size=\"md\"\n color={cell.iconColor ?? 'secondary'}\n label={cell.iconLabel}\n />\n </span>\n {renderTextCopy(cell, options)}\n </span>\n );\n }\n\n if (cell.kind === 'score-text') {\n return (\n <span class=\"ds-table__cell-score-text\">\n {renderTextCopy(cell, options)}\n <span class=\"ds-table__cell-score-text-score\">\n <ds-score\n size=\"sm\"\n value={cell.score}\n level={cell.scoreLevel}\n label={cell.scoreLabel}\n isLoading={cell.scoreLoading}\n />\n </span>\n </span>\n );\n }\n\n if (cell.kind === 'image') {\n const value = cell.value;\n return (\n <span class=\"ds-table__cell-image\">\n {value.src ? (\n <img\n class=\"ds-table__cell-image-content\"\n src={value.src}\n alt={value.alt}\n loading=\"lazy\"\n />\n ) : (\n <span class=\"ds-table__cell-image-placeholder\" role=\"img\" aria-label={value.alt} />\n )}\n </span>\n );\n }\n\n if (cell.kind === 'action') {\n const value = cell.value;\n const menu = isRenderableTableActionMenu(value);\n const triggerId = tableActionTriggerId(actionMenuElementId, row.id, column.id);\n const expanded = menu && actionMenu?.rowId === row.id && actionMenu?.columnId === column.id;\n return (\n <ds-button-unfilled\n id={menu ? triggerId : undefined}\n variant={menu ? 'icon' : (value.variant ?? 'label')}\n size=\"md\"\n isInset={true}\n insetDepth=\"double\"\n label={value.label ?? ''}\n icon={value.icon ?? (menu ? 'Ellipses' : '')}\n aria-label={value.ariaLabel ?? null}\n hasBorder={value.hasBorder ?? false}\n isInactive={value.isInactive ?? false}\n isLoading={value.isLoading ?? false}\n hasMenu={menu}\n expanded={menu ? expanded : undefined}\n surfaceOpen={\n menu && actionMenuSurface?.rowId === row.id && actionMenuSurface?.columnId === column.id\n }\n controls={menu ? actionMenuElementId : undefined}\n onDsClick={event => {\n event.stopPropagation();\n if (menu) {\n onActionMenuToggle(row, column, event.detail);\n return;\n }\n if (isTableCellActionMenu(value)) return;\n onCellAction({ actionId: value.actionId, rowId: row.id, columnId: column.id });\n }}\n />\n );\n }\n\n if (cell.kind === 'tag') {\n const value = cell.value;\n const variant = cell.variant;\n const tag = (\n <ds-tag\n label={value.label}\n intent={value.intent ?? 'neutral'}\n contrast={value.contrast ?? 'faint'}\n size={variant === 'text-with-tag' ? 'sm' : 'md'}\n icon={value.icon ?? ''}\n rounded={value.rounded ?? false}\n isInset\n insetDepth={variant === 'text-with-tag' ? 'single' : 'double'}\n />\n );\n if (variant === 'tag-only') return tag;\n return (\n <span class={`ds-table__cell-tag-stack ds-table__cell-tag-stack--${variant}`}>\n {variant === 'tag-with-text' && tag}\n <ds-text\n class=\"ds-table__cell-tag-text ds-table__cell-track\"\n as=\"span\"\n variant={variant === 'tag-with-text' ? 'text-body-small' : 'text-body-medium'}\n color=\"secondary\"\n lineTruncation={1}\n data-table-truncate=\"\"\n >\n {renderHighlightedText(\n value.text,\n tableCellFieldMatcher(options, variant === 'tag-with-text' ? 1 : 0)\n )}\n </ds-text>\n {variant === 'text-with-tag' && <span class=\"ds-table__cell-tag-control-track\">{tag}</span>}\n </span>\n );\n }\n\n if (cell.kind === 'tags') {\n if (cell.value.variant === 'inline') {\n return (\n <span class=\"ds-table__cell-tag-stack ds-table__cell-tag-stack--tag-with-text ds-table__cell-inline-tags-stack\">\n <span class=\"ds-table__cell-inline-tags\">\n {cell.value.items.map((item, index) => [\n index > 0 && (\n <ds-text\n class=\"ds-table__cell-tag-separator\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"quaternary\"\n aria-hidden=\"true\"\n >\n /\n </ds-text>\n ),\n <ds-tag\n key={`${item.label}-${index}`}\n label={item.label}\n intent={item.intent ?? 'neutral'}\n contrast={item.contrast ?? 'faint'}\n size=\"md\"\n icon={item.icon ?? ''}\n rounded={item.rounded ?? false}\n isInset\n insetDepth=\"double\"\n />,\n ])}\n </span>\n {cell.tracks === 2 && (\n <ds-text\n class=\"ds-table__cell-tag-text ds-table__cell-track\"\n as=\"span\"\n variant=\"text-body-small\"\n color=\"secondary\"\n lineTruncation={1}\n data-table-truncate=\"\"\n >\n {renderHighlightedText(cell.value.text, tableCellFieldMatcher(options, 1))}\n </ds-text>\n )}\n </span>\n );\n }\n return (\n <span\n class=\"ds-table__cell-tags\"\n style={{ '--_table-cell-tags-min-block-size': tableCellTrackStackBlockSize(cell.tracks) }}\n >\n {cell.value.items.map((item, index) => (\n <ds-tag\n key={`${item.label}-${index}`}\n label={item.label}\n intent={item.intent ?? 'neutral'}\n contrast={item.contrast ?? 'faint'}\n size=\"sm\"\n icon={item.icon ?? ''}\n rounded={item.rounded ?? false}\n isInset\n insetDepth=\"single\"\n />\n ))}\n </span>\n );\n }\n\n if (cell.kind !== 'text') return null;\n return renderTextCopy(cell, options);\n}\n\nfunction renderTextCopy(\n cell: Extract<TableCellPresentation, { kind: 'text' | 'icon-text' | 'score-text' }>,\n options: TableCellViewOptions\n) {\n const text = cell.value;\n const overflow = tableCellTextOverflowProps(cell.lineClamp);\n const primaryText = cell.kind === 'text' && cell.primaryText;\n const href = resolveSafeUrl(text.href);\n const primary = (\n <ds-text\n class=\"ds-table__cell-primary ds-table__cell-track ds-table__cell-track--text\"\n as=\"span\"\n variant=\"text-body-medium\"\n color={href ? 'inherit' : 'primary'}\n lineTruncation={overflow.lineTruncation}\n wrap={overflow.wrap}\n fontFeature={text.fontFeature ?? 'normal'}\n data-table-truncate={truncateAttr(cell.lineClamp)}\n >\n {renderHighlightedText(text.primary, tableCellFieldMatcher(options, 0))}\n </ds-text>\n );\n\n return (\n <span class={{ 'ds-table__cell-copy': true, 'ds-table__cell-copy--wrap': cell.wraps }}>\n {href ? (\n <a\n class=\"ds-table__cell-link ds-text-action ds-focus-ring\"\n href={href}\n target={text.target === '_blank' ? '_blank' : undefined}\n rel={text.target === '_blank' ? 'noopener noreferrer' : undefined}\n >\n {primary}\n </a>\n ) : (\n primary\n )}\n {renderTextTrack(text.secondary, {\n track: 'secondary',\n variant: primaryText ? 'text-body-medium' : 'text-body-small',\n defaultColor: primaryText ? 'primary' : 'secondary',\n wholeColor: text.secondaryColor,\n lineClamp: cell.lineClamp,\n highlightMatcherForRun: runIndex => tableCellFieldMatcher(options, 1 + runIndex),\n })}\n {renderTextTrack(text.tertiary, {\n track: 'tertiary',\n variant: 'text-body-small',\n defaultColor: 'secondary',\n wholeColor: text.tertiaryColor,\n lineClamp: cell.lineClamp,\n highlightMatcherForRun: runIndex =>\n tableCellFieldMatcher(options, 1 + (text.secondary?.length ?? 0) + runIndex),\n })}\n </span>\n );\n}\n\nfunction renderTextTrack(\n runs: TableCellTextRun[] | undefined,\n options: {\n track: 'secondary' | 'tertiary';\n variant: 'text-body-medium' | 'text-body-small';\n defaultColor: 'primary' | 'secondary';\n wholeColor?: TableCellTextRun['color'];\n lineClamp: Extract<\n TableCellPresentation,\n { kind: 'text' | 'icon-text' | 'score-text' }\n >['lineClamp'];\n highlightMatcherForRun: (runIndex: number) => TableHighlightMatcher;\n }\n) {\n if (!runs?.length) return null;\n const trackClass = `ds-table__cell-${options.track} ds-table__cell-track ds-table__cell-track--text`;\n const overflow = tableCellTextOverflowProps(options.lineClamp);\n const colorFor = (run: TableCellTextRun) =>\n run.color ?? options.wholeColor ?? options.defaultColor;\n const renderRun = (run: TableCellTextRun, index: number, className: string) => {\n const copy = (\n <ds-text\n class={className}\n as=\"span\"\n variant={options.variant}\n color={colorFor(run)}\n decoration={run.help ? 'dotted-underline' : undefined}\n lineTruncation={overflow.lineTruncation}\n wrap={overflow.wrap}\n data-table-truncate={truncateAttr(options.lineClamp)}\n >\n {renderHighlightedText(run.text, options.highlightMatcherForRun(index))}\n </ds-text>\n );\n return run.help ? (\n <ds-tooltip\n key={`${options.track}-run-${index}`}\n label={run.help}\n side=\"bottom\"\n align=\"start\"\n size=\"sm\"\n wrapLabel={true}\n >\n {copy}\n </ds-tooltip>\n ) : (\n copy\n );\n };\n if (runs.length === 1) {\n const run = runs[0];\n if (!run.help) return renderRun(run, 0, trackClass);\n return <span class={trackClass}>{renderRun(run, 0, 'ds-table__cell-run')}</span>;\n }\n\n return (\n <span class={`${trackClass} ds-table__cell-track--runs`}>\n {runs.map((run, index) => [\n index > 0 && (\n <ds-text\n key={`${options.track}-sep-${index}`}\n class=\"ds-table__cell-run-separator\"\n as=\"span\"\n variant={options.variant}\n color=\"secondary\"\n aria-hidden=\"true\"\n >\n ·\n </ds-text>\n ),\n renderRun(run, index, 'ds-table__cell-run'),\n ])}\n </span>\n );\n}\n\nfunction renderHighlightedText(value: string | number | undefined, matcher: TableHighlightMatcher) {\n if (value === undefined) return null;\n return matcher(value).map((segment, index) =>\n segment.match ? (\n <mark class=\"ds-table__match\" key={`match-${index}`}>\n {segment.text}\n </mark>\n ) : (\n <span key={`text-${index}`}>{segment.text}</span>\n )\n );\n}\n\nfunction tableCellFieldMatcher(\n options: TableCellViewOptions,\n fieldIndex: number\n): TableHighlightMatcher {\n const selected = options.highlightFieldIds;\n if (selected.length === 0 || selected.includes(options.column.id)) {\n return options.highlightMatcher;\n }\n const fieldId = options.column.headerSegments?.[fieldIndex]?.sortKey;\n return fieldId && selected.includes(fieldId)\n ? options.highlightMatcher\n : TABLE_NO_HIGHLIGHT_MATCHER;\n}\n\nfunction truncateAttr(\n lineClamp: Extract<\n TableCellPresentation,\n { kind: 'text' | 'icon-text' | 'score-text' }\n >['lineClamp']\n) {\n return lineClamp === 'none' ? undefined : '';\n}\n",
1509
+ "content": "import { h } from '@stencil/core';\nimport {\n resolveTableCellPresentation,\n tableCellTrackStackBlockSize,\n tableCellTextOverflowProps,\n type TableCellPresentation,\n} from './table-cell-model';\nimport {\n isRenderableTableActionMenu,\n isTableCellActionMenu,\n tableActionTriggerId,\n} from './table-action-menu';\nimport { tableRowSelectionLabel } from './table-model';\nimport type { TableRenderModel } from './table-render-model';\nimport { TABLE_NO_HIGHLIGHT_MATCHER, type TableHighlightMatcher } from './table-highlight';\nimport type { TableCellActionDetail, TableCellTextRun, TableColumn, TableRow } from './table-types';\nimport { resolveSafeUrl } from '../../utils/safe-url';\n\nexport interface TableRowViewOptions {\n row: TableRow;\n model: TableRenderModel;\n visibleColumns: TableColumn[];\n emptyCellLabel: string;\n actionMenuElementId: string;\n actionMenu: { rowId: string; columnId: string } | null;\n actionMenuSurface?: { rowId: string; columnId: string } | null;\n highlightMatcher: TableHighlightMatcher;\n highlightFieldIds: string[];\n surfaceOpenRowId?: string;\n ariaRowIndex?: number;\n variableVirtualSize?: boolean;\n intrinsicBlockSize?: number;\n rowKey?: string;\n renderSelectionControl: (\n label: string,\n checked: boolean,\n indeterminate: boolean,\n disabled: boolean,\n onActivate: () => void\n ) => unknown;\n renderStickyEdge: (sticky: TableColumn['sticky']) => unknown;\n onRowActivate: (row: TableRow, event: Event) => void;\n onRowKeyDown: (row: TableRow, event: KeyboardEvent) => void;\n onRowSelection: (row: TableRow) => void;\n onCellAction: (detail: TableCellActionDetail) => void;\n onActionMenuToggle: (row: TableRow, column: TableColumn, event: MouseEvent) => void;\n}\n\n/** Render one semantic body row and the complete declarative table-cell vocabulary. */\nexport function renderTableRow({\n row,\n model,\n visibleColumns,\n emptyCellLabel,\n actionMenuElementId,\n actionMenu,\n actionMenuSurface,\n highlightMatcher,\n highlightFieldIds,\n surfaceOpenRowId,\n ariaRowIndex,\n variableVirtualSize = false,\n intrinsicBlockSize,\n rowKey = row.id,\n renderSelectionControl,\n renderStickyEdge,\n onRowActivate,\n onRowKeyDown,\n onRowSelection,\n onCellAction,\n onActionMenuToggle,\n}: TableRowViewOptions) {\n const selected = model.selectedRowIds.has(row.id);\n const surfaceOpen = row.id === surfaceOpenRowId && !!row.interactive && !row.disabled;\n const rowSelectable = row.selectable !== false && !row.disabled;\n const rowInteractive =\n !row.disabled &&\n (model.selectable && model.selectedRowIds.size > 0 ? rowSelectable : !!row.interactive);\n const beforeSpacer =\n model.elasticSpacerIndex == null\n ? visibleColumns\n : visibleColumns.slice(0, model.elasticSpacerIndex);\n const afterSpacer =\n model.elasticSpacerIndex == null ? [] : visibleColumns.slice(model.elasticSpacerIndex);\n const renderColumn = (column: TableColumn) =>\n renderTableCell({\n row,\n column,\n selected,\n surfaceOpen,\n emptyCellLabel,\n actionMenuElementId,\n actionMenu,\n actionMenuSurface,\n highlightMatcher,\n highlightFieldIds,\n renderStickyEdge,\n onCellAction,\n onActionMenuToggle,\n });\n\n return (\n <tr\n key={rowKey}\n class={{\n 'ds-table__row': true,\n 'ds-table__row--selected': selected,\n 'ds-table__row--disabled': !!row.disabled,\n 'ds-table__row--interactive': rowInteractive,\n 'ds-focus-ring': rowInteractive,\n }}\n data-row-id={row.id}\n data-virtual-id={ariaRowIndex != null ? `row:${row.id}` : undefined}\n data-virtual-pool-key={ariaRowIndex != null ? rowKey : undefined}\n data-virtual-measure={ariaRowIndex != null && variableVirtualSize ? 'true' : undefined}\n data-selected={selected ? 'true' : undefined}\n data-surface-open={surfaceOpen ? 'true' : undefined}\n style={\n intrinsicBlockSize != null\n ? { '--_table-row-intrinsic-block-size': `${intrinsicBlockSize}px` }\n : undefined\n }\n aria-rowindex={ariaRowIndex}\n aria-disabled={row.disabled ? 'true' : undefined}\n tabIndex={rowInteractive ? 0 : undefined}\n onClick={event => onRowActivate(row, event)}\n onKeyDown={event => onRowKeyDown(row, event)}\n >\n {model.selectable && (\n <td\n class={{\n 'ds-table__cell': true,\n 'ds-table__selection-cell': true,\n 'ds-table__cell--sticky-start': true,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--grouped': true,\n 'ds-interaction-fill--selected': selected,\n 'ds-interaction-fill--surface-open': surfaceOpen,\n }}\n >\n {renderSelectionControl(\n `${selected ? 'Deselect' : 'Select'} ${tableRowSelectionLabel(row, visibleColumns)}`,\n selected,\n false,\n !rowSelectable,\n () => onRowSelection(row)\n )}\n {renderStickyEdge('start')}\n </td>\n )}\n {beforeSpacer.map(renderColumn)}\n {model.elasticSpacerIndex != null && (\n <td\n class={{\n 'ds-table__cell': true,\n 'ds-table__elastic-spacer-cell': true,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--grouped': true,\n 'ds-interaction-fill--selected': selected,\n 'ds-interaction-fill--surface-open': surfaceOpen,\n }}\n aria-hidden=\"true\"\n role=\"presentation\"\n data-elastic-spacer=\"true\"\n />\n )}\n {afterSpacer.map(renderColumn)}\n </tr>\n );\n}\n\ninterface TableCellViewOptions {\n row: TableRow;\n column: TableColumn;\n selected: boolean;\n surfaceOpen: boolean;\n emptyCellLabel: string;\n actionMenuElementId: string;\n actionMenu: { rowId: string; columnId: string } | null;\n actionMenuSurface?: { rowId: string; columnId: string } | null;\n highlightMatcher: TableHighlightMatcher;\n highlightFieldIds: string[];\n renderStickyEdge: (sticky: TableColumn['sticky']) => unknown;\n onCellAction: (detail: TableCellActionDetail) => void;\n onActionMenuToggle: (row: TableRow, column: TableColumn, event: MouseEvent) => void;\n}\n\nfunction renderTableCell(options: TableCellViewOptions) {\n const { row, column, selected, surfaceOpen, renderStickyEdge } = options;\n const align = column.align ?? 'start';\n const cell = resolveTableCellPresentation(row.cells[column.id], column);\n const tagCell = cell.kind === 'tag';\n const tagsCell = cell.kind === 'tags';\n const iconCell = cell.kind === 'icon';\n const iconTextCell = cell.kind === 'icon-text';\n const scoreTextCell = cell.kind === 'score-text';\n const imageCell = cell.kind === 'image';\n const actionCell = cell.kind === 'action';\n const actionMenuCell = actionCell && isRenderableTableActionMenu(cell.value);\n const textCell = cell.kind === 'text';\n const primaryTextCell = textCell && cell.primaryText;\n const singleTextCell = textCell && cell.singleLine;\n const emptyCell = cell.kind === 'empty';\n const blankCell = cell.kind === 'blank';\n const tagVariant = tagCell ? cell.variant : undefined;\n const tagsVariant = tagsCell ? cell.variant : undefined;\n const textVariant = textCell ? cell.variant : undefined;\n const imageVariant = imageCell ? cell.variant : undefined;\n const iconTextVariant = iconTextCell ? cell.variant : undefined;\n const scoreTextVariant = scoreTextCell ? cell.variant : undefined;\n const wraps = (textCell || iconTextCell || scoreTextCell) && cell.wraps;\n\n return (\n <td\n key={`${row.id}:${column.id}`}\n class={{\n 'ds-table__cell': true,\n [`ds-table__cell--align-${align}`]: true,\n 'ds-table__cell--tag': tagCell,\n [`ds-table__cell--tag-${tagVariant}`]: tagCell,\n 'ds-table__cell--tags': tagsCell,\n [`ds-table__cell--tags-${tagsVariant}`]: tagsCell,\n 'ds-table__cell--icon': iconCell,\n 'ds-table__cell--icon-text': iconTextCell,\n [`ds-table__cell--icon-text-${iconTextVariant}`]: iconTextCell,\n 'ds-table__cell--icon-text-wrap': iconTextCell && wraps,\n 'ds-table__cell--score-text': scoreTextCell,\n [`ds-table__cell--score-text-${scoreTextVariant}`]: scoreTextCell,\n 'ds-table__cell--score-text-wrap': scoreTextCell && wraps,\n 'ds-table__cell--image': imageCell,\n [`ds-table__cell--image-${imageVariant}`]: imageCell,\n 'ds-table__cell--action': actionCell,\n 'ds-table__cell--action-menu': actionMenuCell,\n 'ds-table__cell--primary-text': primaryTextCell,\n 'ds-table__cell--text-single': singleTextCell,\n 'ds-table__cell--text-multi': textCell && !singleTextCell && textVariant !== 'triple',\n 'ds-table__cell--text-triple': textVariant === 'triple',\n 'ds-table__cell--text-wrap': textCell && wraps,\n 'ds-table__cell--empty': emptyCell,\n 'ds-table__cell--blank': blankCell,\n 'ds-table__cell--sticky-start': column.sticky === 'start',\n 'ds-table__cell--sticky-end': column.sticky === 'end',\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--grouped': true,\n 'ds-interaction-fill--selected': selected,\n 'ds-interaction-fill--surface-open': surfaceOpen,\n }}\n data-column-id={column.id}\n data-cell-type={cell.cellType}\n data-cell-variant={\n tagVariant ??\n tagsVariant ??\n textVariant ??\n imageVariant ??\n iconTextVariant ??\n scoreTextVariant\n }\n data-cell-tracks={tagsCell ? cell.tracks : undefined}\n >\n <span class=\"ds-table__cell-content ds-interaction-fill__content\">\n {renderTableCellValue(cell, options)}\n </span>\n {renderStickyEdge(column.sticky)}\n </td>\n );\n}\n\nfunction renderTableCellValue(cell: TableCellPresentation, options: TableCellViewOptions) {\n const {\n row,\n column,\n emptyCellLabel,\n actionMenuElementId,\n actionMenu,\n actionMenuSurface,\n onCellAction,\n onActionMenuToggle,\n } = options;\n if (cell.kind === 'blank') return null;\n\n if (cell.kind === 'empty') {\n return (\n <ds-text\n class=\"ds-table__cell-track ds-table__cell-track--text\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"tertiary\"\n >\n <span aria-hidden=\"true\">—</span>\n <span class=\"ds-visually-hidden\">{emptyCellLabel}</span>\n </ds-text>\n );\n }\n\n if (cell.kind === 'icon') {\n const value = cell.value;\n return (\n <ds-icon name={value.icon} size=\"md\" color={value.color ?? 'secondary'} label={value.label} />\n );\n }\n\n if (cell.kind === 'icon-text') {\n return (\n <span class=\"ds-table__cell-icon-text\">\n <span class=\"ds-table__cell-icon-text-icon\">\n <ds-icon\n name={cell.icon}\n size=\"md\"\n color={cell.iconColor ?? 'secondary'}\n label={cell.iconLabel}\n />\n </span>\n {renderTextCopy(cell, options)}\n </span>\n );\n }\n\n if (cell.kind === 'score-text') {\n return (\n <span class=\"ds-table__cell-score-text\">\n {renderTextCopy(cell, options)}\n <span class=\"ds-table__cell-score-text-score\">\n <ds-score\n size=\"sm\"\n value={cell.score}\n level={cell.scoreLevel}\n label={cell.scoreLabel}\n isLoading={cell.scoreLoading}\n />\n </span>\n </span>\n );\n }\n\n if (cell.kind === 'image') {\n const value = cell.value;\n return (\n <span class=\"ds-table__cell-image\">\n {value.src ? (\n <img\n class=\"ds-table__cell-image-content\"\n src={value.src}\n alt={value.alt}\n loading=\"lazy\"\n />\n ) : (\n <span class=\"ds-table__cell-image-placeholder\" role=\"img\" aria-label={value.alt} />\n )}\n </span>\n );\n }\n\n if (cell.kind === 'action') {\n const value = cell.value;\n const menu = isRenderableTableActionMenu(value);\n const triggerId = tableActionTriggerId(actionMenuElementId, row.id, column.id);\n const expanded = menu && actionMenu?.rowId === row.id && actionMenu?.columnId === column.id;\n return (\n <ds-button-unfilled\n id={menu ? triggerId : undefined}\n variant={menu ? 'icon' : (value.variant ?? 'label')}\n size=\"md\"\n isInset={true}\n insetDepth=\"double\"\n label={value.label ?? ''}\n icon={value.icon ?? (menu ? 'Ellipses' : '')}\n aria-label={value.ariaLabel ?? null}\n hasBorder={value.hasBorder ?? false}\n isInactive={value.isInactive ?? false}\n isLoading={value.isLoading ?? false}\n hasMenu={menu}\n expanded={menu ? expanded : undefined}\n surfaceOpen={\n menu && actionMenuSurface?.rowId === row.id && actionMenuSurface?.columnId === column.id\n }\n controls={menu ? actionMenuElementId : undefined}\n onDsClick={event => {\n event.stopPropagation();\n if (menu) {\n onActionMenuToggle(row, column, event.detail);\n return;\n }\n if (isTableCellActionMenu(value)) return;\n onCellAction({ actionId: value.actionId, rowId: row.id, columnId: column.id });\n }}\n />\n );\n }\n\n if (cell.kind === 'tag') {\n const value = cell.value;\n const variant = cell.variant;\n const tag = (\n <ds-tag\n label={value.label}\n intent={value.intent ?? 'neutral'}\n contrast={value.contrast ?? 'faint'}\n size={variant === 'text-with-tag' ? 'sm' : 'md'}\n icon={value.icon ?? ''}\n rounded={value.rounded ?? false}\n isInset\n insetDepth={variant === 'text-with-tag' ? 'single' : 'double'}\n />\n );\n if (variant === 'tag-only') return tag;\n return (\n <span class={`ds-table__cell-tag-stack ds-table__cell-tag-stack--${variant}`}>\n {variant === 'tag-with-text' && tag}\n <ds-text\n class=\"ds-table__cell-tag-text ds-table__cell-track\"\n as=\"span\"\n variant={variant === 'tag-with-text' ? 'text-body-small' : 'text-body-medium'}\n color=\"secondary\"\n lineTruncation={1}\n data-table-truncate=\"\"\n >\n {renderHighlightedText(\n value.text,\n tableCellFieldMatcher(options, variant === 'tag-with-text' ? 1 : 0)\n )}\n </ds-text>\n {variant === 'text-with-tag' && <span class=\"ds-table__cell-tag-control-track\">{tag}</span>}\n </span>\n );\n }\n\n if (cell.kind === 'tags') {\n if (cell.value.variant === 'inline') {\n return (\n <span class=\"ds-table__cell-tag-stack ds-table__cell-tag-stack--tag-with-text ds-table__cell-inline-tags-stack\">\n <span class=\"ds-table__cell-inline-tags\">\n {cell.value.items.map((item, index) => [\n index > 0 && (\n <ds-text\n class=\"ds-table__cell-tag-separator\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"quaternary\"\n aria-hidden=\"true\"\n >\n /\n </ds-text>\n ),\n <ds-tag\n key={`${item.label}-${index}`}\n label={item.label}\n intent={item.intent ?? 'neutral'}\n contrast={item.contrast ?? 'faint'}\n size=\"md\"\n icon={item.icon ?? ''}\n rounded={item.rounded ?? false}\n isInset\n insetDepth=\"double\"\n />,\n ])}\n </span>\n {cell.tracks === 2 && (\n <ds-text\n class=\"ds-table__cell-tag-text ds-table__cell-track\"\n as=\"span\"\n variant=\"text-body-small\"\n color=\"secondary\"\n lineTruncation={1}\n data-table-truncate=\"\"\n >\n {renderHighlightedText(cell.value.text, tableCellFieldMatcher(options, 1))}\n </ds-text>\n )}\n </span>\n );\n }\n return (\n <span\n class=\"ds-table__cell-tags\"\n style={{ '--_table-cell-tags-min-block-size': tableCellTrackStackBlockSize(cell.tracks) }}\n >\n {cell.value.items.map((item, index) => (\n <ds-tag\n key={`${item.label}-${index}`}\n label={item.label}\n intent={item.intent ?? 'neutral'}\n contrast={item.contrast ?? 'faint'}\n size=\"sm\"\n icon={item.icon ?? ''}\n rounded={item.rounded ?? false}\n isInset\n insetDepth=\"single\"\n />\n ))}\n </span>\n );\n }\n\n if (cell.kind !== 'text') return null;\n return renderTextCopy(cell, options);\n}\n\nfunction renderTextCopy(\n cell: Extract<TableCellPresentation, { kind: 'text' | 'icon-text' | 'score-text' }>,\n options: TableCellViewOptions\n) {\n const text = cell.value;\n const overflow = tableCellTextOverflowProps(cell.lineClamp);\n const primaryText = cell.kind === 'text' && cell.primaryText;\n const href = resolveSafeUrl(text.href);\n const primary = (\n <ds-text\n class=\"ds-table__cell-primary ds-table__cell-track ds-table__cell-track--text\"\n as=\"span\"\n variant=\"text-body-medium\"\n color={href ? 'inherit' : 'primary'}\n lineTruncation={overflow.lineTruncation}\n wrap={overflow.wrap}\n fontFeature={text.fontFeature ?? 'normal'}\n data-table-truncate={truncateAttr(cell.lineClamp)}\n >\n {renderHighlightedText(text.primary, tableCellFieldMatcher(options, 0))}\n </ds-text>\n );\n\n return (\n <span class={{ 'ds-table__cell-copy': true, 'ds-table__cell-copy--wrap': cell.wraps }}>\n {href ? (\n <a\n class=\"ds-table__cell-link ds-text-action ds-focus-ring\"\n href={href}\n target={text.target === '_blank' ? '_blank' : undefined}\n rel={text.target === '_blank' ? 'noopener noreferrer' : undefined}\n >\n {primary}\n </a>\n ) : (\n primary\n )}\n {renderTextTrack(text.secondary, {\n track: 'secondary',\n variant: primaryText ? 'text-body-medium' : 'text-body-small',\n defaultColor: primaryText ? 'primary' : 'secondary',\n wholeColor: text.secondaryColor,\n lineClamp: cell.lineClamp,\n highlightMatcherForRun: runIndex => tableCellFieldMatcher(options, 1 + runIndex),\n })}\n {renderTextTrack(text.tertiary, {\n track: 'tertiary',\n variant: 'text-body-small',\n defaultColor: 'secondary',\n wholeColor: text.tertiaryColor,\n lineClamp: cell.lineClamp,\n highlightMatcherForRun: runIndex =>\n tableCellFieldMatcher(options, 1 + (text.secondary?.length ?? 0) + runIndex),\n })}\n </span>\n );\n}\n\nfunction renderTextTrack(\n runs: TableCellTextRun[] | undefined,\n options: {\n track: 'secondary' | 'tertiary';\n variant: 'text-body-medium' | 'text-body-small';\n defaultColor: 'primary' | 'secondary';\n wholeColor?: TableCellTextRun['color'];\n lineClamp: Extract<\n TableCellPresentation,\n { kind: 'text' | 'icon-text' | 'score-text' }\n >['lineClamp'];\n highlightMatcherForRun: (runIndex: number) => TableHighlightMatcher;\n }\n) {\n if (!runs?.length) return null;\n const trackClass = `ds-table__cell-${options.track} ds-table__cell-track ds-table__cell-track--text`;\n const overflow = tableCellTextOverflowProps(options.lineClamp);\n const colorFor = (run: TableCellTextRun) =>\n run.color ?? options.wholeColor ?? options.defaultColor;\n const renderRun = (run: TableCellTextRun, index: number, className: string) => {\n const copy = (\n <ds-text\n class={className}\n as=\"span\"\n variant={options.variant}\n color={colorFor(run)}\n decoration={run.help ? 'dotted-underline' : undefined}\n lineTruncation={overflow.lineTruncation}\n wrap={overflow.wrap}\n data-table-truncate={truncateAttr(options.lineClamp)}\n >\n {renderHighlightedText(run.text, options.highlightMatcherForRun(index))}\n </ds-text>\n );\n return run.help ? (\n <ds-tooltip\n key={`${options.track}-run-${index}`}\n label={run.help}\n side=\"bottom\"\n align=\"start\"\n size=\"sm\"\n wrapLabel={true}\n >\n {copy}\n </ds-tooltip>\n ) : (\n copy\n );\n };\n if (runs.length === 1) {\n const run = runs[0];\n if (!run.help) return renderRun(run, 0, trackClass);\n return <span class={trackClass}>{renderRun(run, 0, 'ds-table__cell-run')}</span>;\n }\n\n return (\n <span class={`${trackClass} ds-table__cell-track--runs`}>\n {runs.map((run, index) => [\n index > 0 && (\n <ds-text\n key={`${options.track}-sep-${index}`}\n class=\"ds-table__cell-run-separator\"\n as=\"span\"\n variant={options.variant}\n color=\"secondary\"\n aria-hidden=\"true\"\n >\n ·\n </ds-text>\n ),\n renderRun(run, index, 'ds-table__cell-run'),\n ])}\n </span>\n );\n}\n\nfunction renderHighlightedText(value: string | number | undefined, matcher: TableHighlightMatcher) {\n if (value === undefined) return null;\n return matcher(value).map((segment, index) =>\n segment.match ? (\n <mark class=\"ds-table__match\" key={`match-${index}`}>\n {segment.text}\n </mark>\n ) : (\n <span key={`text-${index}`}>{segment.text}</span>\n )\n );\n}\n\nfunction tableCellFieldMatcher(\n options: TableCellViewOptions,\n fieldIndex: number\n): TableHighlightMatcher {\n const selected = options.highlightFieldIds;\n if (selected.length === 0 || selected.includes(options.column.id)) {\n return options.highlightMatcher;\n }\n const fieldId = options.column.segments?.[fieldIndex]?.sortKey;\n return fieldId && selected.includes(fieldId)\n ? options.highlightMatcher\n : TABLE_NO_HIGHLIGHT_MATCHER;\n}\n\nfunction truncateAttr(\n lineClamp: Extract<\n TableCellPresentation,\n { kind: 'text' | 'icon-text' | 'score-text' }\n >['lineClamp']\n) {\n return lineClamp === 'none' ? undefined : '';\n}\n",
1510
1510
  "type": "registry:ui"
1511
1511
  },
1512
1512
  {
@@ -1521,7 +1521,7 @@
1521
1521
  },
1522
1522
  {
1523
1523
  "path": "src/wc/components/Table/table-types.ts",
1524
- "content": "import type { TagContrast, TagIntent } from '../Tag/Tag';\nimport type { IconColor } from '../Icon/Icon';\nimport type { SafetyScoreLevel } from '../Score/score-types';\nimport type { TextColor } from '../Text/text-types';\nimport type { PaginationState } from '../Pagination/pagination-types';\n\nexport type TableSortDirection = 'asc' | 'desc';\nexport type TableSelectionMode = 'none' | 'multiple';\nexport type TableCellAlign = 'start' | 'center' | 'end';\nexport type TableCellLinkTarget = '_self' | '_blank';\nexport type TableColumnSticky = 'start' | 'end';\nexport type TableCaptionVisibility = 'visible' | 'hidden';\nexport type TableLoadMoreMode = 'auto' | 'manual';\nexport type TableDataMode = 'infinite' | 'pagination' | 'virtual';\nexport interface TableDataModeChangeDetail {\n dataMode: TableDataMode;\n}\nexport type TableLoadMoreReason = 'auto' | 'manual' | 'retry';\nexport type TableColumnWidth = 'xs' | 'sm' | 'md' | 'lg' | 'xl';\n/** Visible line budget before a text track ellipsizes. */\nexport type TableCellMaxLines = 1 | 2 | 3;\n/** Resolved clamp, including unlimited wrapping. */\nexport type TableCellLineClamp = TableCellMaxLines | 'none';\n/** Semantic color for a group section header (faint surface + bold title). */\nexport type TableGroupIntent =\n | 'brand'\n | 'neutral'\n | 'negative'\n | 'warning'\n | 'caution'\n | 'positive';\n\n/** Independently sortable label within a compound column header. */\nexport interface TableHeaderSegment {\n /** Compact label rendered inside the table header. */\n label: string;\n /** Complete data-point label used when the segment is named outside the table header. */\n dataLabel?: string;\n /** Whether this data point is offered by TableSearch. Defaults to true. */\n searchable?: boolean;\n /** Stable key emitted through TableSortState.columnId. */\n sortKey: string;\n /** Visible separator rendered after this label when another segment follows. */\n separator?: string;\n}\n\n/** Controlled member-row sort state. Group order is controlled separately. */\nexport interface TableSortState {\n columnId: string;\n direction: TableSortDirection;\n}\n\n/** One controlled grouping level. Applications supply groups in their final fixed order. */\nexport interface TableGroupingState {\n columnId: string;\n direction: TableSortDirection;\n /** Optional application-owned data point used to order the group sections. */\n orderBy?: string;\n}\n\n/** One independently colored run inside a secondary or tertiary track. */\nexport interface TableCellTextRun {\n text: string;\n /** Defaults to the track’s standard color. */\n color?: TextColor;\n /** Optional supplementary copy shown from a dotted-underlined tooltip trigger. */\n help?: string;\n}\n\n/** A secondary or tertiary track: one string, or up to three colorable runs. */\nexport type TableCellTextTrack = string | TableCellTextRun[];\n\n/** Standard two- or three-line cell content owned by the table renderer. */\nexport interface TableCellText {\n primary: string | number;\n secondary?: TableCellTextTrack;\n /** Third subdued track; ignored on dual-primary cells. */\n tertiary?: TableCellTextTrack;\n /** Optional semantic foreground for a string secondary track. */\n secondaryColor?: TextColor;\n /** Optional semantic foreground for a string tertiary track. */\n tertiaryColor?: TextColor;\n /** Application-owned URL for the primary track only. Scalars stay unlinked. */\n href?: string;\n /** Native anchor target for a resolved primary link. */\n target?: TableCellLinkTarget;\n /** Allow this cell to wrap even when its column truncates by default. */\n wrap?: boolean;\n /** Clamp wrapping text after 1, 2, or 3 lines. Overrides column wrap. */\n maxLines?: TableCellMaxLines;\n fontFeature?: 'normal' | 'tabular-nums';\n}\n\n/** Two-line text content where both tracks use primary body text. */\nexport interface TableCellPrimaryText {\n kind: 'primary-text';\n primary: string | number;\n secondary: string | number;\n /** Application-owned URL for the primary track only. */\n href?: string;\n /** Native anchor target for a resolved primary link. */\n target?: TableCellLinkTarget;\n /** Allow this cell to wrap even when its column truncates by default. */\n wrap?: boolean;\n /** Clamp wrapping text after 1, 2, or 3 lines. Overrides column wrap. */\n maxLines?: TableCellMaxLines;\n fontFeature?: 'normal' | 'tabular-nums';\n}\n\n/** Applicable data point whose value is currently unavailable. */\nexport interface TableCellEmpty {\n kind: 'empty';\n}\n\n/** Data point that is intentionally not applicable to this row. */\nexport interface TableCellBlank {\n kind: 'blank';\n}\n\n/** Declarative icon-only content rendered by the table's standard cell primitive. */\nexport interface TableCellIcon {\n kind: 'icon';\n /** Exact canonical IcoMo export name. */\n icon: string;\n color?: IconColor;\n /** Accessible label for informative icons. Omit only when the icon is decorative. */\n label?: string;\n /** Application-owned value used when this icon column participates in sorting. */\n sortValue?: string | number | boolean;\n}\n\n/**\n * Prefix icon beside a text copy stack. Independent of icon-only and plain\n * text cells so those recipes stay unchanged.\n */\nexport interface TableCellIconText {\n kind: 'icon-text';\n /** Exact canonical IcoMo export name. */\n icon: string;\n iconColor?: IconColor;\n /** Accessible name. Omit when the glyph is decorative. */\n iconLabel?: string;\n primary: string | number;\n secondary?: TableCellTextTrack;\n tertiary?: TableCellTextTrack;\n secondaryColor?: TextColor;\n tertiaryColor?: TextColor;\n href?: string;\n target?: TableCellLinkTarget;\n wrap?: boolean;\n maxLines?: TableCellMaxLines;\n fontFeature?: 'normal' | 'tabular-nums';\n}\n\n/**\n * Compact safety-score fill at the top-right of a text copy stack. The score\n * occupies one 24px track while the copy keeps the standard text-cell anatomy.\n */\nexport interface TableCellScoreText {\n kind: 'score-text';\n /** Display-ready safety score. Numeric values infer their semantic level. */\n score: string | number;\n scoreLevel?: SafetyScoreLevel;\n /** Accessible name prefix. Defaults to “Safety score”. */\n scoreLabel?: string;\n /** Replace the score figure with its matching sm skeleton. */\n scoreLoading?: boolean;\n primary: string | number;\n secondary?: TableCellTextTrack;\n tertiary?: TableCellTextTrack;\n secondaryColor?: TextColor;\n tertiaryColor?: TextColor;\n href?: string;\n target?: TableCellLinkTarget;\n wrap?: boolean;\n maxLines?: TableCellMaxLines;\n fontFeature?: 'normal' | 'tabular-nums';\n}\n\n/** Body-row track stack an image preview occupies. */\nexport type TableCellImageTracks = 1 | 2 | 3;\n\n/** Declarative 16:9 image preview rendered by the table's standard cell primitive. */\nexport interface TableCellImage {\n kind: 'image';\n /** Optional image source. Omit it to render the standard bordered placeholder. */\n src?: string;\n /** Accessible description for either the image or its placeholder. */\n alt: string;\n /**\n * Which body-row contract the preview fills. The thumbnail height is that\n * cell height minus 8px padding; width follows 16:9. Defaults to 1.\n */\n tracks?: TableCellImageTracks;\n}\n\ninterface TableCellActionBase {\n kind: 'action';\n /** Add the ButtonUnfilled resting border. Action cells are unbordered by default. */\n hasBorder?: boolean;\n isInactive?: boolean;\n isLoading?: boolean;\n}\n\n/** One command inside an overflow action menu. */\nexport interface TableCellActionMenuItem {\n /** Stable application-owned action identity emitted with dsCellAction. */\n actionId: string;\n label: string;\n isInactive?: boolean;\n isDestructive?: boolean;\n}\n\n/** Visual separator between overflow action-menu commands. */\nexport interface TableCellActionDivider {\n kind: 'divider';\n}\n\nexport type TableCellActionMenuEntry = TableCellActionMenuItem | TableCellActionDivider;\n\n/** Declarative ButtonUnfilled content rendered by the table's standard action cell. */\nexport type TableCellAction = TableCellActionBase &\n (\n | {\n variant?: 'label';\n /** Stable application-owned action identity emitted with dsCellAction. */\n actionId: string;\n label: string;\n icon?: never;\n ariaLabel?: string;\n items?: never;\n }\n | {\n variant: 'icon-label';\n actionId: string;\n label: string;\n icon: string;\n ariaLabel?: string;\n items?: never;\n }\n | {\n variant: 'icon';\n actionId: string;\n label?: never;\n icon: string;\n ariaLabel: string;\n items?: never;\n }\n | {\n variant?: 'icon';\n /** Overflow commands. The Ellipses trigger opens ds-menu instead of emitting on click. */\n items: TableCellActionMenuEntry[];\n ariaLabel: string;\n icon?: string;\n actionId?: never;\n label?: never;\n }\n );\n\n/** Declarative Tag content rendered by the table's standard cell primitive. */\nexport type TableCellTagVariant = 'tag-only' | 'tag-with-text' | 'text-with-tag';\n\n/** One Tag inside a single- or multiple-Tag table cell. */\nexport interface TableCellTagItem {\n label: string;\n intent?: TagIntent;\n contrast?: TagContrast;\n icon?: string;\n rounded?: boolean;\n}\n\nexport type TableCellTag = TableCellTagItem & { kind: 'tag' } & (\n | {\n variant?: 'tag-only';\n text?: never;\n }\n | {\n variant: Exclude<TableCellTagVariant, 'tag-only'>;\n text: string;\n }\n );\n\n/** Multiple Tags in wrapping tracks or a compact slash-separated primary track. */\nexport type TableCellTags = { kind: 'tags' } & (\n | {\n variant?: 'wrap';\n items: TableCellTagItem[];\n /** Expected positive wrapped-line count used for row geometry and virtual estimates. */\n tracks: number;\n text?: never;\n }\n | {\n variant: 'inline';\n /** One to three tags. Runtime inputs beyond three are capped. */\n items:\n | [TableCellTagItem]\n | [TableCellTagItem, TableCellTagItem]\n | [TableCellTagItem, TableCellTagItem, TableCellTagItem];\n /** Optional secondary text beneath the non-wrapping tag track. */\n text?: string;\n tracks?: never;\n }\n);\n\nexport type TableCellValue =\n | string\n | number\n | null\n | undefined\n | TableCellText\n | TableCellPrimaryText\n | TableCellTag\n | TableCellTags\n | TableCellIcon\n | TableCellIconText\n | TableCellScoreText\n | TableCellImage\n | TableCellAction\n | TableCellEmpty\n | TableCellBlank;\n\nexport type TableSkeletonWidth = string | number;\n\n/** Representative loading geometry for one table column. */\nexport type TableCellSkeleton =\n | {\n kind: 'text';\n /** One track for scalar content, two for primary and secondary, three when tertiary is present. */\n lines?: 1 | 2 | 3;\n primaryWidth?: TableSkeletonWidth;\n secondaryWidth?: TableSkeletonWidth;\n tertiaryWidth?: TableSkeletonWidth;\n }\n | {\n kind: 'image';\n /** Match the loaded image cell's track stack. Defaults to 1. */\n tracks?: TableCellImageTracks;\n }\n | {\n kind: 'tag';\n width?: TableSkeletonWidth;\n }\n | {\n kind: 'icon';\n rounded?: boolean;\n }\n | {\n kind: 'icon-text';\n lines?: 1 | 2 | 3;\n primaryWidth?: TableSkeletonWidth;\n secondaryWidth?: TableSkeletonWidth;\n tertiaryWidth?: TableSkeletonWidth;\n }\n | {\n kind: 'score-text';\n lines?: 1 | 2 | 3;\n primaryWidth?: TableSkeletonWidth;\n secondaryWidth?: TableSkeletonWidth;\n tertiaryWidth?: TableSkeletonWidth;\n }\n | {\n kind: 'action';\n /** Icon actions use a square control canvas; labeled actions may provide a wider width. */\n variant?: 'icon' | 'label' | 'icon-label';\n width?: TableSkeletonWidth;\n }\n | {\n kind: 'blank';\n };\n\nexport interface TableColumn {\n /** Stable column identity. */\n id: string;\n /** Visible column label. May be empty when headerLabel supplies a non-visual name. */\n header: string;\n /** Complete data-point label used by controls such as Sort and Search. */\n dataLabel?: string;\n /** Whether this data point is offered by TableSearch. Defaults to true for data columns. */\n searchable?: boolean;\n /** Screen-reader-only column name for an intentionally blank visual header. */\n headerLabel?: string;\n /** Supplementary header help. Does not replace the visible or accessible column name. */\n help?: string;\n /** Optional labels for columns that present and sort multiple related data points. */\n headerSegments?: TableHeaderSegment[];\n align?: TableCellAlign;\n sortable?: boolean;\n /** Preferred TokoMo table-column width. Numbers remain available for exceptional custom pixel widths. */\n size?: TableColumnWidth | number;\n /** Derive a fixed image-column width from the matching 1, 2, or 3 track cell geometry. Ignored when size is set. */\n imageTracks?: TableCellImageTracks;\n /** Minimum accepted preferred width in CSS pixels. */\n minSize?: number;\n /** Maximum accepted preferred width in CSS pixels. */\n maxSize?: number;\n /** Wrap cell text instead of truncating it to one line. */\n wrap?: boolean;\n /** Clamp wrapping text after 1, 2, or 3 lines. Omit with wrap for unlimited wrap. */\n maxLines?: TableCellMaxLines;\n /** Pin one application column to either inline edge during horizontal scrolling. */\n sticky?: TableColumnSticky;\n /**\n * Declare a trailing application-action lane. Action columns keep fixed\n * geometry and may host grouped table-level controls in their header.\n */\n kind?: 'data' | 'action';\n /** Representative loading geometry for this column. Defaults to one text line. */\n skeleton?: TableCellSkeleton;\n}\n\nexport interface TableRow {\n /** Stable identity, unique across all groups. */\n id: string;\n cells: Record<string, TableCellValue>;\n /** Accessible row name used by its selection control. Falls back to the first cell. */\n selectionLabel?: string;\n /** Exclude this row from selection without muting its content. */\n selectable?: boolean;\n /** Keep the row visible while disabling its selection control and styling it as inactive. */\n disabled?: boolean;\n /** Make the complete row a keyboard- and pointer-activatable application target. */\n interactive?: boolean;\n}\n\n/** Supporting copy on a group section header's second track. */\nexport interface TableGroupAccessory {\n text: string;\n /** Optional tooltip. Uses the shared dotted underline when present. */\n help?: string;\n}\n\n/** Leading group-header accessory. Score is the first supported hero. */\nexport interface TableGroupHeroScore {\n kind: 'score';\n value: string | number;\n level?: SafetyScoreLevel;\n /** Span one label track by default, or center a larger score across two tracks. */\n tracks?: 1 | 2;\n /** Accessible name prefix, for example “Safety score”. */\n label?: string;\n isLoading?: boolean;\n}\n\nexport type TableGroupHero = TableGroupHeroScore;\n\nexport interface TableGroup {\n /** Stable group identity. */\n id: string;\n label: string;\n value?: string | number;\n /** Total server-side member count; may exceed the currently loaded row count. */\n totalCount?: number;\n /** Optional localized total-count phrase such as “166 events” for accessible loaded progress. */\n countLabel?: string;\n /**\n * Optional semantic intent for the group section header. Applies a faint\n * intent background and bold intent title color.\n */\n intent?: TableGroupIntent;\n /**\n * Optional accessory copy on a second 24px track under the label and count.\n * Middle-dot separated. At most four items are shown.\n */\n accessories?: TableGroupAccessory[];\n /**\n * Optional leading hero before the label and count. A score uses sm on both\n * single-track and two-track headers and sits at the start of the copy stack.\n */\n hero?: TableGroupHero;\n /** Whether this group has more member rows available to load. */\n hasMore?: boolean;\n /** Controlled incremental loading state for this group's member rows. */\n loadingMore?: boolean;\n /** Controlled incremental loading failure for this group's member rows. */\n loadMoreError?: string;\n /** Reset key for this group's query, sort, or filter state. Defaults to the group ID. */\n loadIdentity?: string | number;\n rows: TableRow[];\n}\n\nexport interface TableSortChangeDetail {\n sort: TableSortState | null;\n}\n\nexport interface TableSelectionChangeDetail {\n selectedRowIds: string[];\n scope: 'row' | 'all-loaded' | 'group';\n changedRowId?: string;\n /** Present when scope is `group`. */\n groupId?: string;\n selected?: boolean;\n}\n\nexport interface TableLoadMoreDetail {\n reason: TableLoadMoreReason;\n loadIdentity: string | number;\n loadedRowCount: number;\n}\n\nexport interface TableGroupLoadMoreDetail extends TableLoadMoreDetail {\n groupId: string;\n}\n\nexport interface TableCellActionDetail {\n actionId: string;\n rowId: string;\n columnId: string;\n}\n\nexport interface TableGroupCollapseChangeDetail {\n scope: 'group' | 'all';\n /** Present when a single group was toggled. */\n groupId?: string;\n collapsed: boolean;\n collapsedGroupIds: string[];\n}\n\nexport interface TableRowActivateDetail {\n rowId: string;\n}\n\n/** Controlled show/hide and data-column order for the table-owned customizer. */\nexport interface TableColumnsConfigChangeDetail {\n hiddenColumnIds: string[];\n columnOrder: string[];\n}\n\n/** Controlled top-level pagination state. Rows or groups contain only the active page. */\nexport interface TablePaginationState extends PaginationState {\n /** Change to request one fresh fitted-capacity snapshot for the current query shape. */\n fitIdentity?: string | number;\n}\n",
1524
+ "content": "import type { DataField, DataFieldSegment } from '../../utils/data-field';\nimport type { TagContrast, TagIntent } from '../Tag/Tag';\nimport type { IconColor } from '../Icon/Icon';\nimport type { SafetyScoreLevel } from '../Score/score-types';\nimport type { TextColor } from '../Text/text-types';\nimport type { PaginationState } from '../Pagination/pagination-types';\n\nexport type DataSortDirection = 'asc' | 'desc';\nexport type TableSelectionMode = 'none' | 'multiple';\nexport type TableCellAlign = 'start' | 'center' | 'end';\nexport type TableCellLinkTarget = '_self' | '_blank';\nexport type TableColumnSticky = 'start' | 'end';\nexport type TableCaptionVisibility = 'visible' | 'hidden';\nexport type TableLoadMoreMode = 'auto' | 'manual';\nexport type TableDataMode = 'infinite' | 'pagination' | 'virtual';\nexport interface TableDataModeChangeDetail {\n dataMode: TableDataMode;\n}\nexport type TableLoadMoreReason = 'auto' | 'manual' | 'retry';\nexport type TableColumnWidth = 'xs' | 'sm' | 'md' | 'lg' | 'xl';\n/** Visible line budget before a text track ellipsizes. */\nexport type TableCellMaxLines = 1 | 2 | 3;\n/** Resolved clamp, including unlimited wrapping. */\nexport type TableCellLineClamp = TableCellMaxLines | 'none';\n/** Semantic color for a group section header (faint surface + bold title). */\nexport type TableGroupIntent =\n | 'brand'\n | 'neutral'\n | 'negative'\n | 'warning'\n | 'caution'\n | 'positive';\n\n/** Independently sortable label within a compound column header. */\nexport interface TableHeaderSegment extends DataFieldSegment {\n /** Visible separator rendered after this label when another segment follows. */\n separator?: string;\n}\n\n/** Controlled member-row sort state. Group order is controlled separately. */\nexport interface DataSortState {\n fieldId: string;\n direction: DataSortDirection;\n}\n\n/** One controlled grouping level. Applications supply groups in their final fixed order. */\nexport interface DataGroupingState {\n fieldId: string;\n direction: DataSortDirection;\n /** Optional application-owned data point used to order the group sections. */\n orderBy?: string;\n}\n\n/** One independently colored run inside a secondary or tertiary track. */\nexport interface TableCellTextRun {\n text: string;\n /** Defaults to the track’s standard color. */\n color?: TextColor;\n /** Optional supplementary copy shown from a dotted-underlined tooltip trigger. */\n help?: string;\n}\n\n/** A secondary or tertiary track: one string, or up to three colorable runs. */\nexport type TableCellTextTrack = string | TableCellTextRun[];\n\n/** Standard two- or three-line cell content owned by the table renderer. */\nexport interface TableCellText {\n primary: string | number;\n secondary?: TableCellTextTrack;\n /** Third subdued track; ignored on dual-primary cells. */\n tertiary?: TableCellTextTrack;\n /** Optional semantic foreground for a string secondary track. */\n secondaryColor?: TextColor;\n /** Optional semantic foreground for a string tertiary track. */\n tertiaryColor?: TextColor;\n /** Application-owned URL for the primary track only. Scalars stay unlinked. */\n href?: string;\n /** Native anchor target for a resolved primary link. */\n target?: TableCellLinkTarget;\n /** Allow this cell to wrap even when its column truncates by default. */\n wrap?: boolean;\n /** Clamp wrapping text after 1, 2, or 3 lines. Overrides column wrap. */\n maxLines?: TableCellMaxLines;\n fontFeature?: 'normal' | 'tabular-nums';\n}\n\n/** Two-line text content where both tracks use primary body text. */\nexport interface TableCellPrimaryText {\n kind: 'primary-text';\n primary: string | number;\n secondary: string | number;\n /** Application-owned URL for the primary track only. */\n href?: string;\n /** Native anchor target for a resolved primary link. */\n target?: TableCellLinkTarget;\n /** Allow this cell to wrap even when its column truncates by default. */\n wrap?: boolean;\n /** Clamp wrapping text after 1, 2, or 3 lines. Overrides column wrap. */\n maxLines?: TableCellMaxLines;\n fontFeature?: 'normal' | 'tabular-nums';\n}\n\n/** Applicable data point whose value is currently unavailable. */\nexport interface TableCellEmpty {\n kind: 'empty';\n}\n\n/** Data point that is intentionally not applicable to this row. */\nexport interface TableCellBlank {\n kind: 'blank';\n}\n\n/** Declarative icon-only content rendered by the table's standard cell primitive. */\nexport interface TableCellIcon {\n kind: 'icon';\n /** Exact canonical IcoMo export name. */\n icon: string;\n color?: IconColor;\n /** Accessible label for informative icons. Omit only when the icon is decorative. */\n label?: string;\n /** Application-owned value used when this icon column participates in sorting. */\n sortValue?: string | number | boolean;\n}\n\n/**\n * Prefix icon beside a text copy stack. Independent of icon-only and plain\n * text cells so those recipes stay unchanged.\n */\nexport interface TableCellIconText {\n kind: 'icon-text';\n /** Exact canonical IcoMo export name. */\n icon: string;\n iconColor?: IconColor;\n /** Accessible name. Omit when the glyph is decorative. */\n iconLabel?: string;\n primary: string | number;\n secondary?: TableCellTextTrack;\n tertiary?: TableCellTextTrack;\n secondaryColor?: TextColor;\n tertiaryColor?: TextColor;\n href?: string;\n target?: TableCellLinkTarget;\n wrap?: boolean;\n maxLines?: TableCellMaxLines;\n fontFeature?: 'normal' | 'tabular-nums';\n}\n\n/**\n * Compact safety-score fill at the top-right of a text copy stack. The score\n * occupies one 24px track while the copy keeps the standard text-cell anatomy.\n */\nexport interface TableCellScoreText {\n kind: 'score-text';\n /** Display-ready safety score. Numeric values infer their semantic level. */\n score: string | number;\n scoreLevel?: SafetyScoreLevel;\n /** Accessible name prefix. Defaults to “Safety score”. */\n scoreLabel?: string;\n /** Replace the score figure with its matching sm skeleton. */\n scoreLoading?: boolean;\n primary: string | number;\n secondary?: TableCellTextTrack;\n tertiary?: TableCellTextTrack;\n secondaryColor?: TextColor;\n tertiaryColor?: TextColor;\n href?: string;\n target?: TableCellLinkTarget;\n wrap?: boolean;\n maxLines?: TableCellMaxLines;\n fontFeature?: 'normal' | 'tabular-nums';\n}\n\n/** Body-row track stack an image preview occupies. */\nexport type TableCellImageTracks = 1 | 2 | 3;\n\n/** Declarative 16:9 image preview rendered by the table's standard cell primitive. */\nexport interface TableCellImage {\n kind: 'image';\n /** Optional image source. Omit it to render the standard bordered placeholder. */\n src?: string;\n /** Accessible description for either the image or its placeholder. */\n alt: string;\n /**\n * Which body-row contract the preview fills. The thumbnail height is that\n * cell height minus 8px padding; width follows 16:9. Defaults to 1.\n */\n tracks?: TableCellImageTracks;\n}\n\ninterface TableCellActionBase {\n kind: 'action';\n /** Add the ButtonUnfilled resting border. Action cells are unbordered by default. */\n hasBorder?: boolean;\n isInactive?: boolean;\n isLoading?: boolean;\n}\n\n/** One command inside an overflow action menu. */\nexport interface TableCellActionMenuItem {\n /** Stable application-owned action identity emitted with dsCellAction. */\n actionId: string;\n label: string;\n isInactive?: boolean;\n isDestructive?: boolean;\n}\n\n/** Visual separator between overflow action-menu commands. */\nexport interface TableCellActionDivider {\n kind: 'divider';\n}\n\nexport type TableCellActionMenuEntry = TableCellActionMenuItem | TableCellActionDivider;\n\n/** Declarative ButtonUnfilled content rendered by the table's standard action cell. */\nexport type TableCellAction = TableCellActionBase &\n (\n | {\n variant?: 'label';\n /** Stable application-owned action identity emitted with dsCellAction. */\n actionId: string;\n label: string;\n icon?: never;\n ariaLabel?: string;\n items?: never;\n }\n | {\n variant: 'icon-label';\n actionId: string;\n label: string;\n icon: string;\n ariaLabel?: string;\n items?: never;\n }\n | {\n variant: 'icon';\n actionId: string;\n label?: never;\n icon: string;\n ariaLabel: string;\n items?: never;\n }\n | {\n variant?: 'icon';\n /** Overflow commands. The Ellipses trigger opens ds-menu instead of emitting on click. */\n items: TableCellActionMenuEntry[];\n ariaLabel: string;\n icon?: string;\n actionId?: never;\n label?: never;\n }\n );\n\n/** Declarative Tag content rendered by the table's standard cell primitive. */\nexport type TableCellTagVariant = 'tag-only' | 'tag-with-text' | 'text-with-tag';\n\n/** One Tag inside a single- or multiple-Tag table cell. */\nexport interface TableCellTagItem {\n label: string;\n intent?: TagIntent;\n contrast?: TagContrast;\n icon?: string;\n rounded?: boolean;\n}\n\nexport type TableCellTag = TableCellTagItem & { kind: 'tag' } & (\n | {\n variant?: 'tag-only';\n text?: never;\n }\n | {\n variant: Exclude<TableCellTagVariant, 'tag-only'>;\n text: string;\n }\n );\n\n/** Multiple Tags in wrapping tracks or a compact slash-separated primary track. */\nexport type TableCellTags = { kind: 'tags' } & (\n | {\n variant?: 'wrap';\n items: TableCellTagItem[];\n /** Expected positive wrapped-line count used for row geometry and virtual estimates. */\n tracks: number;\n text?: never;\n }\n | {\n variant: 'inline';\n /** One to three tags. Runtime inputs beyond three are capped. */\n items:\n | [TableCellTagItem]\n | [TableCellTagItem, TableCellTagItem]\n | [TableCellTagItem, TableCellTagItem, TableCellTagItem];\n /** Optional secondary text beneath the non-wrapping tag track. */\n text?: string;\n tracks?: never;\n }\n);\n\nexport type TableCellValue =\n | string\n | number\n | null\n | undefined\n | TableCellText\n | TableCellPrimaryText\n | TableCellTag\n | TableCellTags\n | TableCellIcon\n | TableCellIconText\n | TableCellScoreText\n | TableCellImage\n | TableCellAction\n | TableCellEmpty\n | TableCellBlank;\n\nexport type TableSkeletonWidth = string | number;\n\n/** Representative loading geometry for one table column. */\nexport type TableCellSkeleton =\n | {\n kind: 'text';\n /** One track for scalar content, two for primary and secondary, three when tertiary is present. */\n lines?: 1 | 2 | 3;\n primaryWidth?: TableSkeletonWidth;\n secondaryWidth?: TableSkeletonWidth;\n tertiaryWidth?: TableSkeletonWidth;\n }\n | {\n kind: 'image';\n /** Match the loaded image cell's track stack. Defaults to 1. */\n tracks?: TableCellImageTracks;\n }\n | {\n kind: 'tag';\n width?: TableSkeletonWidth;\n }\n | {\n kind: 'icon';\n rounded?: boolean;\n }\n | {\n kind: 'icon-text';\n lines?: 1 | 2 | 3;\n primaryWidth?: TableSkeletonWidth;\n secondaryWidth?: TableSkeletonWidth;\n tertiaryWidth?: TableSkeletonWidth;\n }\n | {\n kind: 'score-text';\n lines?: 1 | 2 | 3;\n primaryWidth?: TableSkeletonWidth;\n secondaryWidth?: TableSkeletonWidth;\n tertiaryWidth?: TableSkeletonWidth;\n }\n | {\n kind: 'action';\n /** Icon actions use a square control canvas; labeled actions may provide a wider width. */\n variant?: 'icon' | 'label' | 'icon-label';\n width?: TableSkeletonWidth;\n }\n | {\n kind: 'blank';\n };\n\nexport interface TableColumn extends DataField {\n segments?: TableHeaderSegment[];\n /** Supplementary header help. Does not replace the visible or accessible column name. */\n help?: string;\n align?: TableCellAlign;\n /** Preferred TokoMo table-column width. Numbers remain available for exceptional custom pixel widths. */\n size?: TableColumnWidth | number;\n /** Derive a fixed image-column width from the matching 1, 2, or 3 track cell geometry. Ignored when size is set. */\n imageTracks?: TableCellImageTracks;\n /** Minimum accepted preferred width in CSS pixels. */\n minSize?: number;\n /** Maximum accepted preferred width in CSS pixels. */\n maxSize?: number;\n /** Wrap cell text instead of truncating it to one line. */\n wrap?: boolean;\n /** Clamp wrapping text after 1, 2, or 3 lines. Omit with wrap for unlimited wrap. */\n maxLines?: TableCellMaxLines;\n /** Pin one application column to either inline edge during horizontal scrolling. */\n sticky?: TableColumnSticky;\n /**\n * Declare a trailing application-action lane. Action columns keep fixed\n * geometry and may host grouped table-level controls in their header.\n */\n kind?: 'data' | 'action';\n /** Representative loading geometry for this column. Defaults to one text line. */\n skeleton?: TableCellSkeleton;\n}\n\nexport interface TableRow {\n /** Stable identity, unique across all groups. */\n id: string;\n cells: Record<string, TableCellValue>;\n /** Accessible row name used by its selection control. Falls back to the first cell. */\n selectionLabel?: string;\n /** Exclude this row from selection without muting its content. */\n selectable?: boolean;\n /** Keep the row visible while disabling its selection control and styling it as inactive. */\n disabled?: boolean;\n /** Make the complete row a keyboard- and pointer-activatable application target. */\n interactive?: boolean;\n}\n\n/** Supporting copy on a group section header's second track. */\nexport interface TableGroupAccessory {\n text: string;\n /** Optional tooltip. Uses the shared dotted underline when present. */\n help?: string;\n}\n\n/** Leading group-header accessory. Score is the first supported hero. */\nexport interface TableGroupHeroScore {\n kind: 'score';\n value: string | number;\n level?: SafetyScoreLevel;\n /** Span one label track by default, or center a larger score across two tracks. */\n tracks?: 1 | 2;\n /** Accessible name prefix, for example “Safety score”. */\n label?: string;\n isLoading?: boolean;\n}\n\nexport type TableGroupHero = TableGroupHeroScore;\n\nexport interface DataGroup {\n /** Stable group identity. */\n id: string;\n label: string;\n value?: string | number;\n /** Total server-side member count; may exceed the currently loaded row count. */\n totalCount?: number;\n /** Optional localized total-count phrase such as “166 events” for accessible loaded progress. */\n countLabel?: string;\n /**\n * Optional semantic intent for the group section header. Applies a faint\n * intent background and bold intent title color.\n */\n intent?: TableGroupIntent;\n /**\n * Optional accessory copy on a second 24px track under the label and count.\n * Middle-dot separated. At most four items are shown.\n */\n accessories?: TableGroupAccessory[];\n /**\n * Optional leading hero before the label and count. A score uses sm on both\n * single-track and two-track headers and sits at the start of the copy stack.\n */\n hero?: TableGroupHero;\n /** Whether this group has more member rows available to load. */\n hasMore?: boolean;\n /** Controlled incremental loading state for this group's member rows. */\n loadingMore?: boolean;\n /** Controlled incremental loading failure for this group's member rows. */\n loadMoreError?: string;\n /** Reset key for this group's query, sort, or filter state. Defaults to the group ID. */\n loadIdentity?: string | number;\n rows: TableRow[];\n}\n\nexport interface DataSortChangeDetail {\n sort: DataSortState | null;\n}\n\nexport interface TableSelectionChangeDetail {\n selectedRowIds: string[];\n scope: 'row' | 'all-loaded' | 'group';\n changedRowId?: string;\n /** Present when scope is `group`. */\n groupId?: string;\n selected?: boolean;\n}\n\nexport interface TableLoadMoreDetail {\n reason: TableLoadMoreReason;\n loadIdentity: string | number;\n loadedRowCount: number;\n}\n\nexport interface TableGroupLoadMoreDetail extends TableLoadMoreDetail {\n groupId: string;\n}\n\nexport interface TableCellActionDetail {\n actionId: string;\n rowId: string;\n columnId: string;\n}\n\nexport interface TableGroupCollapseChangeDetail {\n scope: 'group' | 'all';\n /** Present when a single group was toggled. */\n groupId?: string;\n collapsed: boolean;\n collapsedGroupIds: string[];\n}\n\nexport interface TableRowActivateDetail {\n rowId: string;\n}\n\n/** Controlled show/hide and data-column order for the table-owned customizer. */\nexport interface DataFieldsConfigChangeDetail {\n hiddenFieldIds: string[];\n fieldOrder: string[];\n}\n\n/** Controlled top-level pagination state. Rows or groups contain only the active page. */\nexport interface TablePaginationState extends PaginationState {\n /** Change to request one fresh fitted-capacity snapshot for the current query shape. */\n fitIdentity?: string | number;\n}\n",
1525
1525
  "type": "registry:ui"
1526
1526
  },
1527
1527
  {
@@ -1536,7 +1536,7 @@
1536
1536
  },
1537
1537
  {
1538
1538
  "path": "src/wc/components/Table/table-virtual-model.ts",
1539
- "content": "import { resolveTableCellPresentation } from './table-cell-model';\nimport { tableGroupAccessories } from './table-model';\nimport type { TableColumn, TableGroup, TableRow } from './table-types';\n\n/**\n * Token-aligned body-row track contracts: 40 / 64 / 88, plus wrapping-secondary\n * 112. Every track is 24px with no stack gap. Estimates only; measured heights\n * replace these after paint.\n */\nexport const TABLE_VIRTUAL_ROW_TRACK_SIZE = {\n 1: 40,\n 2: 64,\n 3: 88,\n 4: 112,\n} as const;\n\nconst TABLE_VIRTUAL_LATER_TRACK_INCREMENT =\n TABLE_VIRTUAL_ROW_TRACK_SIZE[2] - TABLE_VIRTUAL_ROW_TRACK_SIZE[1];\n\n/** Estimated row height for any positive named track count. */\nexport function tableVirtualRowTrackSize(tracks: number): number {\n const count = Number.isFinite(tracks) && tracks > 0 ? Math.floor(tracks) : 1;\n return TABLE_VIRTUAL_ROW_TRACK_SIZE[1] + (count - 1) * TABLE_VIRTUAL_LATER_TRACK_INCREMENT;\n}\n\nexport const TABLE_VIRTUAL_GROUP_HEADER_SIZE = TABLE_VIRTUAL_ROW_TRACK_SIZE[1];\nexport const TABLE_VIRTUAL_MIN_OVERSCAN_ROWS = 8;\n\nexport const TABLE_VIRTUAL_VIEWPORT_REQUIRED_HEADING = 'Bounded height required';\nexport const TABLE_VIRTUAL_VIEWPORT_REQUIRED_BODY =\n 'Virtual mode requires height, maxHeight, or fitViewport.';\n\nexport const TABLE_VIRTUAL_TOTAL_SUMMARY_LABEL = '{total} items';\n\nexport type TableVirtualItemKind = 'group' | 'row';\n\nexport interface TableVirtualItem {\n kind: TableVirtualItemKind;\n id: string;\n groupId?: string;\n rowId?: string;\n estimatedSize: number;\n variableSize: boolean;\n}\n\nexport type TableVirtualNode =\n | { kind: 'spacer'; size: number; key: string }\n | { kind: 'row'; index: number; item: TableVirtualItem }\n | {\n kind: 'group';\n groupId: string;\n headerIndex: number;\n nodes: Array<Extract<TableVirtualNode, { kind: 'spacer' | 'row' }>>;\n };\n\nexport interface TableVirtualPlan {\n totalSize: number;\n itemCount: number;\n start: number;\n end: number;\n nodes: TableVirtualNode[];\n mountedIds: ReadonlySet<string>;\n mountedIndexes: ReadonlySet<number>;\n}\n\nexport interface FlattenTableVirtualItemsInput {\n grouped: boolean;\n rows: TableRow[];\n groups: TableGroup[];\n collapsedGroupIds: ReadonlySet<string> | readonly string[];\n columns: TableColumn[];\n}\n\nexport interface ResolveTableVirtualPlanInput {\n items: readonly TableVirtualItem[];\n sizes: readonly number[];\n scrollOffset: number;\n viewportSize: number;\n pinnedRowIds?: ReadonlySet<string>;\n}\n\nexport interface TableVirtualIndex {\n items: readonly TableVirtualItem[];\n sizes: readonly number[];\n prefix: readonly number[];\n itemCount: number;\n totalSize: number;\n hasVariableSize: boolean;\n grouped: boolean;\n groupHeaderIndexes: readonly number[];\n headerIndexByGroup: ReadonlyMap<string, number>;\n groupEndByHeader: ReadonlyMap<number, number>;\n rowIndexById: ReadonlyMap<string, number>;\n itemIndexById: ReadonlyMap<string, number>;\n}\n\nexport interface ResolveTableVirtualIndexedPlanInput {\n scrollOffset: number;\n viewportSize: number;\n scrollDirection?: 'backward' | 'forward' | 'none';\n pinnedRowIds?: ReadonlySet<string>;\n}\n\n/** Map a cell recipe onto the named track stack used for first-paint estimates. */\nexport function estimateTableCellTrackCount(\n value: TableRow['cells'][string],\n column: TableColumn\n): number {\n return tableCellTrackCount(resolveTableCellPresentation(value, column));\n}\n\nfunction tableCellTrackCount(\n presentation: ReturnType<typeof resolveTableCellPresentation>\n): number {\n if (presentation.kind === 'image') {\n return presentation.variant === 'triple' ? 3 : presentation.variant === 'multi' ? 2 : 1;\n }\n if (presentation.kind === 'tag') {\n return presentation.variant === 'text-with-tag' ? 2 : 1;\n }\n if (presentation.kind === 'tags') return presentation.tracks;\n if (\n presentation.kind !== 'text' &&\n presentation.kind !== 'icon-text' &&\n presentation.kind !== 'score-text'\n )\n return 1;\n\n const variantTracks =\n presentation.variant === 'triple'\n ? 3\n : presentation.variant === 'multi' || presentation.variant === 'primary-pair'\n ? 2\n : 1;\n if (!presentation.wraps) return variantTracks;\n if (presentation.lineClamp === 'none') {\n return presentation.kind === 'text' && !presentation.singleLine ? 4 : 3;\n }\n if (presentation.lineClamp === 3 && variantTracks >= 2) return 4;\n return Math.max(variantTracks, presentation.lineClamp);\n}\n\n/** Tallest named track stack across the row's visible cells. */\nexport function estimateTableRowBlockSize(row: TableRow, columns: readonly TableColumn[]): number {\n return tableVirtualRowMetrics(row, columns).estimatedSize;\n}\n\nfunction tableVirtualRowMetrics(\n row: TableRow,\n columns: readonly TableColumn[]\n): Pick<TableVirtualItem, 'estimatedSize' | 'variableSize'> {\n let tracks = 1;\n let variableSize = false;\n for (const column of columns) {\n const value = row.cells[column.id];\n const presentation = resolveTableCellPresentation(value, column);\n if (\n (presentation.kind === 'text' ||\n presentation.kind === 'icon-text' ||\n presentation.kind === 'score-text') &&\n presentation.wraps &&\n presentation.lineClamp === 'none'\n ) {\n variableSize = true;\n }\n if (presentation.kind === 'tags') variableSize = true;\n const count = tableCellTrackCount(presentation);\n if (count > tracks) tracks = count;\n }\n return { estimatedSize: tableVirtualRowTrackSize(tracks), variableSize };\n}\n\nexport function flattenTableVirtualItems(input: FlattenTableVirtualItemsInput): TableVirtualItem[] {\n const collapsed =\n input.collapsedGroupIds instanceof Set\n ? input.collapsedGroupIds\n : new Set(input.collapsedGroupIds);\n if (!input.grouped) {\n return input.rows.map(row => {\n const metrics = tableVirtualRowMetrics(row, input.columns);\n return {\n kind: 'row' as const,\n id: `row:${row.id}`,\n rowId: row.id,\n ...metrics,\n };\n });\n }\n\n const items: TableVirtualItem[] = [];\n for (const group of input.groups) {\n items.push({\n kind: 'group',\n id: `group:${group.id}`,\n groupId: group.id,\n estimatedSize: tableVirtualRowTrackSize(tableGroupAccessories(group).length > 0 ? 2 : 1),\n variableSize: false,\n });\n if (collapsed.has(group.id)) continue;\n for (const row of group.rows) {\n const metrics = tableVirtualRowMetrics(row, input.columns);\n items.push({\n kind: 'row',\n id: `row:${row.id}`,\n rowId: row.id,\n groupId: group.id,\n ...metrics,\n });\n }\n }\n return items;\n}\n\nexport function tableVirtualOverscanPx(viewportSize: number): number {\n const min = TABLE_VIRTUAL_MIN_OVERSCAN_ROWS * TABLE_VIRTUAL_ROW_TRACK_SIZE[1];\n return Math.max(0, viewportSize / 2, min);\n}\n\nexport function buildTableVirtualPrefixSums(sizes: readonly number[]): number[] {\n const prefix = new Array<number>(sizes.length + 1);\n prefix[0] = 0;\n for (let index = 0; index < sizes.length; index += 1) {\n prefix[index + 1] = prefix[index] + Math.max(0, sizes[index] ?? 0);\n }\n return prefix;\n}\n\n/** Largest item index whose start offset is <= `offset`. */\nexport function findTableVirtualIndexAtOffset(prefix: readonly number[], offset: number): number {\n const lastIndex = prefix.length - 2;\n if (lastIndex < 0) return 0;\n const target = Math.max(0, offset);\n let low = 0;\n let high = lastIndex;\n while (low < high) {\n const mid = (low + high + 1) >> 1;\n if ((prefix[mid] ?? 0) <= target) low = mid;\n else high = mid - 1;\n }\n return low;\n}\n\nexport function tableVirtualItemSizes(\n items: readonly TableVirtualItem[],\n measures: ReadonlyMap<string, number>\n): number[] {\n return items.map(item => measures.get(item.id) ?? item.estimatedSize);\n}\n\nexport function sameTableVirtualPlan(\n left: TableVirtualPlan | null | undefined,\n right: TableVirtualPlan | null | undefined\n): boolean {\n if (left === right) return true;\n if (!left || !right) return false;\n if (\n left.start !== right.start ||\n left.end !== right.end ||\n left.totalSize !== right.totalSize ||\n left.itemCount !== right.itemCount ||\n left.mountedIndexes.size !== right.mountedIndexes.size\n ) {\n return false;\n }\n for (const index of left.mountedIndexes) {\n if (!right.mountedIndexes.has(index)) return false;\n }\n return sameVirtualNodes(left.nodes, right.nodes);\n}\n\nfunction sameVirtualNodes(\n left: readonly TableVirtualNode[],\n right: readonly TableVirtualNode[]\n): boolean {\n if (left.length !== right.length) return false;\n for (let index = 0; index < left.length; index += 1) {\n const a = left[index];\n const b = right[index];\n if (!a || !b || a.kind !== b.kind) return false;\n if (a.kind === 'spacer' && b.kind === 'spacer') {\n if (a.size !== b.size || a.key !== b.key) return false;\n continue;\n }\n if (a.kind === 'row' && b.kind === 'row') {\n if (a.index !== b.index || a.item.id !== b.item.id) return false;\n continue;\n }\n if (a.kind === 'group' && b.kind === 'group') {\n if (a.groupId !== b.groupId || a.headerIndex !== b.headerIndex) return false;\n if (!sameVirtualNodes(a.nodes, b.nodes)) return false;\n }\n }\n return true;\n}\n\n/** Precompute dataset-wide lookup state. Rebuild only when items or their sizes change. */\nexport function createTableVirtualIndex(\n items: readonly TableVirtualItem[],\n sizes: readonly number[]\n): TableVirtualIndex {\n const prefix = buildTableVirtualPrefixSums(sizes);\n const itemCount = items.length;\n const groupHeaderIndexes: number[] = [];\n const headerIndexByGroup = new Map<string, number>();\n const groupEndByHeader = new Map<number, number>();\n const rowIndexById = new Map<string, number>();\n const itemIndexById = new Map<string, number>();\n let activeHeaderIndex: number | null = null;\n let hasVariableSize = false;\n\n for (let itemIndex = 0; itemIndex < itemCount; itemIndex += 1) {\n const item = items[itemIndex];\n if (item) {\n itemIndexById.set(item.id, itemIndex);\n hasVariableSize ||= item.variableSize;\n }\n if (item?.kind === 'group' && item.groupId) {\n if (activeHeaderIndex != null) groupEndByHeader.set(activeHeaderIndex, itemIndex);\n activeHeaderIndex = itemIndex;\n groupHeaderIndexes.push(itemIndex);\n headerIndexByGroup.set(item.groupId, itemIndex);\n } else if (item?.kind === 'row' && item.rowId) {\n rowIndexById.set(item.rowId, itemIndex);\n }\n }\n if (activeHeaderIndex != null) groupEndByHeader.set(activeHeaderIndex, itemCount);\n\n return {\n items,\n sizes,\n prefix,\n itemCount,\n totalSize: prefix[itemCount] ?? 0,\n hasVariableSize,\n grouped: groupHeaderIndexes.length > 0,\n groupHeaderIndexes,\n headerIndexByGroup,\n groupEndByHeader,\n rowIndexById,\n itemIndexById,\n };\n}\n\n/** Resolve the mounted slice from cached dataset-wide lookup state. */\nexport function resolveTableVirtualPlanFromIndex(\n index: TableVirtualIndex,\n input: ResolveTableVirtualIndexedPlanInput\n): TableVirtualPlan {\n const { items, prefix, itemCount, totalSize } = index;\n const empty: TableVirtualPlan = {\n totalSize,\n itemCount,\n start: 0,\n end: 0,\n nodes: totalSize > 0 ? [{ kind: 'spacer', size: totalSize, key: 'pad-empty' }] : [],\n mountedIds: new Set(),\n mountedIndexes: new Set(),\n };\n if (itemCount === 0) return empty;\n\n const viewportSize = Math.max(0, input.viewportSize);\n const overscan = tableVirtualOverscanPx(viewportSize);\n const trailingOverscan = (TABLE_VIRTUAL_MIN_OVERSCAN_ROWS * TABLE_VIRTUAL_ROW_TRACK_SIZE[1]) / 4;\n const before = input.scrollDirection === 'forward' ? trailingOverscan : overscan;\n const after = input.scrollDirection === 'backward' ? trailingOverscan : overscan;\n const scrollOffset = Math.max(0, Math.min(input.scrollOffset, Math.max(0, totalSize)));\n const rangeStart = Math.max(0, scrollOffset - before);\n const rangeEnd = scrollOffset + viewportSize + after;\n const start = findTableVirtualIndexAtOffset(prefix, rangeStart);\n let end = start;\n while (end < itemCount && (prefix[end] ?? 0) < rangeEnd) end += 1;\n if (end === start) end = Math.min(itemCount, start + 1);\n\n const mounted = new Set<number>();\n for (let itemIndex = start; itemIndex < end; itemIndex += 1) mounted.add(itemIndex);\n\n const pinHeader = (groupId: string | undefined) => {\n if (!groupId) return;\n const headerIndex = index.headerIndexByGroup.get(groupId);\n if (headerIndex != null) mounted.add(headerIndex);\n };\n for (const mountedIndex of [...mounted]) pinHeader(items[mountedIndex]?.groupId);\n\n const nextHeader = firstTableVirtualIndexAfter(index.groupHeaderIndexes, end - 1);\n if (nextHeader != null) mounted.add(nextHeader);\n\n if (input.pinnedRowIds) {\n for (const rowId of input.pinnedRowIds) {\n const rowIndex = index.rowIndexById.get(rowId);\n if (rowIndex == null) continue;\n mounted.add(rowIndex);\n pinHeader(items[rowIndex]?.groupId);\n }\n }\n\n const mountedSorted = [...mounted].sort((left, right) => left - right);\n const nodes = index.grouped\n ? buildGroupedVirtualNodes(index, mounted, mountedSorted)\n : buildFlatVirtualNodes(index, mountedSorted);\n const mountedIds = new Set<string>();\n for (const mountedIndex of mounted) {\n const id = items[mountedIndex]?.id;\n if (id) mountedIds.add(id);\n }\n\n return {\n totalSize,\n itemCount,\n start: mountedSorted[0] ?? start,\n end: mountedSorted.length > 0 ? mountedSorted[mountedSorted.length - 1]! + 1 : end,\n nodes,\n mountedIds,\n mountedIndexes: mounted,\n };\n}\n\n/** Resolve a one-off plan. Controllers retain and reuse a prepared index. */\nexport function resolveTableVirtualPlan(input: ResolveTableVirtualPlanInput): TableVirtualPlan {\n return resolveTableVirtualPlanFromIndex(createTableVirtualIndex(input.items, input.sizes), input);\n}\n\nfunction firstTableVirtualIndexAfter(\n indexes: readonly number[],\n target: number\n): number | undefined {\n let low = 0;\n let high = indexes.length;\n while (low < high) {\n const middle = (low + high) >> 1;\n if ((indexes[middle] ?? 0) <= target) low = middle + 1;\n else high = middle;\n }\n return indexes[low];\n}\n\nfunction buildFlatVirtualNodes(\n index: TableVirtualIndex,\n mounted: readonly number[]\n): TableVirtualNode[] {\n const { items, prefix, itemCount } = index;\n const nodes: TableVirtualNode[] = [];\n let cursor = 0;\n for (const mountedIndex of mounted) {\n const item = items[mountedIndex];\n if (!item) continue;\n const spacerSize = (prefix[mountedIndex] ?? 0) - (prefix[cursor] ?? 0);\n if (spacerSize > 0) {\n nodes.push({ kind: 'spacer', size: spacerSize, key: `pad-${cursor}-${mountedIndex}` });\n }\n nodes.push({ kind: 'row', index: mountedIndex, item });\n cursor = mountedIndex + 1;\n }\n const trailingSize = (prefix[itemCount] ?? 0) - (prefix[cursor] ?? 0);\n if (trailingSize > 0) {\n nodes.push({ kind: 'spacer', size: trailingSize, key: `pad-${cursor}-${itemCount}` });\n }\n return nodes;\n}\n\nfunction buildGroupedVirtualNodes(\n index: TableVirtualIndex,\n mounted: ReadonlySet<number>,\n mountedSorted: readonly number[]\n): TableVirtualNode[] {\n const { items, prefix, itemCount } = index;\n const nodes: TableVirtualNode[] = [];\n const selectedHeaders = new Set<number>();\n for (const mountedIndex of mountedSorted) {\n const item = items[mountedIndex];\n const headerIndex =\n item?.kind === 'group'\n ? mountedIndex\n : item?.groupId\n ? index.headerIndexByGroup.get(item.groupId)\n : undefined;\n if (headerIndex != null) selectedHeaders.add(headerIndex);\n }\n\n let cursor = 0;\n for (const headerIndex of [...selectedHeaders].sort((left, right) => left - right)) {\n const header = items[headerIndex];\n if (header?.kind !== 'group' || !header.groupId) continue;\n const groupId = header.groupId;\n const groupEnd = index.groupEndByHeader.get(headerIndex) ?? headerIndex + 1;\n const leadingSize = (prefix[headerIndex] ?? 0) - (prefix[cursor] ?? 0);\n if (leadingSize > 0) {\n nodes.push({ kind: 'spacer', size: leadingSize, key: `pad-${cursor}-${headerIndex}` });\n }\n\n const groupNodes: Array<Extract<TableVirtualNode, { kind: 'spacer' | 'row' }>> = [];\n const mountedMembers = mountedSorted.filter(\n mountedIndex => mountedIndex > headerIndex && mountedIndex < groupEnd\n );\n let memberCursor = headerIndex + 1;\n for (const memberIndex of mountedMembers) {\n const item = items[memberIndex];\n const spacerSize = (prefix[memberIndex] ?? 0) - (prefix[memberCursor] ?? 0);\n if (spacerSize > 0) {\n groupNodes.push({\n kind: 'spacer',\n size: spacerSize,\n key: `group-${groupId}-pad-${memberCursor}-${memberIndex}`,\n });\n }\n if (item?.kind === 'row' && mounted.has(memberIndex)) {\n groupNodes.push({ kind: 'row', index: memberIndex, item });\n }\n memberCursor = memberIndex + 1;\n }\n const trailingSize = (prefix[groupEnd] ?? 0) - (prefix[memberCursor] ?? 0);\n if (trailingSize > 0) {\n groupNodes.push({\n kind: 'spacer',\n size: trailingSize,\n key: `group-${groupId}-pad-${memberCursor}-${groupEnd}`,\n });\n }\n\n nodes.push({ kind: 'group', groupId, headerIndex, nodes: groupNodes });\n cursor = groupEnd;\n }\n\n const trailingSize = (prefix[itemCount] ?? 0) - (prefix[cursor] ?? 0);\n if (trailingSize > 0) {\n nodes.push({ kind: 'spacer', size: trailingSize, key: `pad-${cursor}-${itemCount}` });\n }\n return nodes;\n}\n",
1539
+ "content": "import { resolveTableCellPresentation } from './table-cell-model';\nimport { tableGroupAccessories } from './table-model';\nimport type { TableColumn, DataGroup, TableRow } from './table-types';\n\n/**\n * Token-aligned body-row track contracts: 40 / 64 / 88, plus wrapping-secondary\n * 112. Every track is 24px with no stack gap. Estimates only; measured heights\n * replace these after paint.\n */\nexport const TABLE_VIRTUAL_ROW_TRACK_SIZE = {\n 1: 40,\n 2: 64,\n 3: 88,\n 4: 112,\n} as const;\n\nconst TABLE_VIRTUAL_LATER_TRACK_INCREMENT =\n TABLE_VIRTUAL_ROW_TRACK_SIZE[2] - TABLE_VIRTUAL_ROW_TRACK_SIZE[1];\n\n/** Estimated row height for any positive named track count. */\nexport function tableVirtualRowTrackSize(tracks: number): number {\n const count = Number.isFinite(tracks) && tracks > 0 ? Math.floor(tracks) : 1;\n return TABLE_VIRTUAL_ROW_TRACK_SIZE[1] + (count - 1) * TABLE_VIRTUAL_LATER_TRACK_INCREMENT;\n}\n\nexport const TABLE_VIRTUAL_GROUP_HEADER_SIZE = TABLE_VIRTUAL_ROW_TRACK_SIZE[1];\nexport const TABLE_VIRTUAL_MIN_OVERSCAN_ROWS = 8;\n\nexport const TABLE_VIRTUAL_VIEWPORT_REQUIRED_HEADING = 'Bounded height required';\nexport const TABLE_VIRTUAL_VIEWPORT_REQUIRED_BODY =\n 'Virtual mode requires height, maxHeight, or fitViewport.';\n\nexport const TABLE_VIRTUAL_TOTAL_SUMMARY_LABEL = '{total} items';\n\nexport type TableVirtualItemKind = 'group' | 'row';\n\nexport interface TableVirtualItem {\n kind: TableVirtualItemKind;\n id: string;\n groupId?: string;\n rowId?: string;\n estimatedSize: number;\n variableSize: boolean;\n}\n\nexport type TableVirtualNode =\n | { kind: 'spacer'; size: number; key: string }\n | { kind: 'row'; index: number; item: TableVirtualItem }\n | {\n kind: 'group';\n groupId: string;\n headerIndex: number;\n nodes: Array<Extract<TableVirtualNode, { kind: 'spacer' | 'row' }>>;\n };\n\nexport interface TableVirtualPlan {\n totalSize: number;\n itemCount: number;\n start: number;\n end: number;\n nodes: TableVirtualNode[];\n mountedIds: ReadonlySet<string>;\n mountedIndexes: ReadonlySet<number>;\n}\n\nexport interface FlattenTableVirtualItemsInput {\n grouped: boolean;\n rows: TableRow[];\n groups: DataGroup[];\n collapsedGroupIds: ReadonlySet<string> | readonly string[];\n columns: TableColumn[];\n}\n\nexport interface ResolveTableVirtualPlanInput {\n items: readonly TableVirtualItem[];\n sizes: readonly number[];\n scrollOffset: number;\n viewportSize: number;\n pinnedRowIds?: ReadonlySet<string>;\n}\n\nexport interface TableVirtualIndex {\n items: readonly TableVirtualItem[];\n sizes: readonly number[];\n prefix: readonly number[];\n itemCount: number;\n totalSize: number;\n hasVariableSize: boolean;\n grouped: boolean;\n groupHeaderIndexes: readonly number[];\n headerIndexByGroup: ReadonlyMap<string, number>;\n groupEndByHeader: ReadonlyMap<number, number>;\n rowIndexById: ReadonlyMap<string, number>;\n itemIndexById: ReadonlyMap<string, number>;\n}\n\nexport interface ResolveTableVirtualIndexedPlanInput {\n scrollOffset: number;\n viewportSize: number;\n scrollDirection?: 'backward' | 'forward' | 'none';\n pinnedRowIds?: ReadonlySet<string>;\n}\n\n/** Map a cell recipe onto the named track stack used for first-paint estimates. */\nexport function estimateTableCellTrackCount(\n value: TableRow['cells'][string],\n column: TableColumn\n): number {\n return tableCellTrackCount(resolveTableCellPresentation(value, column));\n}\n\nfunction tableCellTrackCount(\n presentation: ReturnType<typeof resolveTableCellPresentation>\n): number {\n if (presentation.kind === 'image') {\n return presentation.variant === 'triple' ? 3 : presentation.variant === 'multi' ? 2 : 1;\n }\n if (presentation.kind === 'tag') {\n return presentation.variant === 'text-with-tag' ? 2 : 1;\n }\n if (presentation.kind === 'tags') return presentation.tracks;\n if (\n presentation.kind !== 'text' &&\n presentation.kind !== 'icon-text' &&\n presentation.kind !== 'score-text'\n )\n return 1;\n\n const variantTracks =\n presentation.variant === 'triple'\n ? 3\n : presentation.variant === 'multi' || presentation.variant === 'primary-pair'\n ? 2\n : 1;\n if (!presentation.wraps) return variantTracks;\n if (presentation.lineClamp === 'none') {\n return presentation.kind === 'text' && !presentation.singleLine ? 4 : 3;\n }\n if (presentation.lineClamp === 3 && variantTracks >= 2) return 4;\n return Math.max(variantTracks, presentation.lineClamp);\n}\n\n/** Tallest named track stack across the row's visible cells. */\nexport function estimateTableRowBlockSize(row: TableRow, columns: readonly TableColumn[]): number {\n return tableVirtualRowMetrics(row, columns).estimatedSize;\n}\n\nfunction tableVirtualRowMetrics(\n row: TableRow,\n columns: readonly TableColumn[]\n): Pick<TableVirtualItem, 'estimatedSize' | 'variableSize'> {\n let tracks = 1;\n let variableSize = false;\n for (const column of columns) {\n const value = row.cells[column.id];\n const presentation = resolveTableCellPresentation(value, column);\n if (\n (presentation.kind === 'text' ||\n presentation.kind === 'icon-text' ||\n presentation.kind === 'score-text') &&\n presentation.wraps &&\n presentation.lineClamp === 'none'\n ) {\n variableSize = true;\n }\n if (presentation.kind === 'tags') variableSize = true;\n const count = tableCellTrackCount(presentation);\n if (count > tracks) tracks = count;\n }\n return { estimatedSize: tableVirtualRowTrackSize(tracks), variableSize };\n}\n\nexport function flattenTableVirtualItems(input: FlattenTableVirtualItemsInput): TableVirtualItem[] {\n const collapsed =\n input.collapsedGroupIds instanceof Set\n ? input.collapsedGroupIds\n : new Set(input.collapsedGroupIds);\n if (!input.grouped) {\n return input.rows.map(row => {\n const metrics = tableVirtualRowMetrics(row, input.columns);\n return {\n kind: 'row' as const,\n id: `row:${row.id}`,\n rowId: row.id,\n ...metrics,\n };\n });\n }\n\n const items: TableVirtualItem[] = [];\n for (const group of input.groups) {\n items.push({\n kind: 'group',\n id: `group:${group.id}`,\n groupId: group.id,\n estimatedSize: tableVirtualRowTrackSize(tableGroupAccessories(group).length > 0 ? 2 : 1),\n variableSize: false,\n });\n if (collapsed.has(group.id)) continue;\n for (const row of group.rows) {\n const metrics = tableVirtualRowMetrics(row, input.columns);\n items.push({\n kind: 'row',\n id: `row:${row.id}`,\n rowId: row.id,\n groupId: group.id,\n ...metrics,\n });\n }\n }\n return items;\n}\n\nexport function tableVirtualOverscanPx(viewportSize: number): number {\n const min = TABLE_VIRTUAL_MIN_OVERSCAN_ROWS * TABLE_VIRTUAL_ROW_TRACK_SIZE[1];\n return Math.max(0, viewportSize / 2, min);\n}\n\nexport function buildTableVirtualPrefixSums(sizes: readonly number[]): number[] {\n const prefix = new Array<number>(sizes.length + 1);\n prefix[0] = 0;\n for (let index = 0; index < sizes.length; index += 1) {\n prefix[index + 1] = prefix[index] + Math.max(0, sizes[index] ?? 0);\n }\n return prefix;\n}\n\n/** Largest item index whose start offset is <= `offset`. */\nexport function findTableVirtualIndexAtOffset(prefix: readonly number[], offset: number): number {\n const lastIndex = prefix.length - 2;\n if (lastIndex < 0) return 0;\n const target = Math.max(0, offset);\n let low = 0;\n let high = lastIndex;\n while (low < high) {\n const mid = (low + high + 1) >> 1;\n if ((prefix[mid] ?? 0) <= target) low = mid;\n else high = mid - 1;\n }\n return low;\n}\n\nexport function tableVirtualItemSizes(\n items: readonly TableVirtualItem[],\n measures: ReadonlyMap<string, number>\n): number[] {\n return items.map(item => measures.get(item.id) ?? item.estimatedSize);\n}\n\nexport function sameTableVirtualPlan(\n left: TableVirtualPlan | null | undefined,\n right: TableVirtualPlan | null | undefined\n): boolean {\n if (left === right) return true;\n if (!left || !right) return false;\n if (\n left.start !== right.start ||\n left.end !== right.end ||\n left.totalSize !== right.totalSize ||\n left.itemCount !== right.itemCount ||\n left.mountedIndexes.size !== right.mountedIndexes.size\n ) {\n return false;\n }\n for (const index of left.mountedIndexes) {\n if (!right.mountedIndexes.has(index)) return false;\n }\n return sameVirtualNodes(left.nodes, right.nodes);\n}\n\nfunction sameVirtualNodes(\n left: readonly TableVirtualNode[],\n right: readonly TableVirtualNode[]\n): boolean {\n if (left.length !== right.length) return false;\n for (let index = 0; index < left.length; index += 1) {\n const a = left[index];\n const b = right[index];\n if (!a || !b || a.kind !== b.kind) return false;\n if (a.kind === 'spacer' && b.kind === 'spacer') {\n if (a.size !== b.size || a.key !== b.key) return false;\n continue;\n }\n if (a.kind === 'row' && b.kind === 'row') {\n if (a.index !== b.index || a.item.id !== b.item.id) return false;\n continue;\n }\n if (a.kind === 'group' && b.kind === 'group') {\n if (a.groupId !== b.groupId || a.headerIndex !== b.headerIndex) return false;\n if (!sameVirtualNodes(a.nodes, b.nodes)) return false;\n }\n }\n return true;\n}\n\n/** Precompute dataset-wide lookup state. Rebuild only when items or their sizes change. */\nexport function createTableVirtualIndex(\n items: readonly TableVirtualItem[],\n sizes: readonly number[]\n): TableVirtualIndex {\n const prefix = buildTableVirtualPrefixSums(sizes);\n const itemCount = items.length;\n const groupHeaderIndexes: number[] = [];\n const headerIndexByGroup = new Map<string, number>();\n const groupEndByHeader = new Map<number, number>();\n const rowIndexById = new Map<string, number>();\n const itemIndexById = new Map<string, number>();\n let activeHeaderIndex: number | null = null;\n let hasVariableSize = false;\n\n for (let itemIndex = 0; itemIndex < itemCount; itemIndex += 1) {\n const item = items[itemIndex];\n if (item) {\n itemIndexById.set(item.id, itemIndex);\n hasVariableSize ||= item.variableSize;\n }\n if (item?.kind === 'group' && item.groupId) {\n if (activeHeaderIndex != null) groupEndByHeader.set(activeHeaderIndex, itemIndex);\n activeHeaderIndex = itemIndex;\n groupHeaderIndexes.push(itemIndex);\n headerIndexByGroup.set(item.groupId, itemIndex);\n } else if (item?.kind === 'row' && item.rowId) {\n rowIndexById.set(item.rowId, itemIndex);\n }\n }\n if (activeHeaderIndex != null) groupEndByHeader.set(activeHeaderIndex, itemCount);\n\n return {\n items,\n sizes,\n prefix,\n itemCount,\n totalSize: prefix[itemCount] ?? 0,\n hasVariableSize,\n grouped: groupHeaderIndexes.length > 0,\n groupHeaderIndexes,\n headerIndexByGroup,\n groupEndByHeader,\n rowIndexById,\n itemIndexById,\n };\n}\n\n/** Resolve the mounted slice from cached dataset-wide lookup state. */\nexport function resolveTableVirtualPlanFromIndex(\n index: TableVirtualIndex,\n input: ResolveTableVirtualIndexedPlanInput\n): TableVirtualPlan {\n const { items, prefix, itemCount, totalSize } = index;\n const empty: TableVirtualPlan = {\n totalSize,\n itemCount,\n start: 0,\n end: 0,\n nodes: totalSize > 0 ? [{ kind: 'spacer', size: totalSize, key: 'pad-empty' }] : [],\n mountedIds: new Set(),\n mountedIndexes: new Set(),\n };\n if (itemCount === 0) return empty;\n\n const viewportSize = Math.max(0, input.viewportSize);\n const overscan = tableVirtualOverscanPx(viewportSize);\n const trailingOverscan = (TABLE_VIRTUAL_MIN_OVERSCAN_ROWS * TABLE_VIRTUAL_ROW_TRACK_SIZE[1]) / 4;\n const before = input.scrollDirection === 'forward' ? trailingOverscan : overscan;\n const after = input.scrollDirection === 'backward' ? trailingOverscan : overscan;\n const scrollOffset = Math.max(0, Math.min(input.scrollOffset, Math.max(0, totalSize)));\n const rangeStart = Math.max(0, scrollOffset - before);\n const rangeEnd = scrollOffset + viewportSize + after;\n const start = findTableVirtualIndexAtOffset(prefix, rangeStart);\n let end = start;\n while (end < itemCount && (prefix[end] ?? 0) < rangeEnd) end += 1;\n if (end === start) end = Math.min(itemCount, start + 1);\n\n const mounted = new Set<number>();\n for (let itemIndex = start; itemIndex < end; itemIndex += 1) mounted.add(itemIndex);\n\n const pinHeader = (groupId: string | undefined) => {\n if (!groupId) return;\n const headerIndex = index.headerIndexByGroup.get(groupId);\n if (headerIndex != null) mounted.add(headerIndex);\n };\n for (const mountedIndex of [...mounted]) pinHeader(items[mountedIndex]?.groupId);\n\n const nextHeader = firstTableVirtualIndexAfter(index.groupHeaderIndexes, end - 1);\n if (nextHeader != null) mounted.add(nextHeader);\n\n if (input.pinnedRowIds) {\n for (const rowId of input.pinnedRowIds) {\n const rowIndex = index.rowIndexById.get(rowId);\n if (rowIndex == null) continue;\n mounted.add(rowIndex);\n pinHeader(items[rowIndex]?.groupId);\n }\n }\n\n const mountedSorted = [...mounted].sort((left, right) => left - right);\n const nodes = index.grouped\n ? buildGroupedVirtualNodes(index, mounted, mountedSorted)\n : buildFlatVirtualNodes(index, mountedSorted);\n const mountedIds = new Set<string>();\n for (const mountedIndex of mounted) {\n const id = items[mountedIndex]?.id;\n if (id) mountedIds.add(id);\n }\n\n return {\n totalSize,\n itemCount,\n start: mountedSorted[0] ?? start,\n end: mountedSorted.length > 0 ? mountedSorted[mountedSorted.length - 1]! + 1 : end,\n nodes,\n mountedIds,\n mountedIndexes: mounted,\n };\n}\n\n/** Resolve a one-off plan. Controllers retain and reuse a prepared index. */\nexport function resolveTableVirtualPlan(input: ResolveTableVirtualPlanInput): TableVirtualPlan {\n return resolveTableVirtualPlanFromIndex(createTableVirtualIndex(input.items, input.sizes), input);\n}\n\nfunction firstTableVirtualIndexAfter(\n indexes: readonly number[],\n target: number\n): number | undefined {\n let low = 0;\n let high = indexes.length;\n while (low < high) {\n const middle = (low + high) >> 1;\n if ((indexes[middle] ?? 0) <= target) low = middle + 1;\n else high = middle;\n }\n return indexes[low];\n}\n\nfunction buildFlatVirtualNodes(\n index: TableVirtualIndex,\n mounted: readonly number[]\n): TableVirtualNode[] {\n const { items, prefix, itemCount } = index;\n const nodes: TableVirtualNode[] = [];\n let cursor = 0;\n for (const mountedIndex of mounted) {\n const item = items[mountedIndex];\n if (!item) continue;\n const spacerSize = (prefix[mountedIndex] ?? 0) - (prefix[cursor] ?? 0);\n if (spacerSize > 0) {\n nodes.push({ kind: 'spacer', size: spacerSize, key: `pad-${cursor}-${mountedIndex}` });\n }\n nodes.push({ kind: 'row', index: mountedIndex, item });\n cursor = mountedIndex + 1;\n }\n const trailingSize = (prefix[itemCount] ?? 0) - (prefix[cursor] ?? 0);\n if (trailingSize > 0) {\n nodes.push({ kind: 'spacer', size: trailingSize, key: `pad-${cursor}-${itemCount}` });\n }\n return nodes;\n}\n\nfunction buildGroupedVirtualNodes(\n index: TableVirtualIndex,\n mounted: ReadonlySet<number>,\n mountedSorted: readonly number[]\n): TableVirtualNode[] {\n const { items, prefix, itemCount } = index;\n const nodes: TableVirtualNode[] = [];\n const selectedHeaders = new Set<number>();\n for (const mountedIndex of mountedSorted) {\n const item = items[mountedIndex];\n const headerIndex =\n item?.kind === 'group'\n ? mountedIndex\n : item?.groupId\n ? index.headerIndexByGroup.get(item.groupId)\n : undefined;\n if (headerIndex != null) selectedHeaders.add(headerIndex);\n }\n\n let cursor = 0;\n for (const headerIndex of [...selectedHeaders].sort((left, right) => left - right)) {\n const header = items[headerIndex];\n if (header?.kind !== 'group' || !header.groupId) continue;\n const groupId = header.groupId;\n const groupEnd = index.groupEndByHeader.get(headerIndex) ?? headerIndex + 1;\n const leadingSize = (prefix[headerIndex] ?? 0) - (prefix[cursor] ?? 0);\n if (leadingSize > 0) {\n nodes.push({ kind: 'spacer', size: leadingSize, key: `pad-${cursor}-${headerIndex}` });\n }\n\n const groupNodes: Array<Extract<TableVirtualNode, { kind: 'spacer' | 'row' }>> = [];\n const mountedMembers = mountedSorted.filter(\n mountedIndex => mountedIndex > headerIndex && mountedIndex < groupEnd\n );\n let memberCursor = headerIndex + 1;\n for (const memberIndex of mountedMembers) {\n const item = items[memberIndex];\n const spacerSize = (prefix[memberIndex] ?? 0) - (prefix[memberCursor] ?? 0);\n if (spacerSize > 0) {\n groupNodes.push({\n kind: 'spacer',\n size: spacerSize,\n key: `group-${groupId}-pad-${memberCursor}-${memberIndex}`,\n });\n }\n if (item?.kind === 'row' && mounted.has(memberIndex)) {\n groupNodes.push({ kind: 'row', index: memberIndex, item });\n }\n memberCursor = memberIndex + 1;\n }\n const trailingSize = (prefix[groupEnd] ?? 0) - (prefix[memberCursor] ?? 0);\n if (trailingSize > 0) {\n groupNodes.push({\n kind: 'spacer',\n size: trailingSize,\n key: `group-${groupId}-pad-${memberCursor}-${groupEnd}`,\n });\n }\n\n nodes.push({ kind: 'group', groupId, headerIndex, nodes: groupNodes });\n cursor = groupEnd;\n }\n\n const trailingSize = (prefix[itemCount] ?? 0) - (prefix[cursor] ?? 0);\n if (trailingSize > 0) {\n nodes.push({ kind: 'spacer', size: trailingSize, key: `pad-${cursor}-${itemCount}` });\n }\n return nodes;\n}\n",
1540
1540
  "type": "registry:ui"
1541
1541
  }
1542
1542
  ]