@ds-mo/ui 14.19.0 → 14.20.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 (363) hide show
  1. package/dist/.package-ready.json +1 -1
  2. package/dist/agent-patterns.json +1 -1
  3. package/dist/agent.json +24 -8
  4. package/dist/angular/ds-filter-menu.d.ts +1 -1
  5. package/dist/angular/ds-filter-menu.js +3 -3
  6. package/dist/components/ds-agent-activity.js +1 -1
  7. package/dist/components/ds-agent-questionnaire.js +1 -1
  8. package/dist/components/ds-agent-response.js +1 -1
  9. package/dist/components/ds-agent-source-list.js +1 -1
  10. package/dist/components/ds-agent-tool-call.js +1 -1
  11. package/dist/components/ds-attachment-list.js +1 -1
  12. package/dist/components/ds-avatar.js +1 -1
  13. package/dist/components/ds-badge.js +1 -1
  14. package/dist/components/ds-banner.js +1 -1
  15. package/dist/components/ds-banner.js.map +1 -1
  16. package/dist/components/ds-bar-action.js +1 -1
  17. package/dist/components/ds-bar-action.js.map +1 -1
  18. package/dist/components/ds-bar-nav.js +1 -1
  19. package/dist/components/ds-bar-title.js +1 -1
  20. package/dist/components/ds-bar-workflow.js +1 -1
  21. package/dist/components/ds-bar-workflow.js.map +1 -1
  22. package/dist/components/ds-breadcrumb.js +1 -1
  23. package/dist/components/ds-button-filled.js +1 -1
  24. package/dist/components/ds-button-unfilled.js +1 -1
  25. package/dist/components/ds-card-chart.js +1 -1
  26. package/dist/components/ds-card-chart.js.map +1 -1
  27. package/dist/components/ds-card-overview.js +1 -1
  28. package/dist/components/ds-card-overview.js.map +1 -1
  29. package/dist/components/ds-card-setting.js +1 -1
  30. package/dist/components/ds-card-setting.js.map +1 -1
  31. package/dist/components/ds-chart-legend.js +1 -1
  32. package/dist/components/ds-chart-legend.js.map +1 -1
  33. package/dist/components/ds-chart.js +1 -1
  34. package/dist/components/ds-chart.js.map +1 -1
  35. package/dist/components/ds-checkbox.js +1 -1
  36. package/dist/components/ds-chip.js +1 -1
  37. package/dist/components/ds-chip.js.map +1 -1
  38. package/dist/components/ds-code-block.js +1 -1
  39. package/dist/components/ds-conversation-list-item.js +1 -1
  40. package/dist/components/ds-conversation-list-item.js.map +1 -1
  41. package/dist/components/ds-conversation-list-section.js +1 -1
  42. package/dist/components/ds-conversation-list.js +1 -1
  43. package/dist/components/ds-conversation-list.js.map +1 -1
  44. package/dist/components/ds-divider.js +1 -1
  45. package/dist/components/ds-empty-state.js +1 -1
  46. package/dist/components/ds-field.js +1 -1
  47. package/dist/components/ds-filter-menu.js +1 -1
  48. package/dist/components/ds-icon.js +1 -1
  49. package/dist/components/ds-input.js +1 -1
  50. package/dist/components/ds-loader.js +1 -1
  51. package/dist/components/ds-markdown.js +1 -1
  52. package/dist/components/ds-menu.js +1 -1
  53. package/dist/components/ds-message-actions.js +1 -1
  54. package/dist/components/ds-message-bubble.js +1 -1
  55. package/dist/components/ds-message-bubble.js.map +1 -1
  56. package/dist/components/ds-message-composer.js +1 -1
  57. package/dist/components/ds-message-composer.js.map +1 -1
  58. package/dist/components/ds-message-scroller.js +1 -1
  59. package/dist/components/ds-message-scroller.js.map +1 -1
  60. package/dist/components/ds-message.js +1 -1
  61. package/dist/components/ds-mobile-bar-nav.js +1 -1
  62. package/dist/components/ds-mobile-header.js +1 -1
  63. package/dist/components/ds-mobile-section-switcher.js +1 -1
  64. package/dist/components/ds-mobile-sheet-nav.js +1 -1
  65. package/dist/components/ds-modal.js +1 -1
  66. package/dist/components/ds-pagination.js +1 -1
  67. package/dist/components/ds-panel-nav.js +1 -1
  68. package/dist/components/ds-panel-sub-nav.js +1 -1
  69. package/dist/components/ds-panel-sub-nav.js.map +1 -1
  70. package/dist/components/ds-panel-tool-header.js +1 -1
  71. package/dist/components/ds-panel-tool-search.js +1 -1
  72. package/dist/components/ds-panel-tool-search.js.map +1 -1
  73. package/dist/components/ds-panel-tools.js +1 -1
  74. package/dist/components/ds-radio.js +1 -1
  75. package/dist/components/ds-scroll-overlay.js +1 -1
  76. package/dist/components/ds-scroll-overlay.js.map +1 -1
  77. package/dist/components/ds-select.js +1 -1
  78. package/dist/components/ds-shell-app.js +1 -1
  79. package/dist/components/ds-shell-app.js.map +1 -1
  80. package/dist/components/ds-shell-page.js +1 -1
  81. package/dist/components/ds-shell-tools.js +1 -1
  82. package/dist/components/ds-skeleton.js +1 -1
  83. package/dist/components/ds-slider.js +1 -1
  84. package/dist/components/ds-slider.js.map +1 -1
  85. package/dist/components/ds-swatch-picker.js +1 -1
  86. package/dist/components/ds-switch.js +1 -1
  87. package/dist/components/ds-tab-group.js +1 -1
  88. package/dist/components/ds-table-filter.js +1 -1
  89. package/dist/components/ds-table-filter.js.map +1 -1
  90. package/dist/components/ds-table-saved-views.js +1 -1
  91. package/dist/components/ds-table-sort.js +1 -1
  92. package/dist/components/ds-table-sort.js.map +1 -1
  93. package/dist/components/ds-table-toolbar.js +1 -1
  94. package/dist/components/ds-table.js +1 -1
  95. package/dist/components/ds-table.js.map +1 -1
  96. package/dist/components/ds-tag.js +1 -1
  97. package/dist/components/ds-text.js +1 -1
  98. package/dist/components/ds-toast.js +1 -1
  99. package/dist/components/ds-toast.js.map +1 -1
  100. package/dist/components/ds-tooltip-chart.js +1 -1
  101. package/dist/components/ds-tooltip.js +1 -1
  102. package/dist/components/ds-typing-indicator.js +1 -1
  103. package/dist/components/p-0XTyQNRG.js +2 -0
  104. package/dist/components/{p-JPTkZ3Ji.js.map → p-0XTyQNRG.js.map} +1 -1
  105. package/dist/components/{p-DaAXY-XS.js → p-1UAVanY9.js} +2 -2
  106. package/dist/components/p-1UAVanY9.js.map +1 -0
  107. package/dist/components/{p-BNiuAMeh.js → p-AtUP95E_.js} +2 -2
  108. package/dist/components/p-AtUP95E_.js.map +1 -0
  109. package/dist/components/p-B-HsfQOK.js +2 -0
  110. package/dist/components/p-B-HsfQOK.js.map +1 -0
  111. package/dist/components/{p-B_jZIC9R.js → p-B2FT4Da6.js} +2 -2
  112. package/dist/components/{p-B_jZIC9R.js.map → p-B2FT4Da6.js.map} +1 -1
  113. package/dist/components/p-B4wTnsUL.js +2 -0
  114. package/dist/components/p-B4wTnsUL.js.map +1 -0
  115. package/dist/components/p-B5-R5mb8.js +2 -0
  116. package/dist/components/p-B5-R5mb8.js.map +1 -0
  117. package/dist/components/p-B6rSDhJY.js +2 -0
  118. package/dist/components/p-B6rSDhJY.js.map +1 -0
  119. package/dist/components/{p-B1JRCDHS.js → p-BHhmB9-4.js} +2 -2
  120. package/dist/components/{p-B1JRCDHS.js.map → p-BHhmB9-4.js.map} +1 -1
  121. package/dist/components/p-BUexfjjg.js +2 -0
  122. package/dist/components/{p-DOUDRJbE.js.map → p-BUexfjjg.js.map} +1 -1
  123. package/dist/components/{p-BBG6Sswb.js → p-BYFS2qWb.js} +2 -2
  124. package/dist/components/{p-BBG6Sswb.js.map → p-BYFS2qWb.js.map} +1 -1
  125. package/dist/components/{p-uI6z53BE.js → p-Bb4KnRy0.js} +2 -2
  126. package/dist/components/{p-HB_cf_aR.js → p-BeA6Jv-3.js} +2 -2
  127. package/dist/components/{p-HB_cf_aR.js.map → p-BeA6Jv-3.js.map} +1 -1
  128. package/dist/components/{p-Bbb9A3NV.js → p-BjRD57w3.js} +2 -2
  129. package/dist/components/p-BjRD57w3.js.map +1 -0
  130. package/dist/components/p-BkelrY4-.js +2 -0
  131. package/dist/components/{p-CwjVgUqy.js.map → p-BkelrY4-.js.map} +1 -1
  132. package/dist/components/{p-r8PMqlqm.js → p-BmssRAEZ.js} +2 -2
  133. package/dist/components/p-BmssRAEZ.js.map +1 -0
  134. package/dist/components/{p-BNkU9dcc.js → p-Bv6FSDPR.js} +2 -2
  135. package/dist/components/{p-BNkU9dcc.js.map → p-Bv6FSDPR.js.map} +1 -1
  136. package/dist/components/{p-D3fMaUJ3.js → p-Bx5BSd6F.js} +2 -2
  137. package/dist/components/p-Bx5BSd6F.js.map +1 -0
  138. package/dist/components/{p-B8q-6a03.js → p-C0qN3PSb.js} +1 -1
  139. package/dist/components/{p-B8q-6a03.js.map → p-C0qN3PSb.js.map} +1 -1
  140. package/dist/components/p-CCmgtju-.js +2 -0
  141. package/dist/components/p-CCmgtju-.js.map +1 -0
  142. package/dist/components/p-CDw_WEVt.js +2 -0
  143. package/dist/components/p-CDw_WEVt.js.map +1 -0
  144. package/dist/components/{p-DFQ9mcbK.js → p-CUL5_8JM.js} +2 -2
  145. package/dist/components/p-CUL5_8JM.js.map +1 -0
  146. package/dist/components/{p-BFfLvMZd.js → p-Cea7rROU.js} +2 -2
  147. package/dist/components/{p-BFfLvMZd.js.map → p-Cea7rROU.js.map} +1 -1
  148. package/dist/components/{p-B0Byw6OX.js → p-CgGcgMjs.js} +1 -1
  149. package/dist/components/{p-B0Byw6OX.js.map → p-CgGcgMjs.js.map} +1 -1
  150. package/dist/components/{p-C-Y7gYgz.js → p-ChfVRPHw.js} +2 -2
  151. package/dist/components/p-ChfVRPHw.js.map +1 -0
  152. package/dist/components/{p-CJq3n5kA.js → p-Ck1hgD6g.js} +2 -2
  153. package/dist/components/p-Ck1hgD6g.js.map +1 -0
  154. package/dist/components/{p-BMc0DE59.js → p-CochmPyg.js} +1 -1
  155. package/dist/components/p-CochmPyg.js.map +1 -0
  156. package/dist/components/{p-DDgeXGbB.js → p-CptSB-tA.js} +2 -2
  157. package/dist/components/p-CptSB-tA.js.map +1 -0
  158. package/dist/components/{p-DBue3ISk.js → p-CyZqk196.js} +2 -2
  159. package/dist/components/p-CyZqk196.js.map +1 -0
  160. package/dist/components/{p-Cxr0IQjC.js → p-CzbsTZt7.js} +2 -2
  161. package/dist/components/p-CzbsTZt7.js.map +1 -0
  162. package/dist/components/{p-ZpQBAwZC.js → p-D7QlTWbN.js} +2 -2
  163. package/dist/components/p-D7QlTWbN.js.map +1 -0
  164. package/dist/components/{p-C6DNhlES.js → p-DA5H_Iq_.js} +1 -1
  165. package/dist/components/p-DA5H_Iq_.js.map +1 -0
  166. package/dist/components/p-DBYv7fRq.js +2 -0
  167. package/dist/components/p-DBYv7fRq.js.map +1 -0
  168. package/dist/components/{p-CY4YFAT3.js → p-DCbKsABt.js} +1 -1
  169. package/dist/components/p-DCbKsABt.js.map +1 -0
  170. package/dist/components/{p-C6R5Lft0.js → p-DFC0sRM-.js} +2 -2
  171. package/dist/components/p-DFC0sRM-.js.map +1 -0
  172. package/dist/components/p-DGe5oXR0.js +2 -0
  173. package/dist/components/p-DGe5oXR0.js.map +1 -0
  174. package/dist/components/{p-CsBICYmt.js → p-DGkn8FwV.js} +2 -2
  175. package/dist/components/p-DGkn8FwV.js.map +1 -0
  176. package/dist/components/p-DIlScMvI.js +2 -0
  177. package/dist/components/p-DIlScMvI.js.map +1 -0
  178. package/dist/components/p-DKt_8s-U.js +2 -0
  179. package/dist/components/p-DKt_8s-U.js.map +1 -0
  180. package/dist/components/p-DLbHgbVj.js +2 -0
  181. package/dist/components/p-DLbHgbVj.js.map +1 -0
  182. package/dist/components/{p-Bu3dOjpW.js → p-DM1Z_hh0.js} +2 -2
  183. package/dist/components/{p-Bu3dOjpW.js.map → p-DM1Z_hh0.js.map} +1 -1
  184. package/dist/components/p-DOUk4KoT.js.map +1 -1
  185. package/dist/components/p-DQBT1Jkf.js +2 -0
  186. package/dist/components/p-DQBT1Jkf.js.map +1 -0
  187. package/dist/components/p-DW5Ita-P.js +2 -0
  188. package/dist/components/{p-DUdOmSFR.js.map → p-DW5Ita-P.js.map} +1 -1
  189. package/dist/components/{p-BS0AbMNK.js → p-DWjTOoXs.js} +2 -2
  190. package/dist/components/p-DWjTOoXs.js.map +1 -0
  191. package/dist/components/{p-DNpLDbWY.js → p-DZ58Sg5a.js} +1 -1
  192. package/dist/components/p-DZ58Sg5a.js.map +1 -0
  193. package/dist/components/{p-BknXYAEc.js → p-DdH7b4JF.js} +2 -2
  194. package/dist/components/p-DdH7b4JF.js.map +1 -0
  195. package/dist/components/p-DfDoqOlx.js +2 -0
  196. package/dist/components/p-DfDoqOlx.js.map +1 -0
  197. package/dist/components/p-Do8wbFff.js +2 -0
  198. package/dist/components/p-Do8wbFff.js.map +1 -0
  199. package/dist/components/p-Is1lFmwp.js +2 -0
  200. package/dist/components/p-Is1lFmwp.js.map +1 -0
  201. package/dist/components/p-L8IIeDXM.js +2 -0
  202. package/dist/components/p-L8IIeDXM.js.map +1 -0
  203. package/dist/components/{p-BUDAwJVV.js → p-caLOcQUK.js} +2 -2
  204. package/dist/components/p-caLOcQUK.js.map +1 -0
  205. package/dist/components/{p-D1eEgwcP.js → p-etg-Uk1u.js} +1 -1
  206. package/dist/components/{p-D1eEgwcP.js.map → p-etg-Uk1u.js.map} +1 -1
  207. package/dist/components/{p-C9gC4haA.js → p-fRDzpfi9.js} +2 -2
  208. package/dist/components/p-fRDzpfi9.js.map +1 -0
  209. package/dist/components/{p-C-vRkDyn.js → p-rCeztgVn.js} +1 -1
  210. package/dist/components/{p-C-vRkDyn.js.map → p-rCeztgVn.js.map} +1 -1
  211. package/dist/components/{p-BKp__CQX.js → p-rw0ZxIE4.js} +2 -2
  212. package/dist/components/p-rw0ZxIE4.js.map +1 -0
  213. package/dist/components/p-v4oqkJms.js +2 -0
  214. package/dist/components/p-v4oqkJms.js.map +1 -0
  215. package/dist/docs/components.json +117 -19
  216. package/dist/lib/shell/index.d.ts +2 -2
  217. package/dist/lib/shell/index.js +8 -6
  218. package/dist/lib/shell/index.js.map +2 -2
  219. package/dist/lib/toast/index.js +1 -4
  220. package/dist/lib/toast/index.js.map +2 -2
  221. package/dist/lib/utils/date-filter-value.d.ts +57 -0
  222. package/dist/lib/utils/index.d.ts +3 -1
  223. package/dist/lib/utils/index.js +171 -22
  224. package/dist/lib/utils/index.js.map +3 -3
  225. package/dist/mcp/cli.js +70 -31
  226. package/dist/mcp-data/registry/agent-activity.json +1 -1
  227. package/dist/mcp-data/registry/agent-questionnaire.json +3 -3
  228. package/dist/mcp-data/registry/agent-source-list.json +2 -2
  229. package/dist/mcp-data/registry/agent-tool-call.json +2 -2
  230. package/dist/mcp-data/registry/attachment-list.json +1 -1
  231. package/dist/mcp-data/registry/badge.json +1 -1
  232. package/dist/mcp-data/registry/banner.json +2 -2
  233. package/dist/mcp-data/registry/bar-action.json +1 -1
  234. package/dist/mcp-data/registry/bar-nav.json +3 -3
  235. package/dist/mcp-data/registry/bar-workflow.json +1 -1
  236. package/dist/mcp-data/registry/breadcrumb.json +1 -1
  237. package/dist/mcp-data/registry/button-filled.json +1 -1
  238. package/dist/mcp-data/registry/button-unfilled.json +1 -1
  239. package/dist/mcp-data/registry/card-chart.json +2 -2
  240. package/dist/mcp-data/registry/card-overview.json +3 -3
  241. package/dist/mcp-data/registry/card-setting.json +1 -1
  242. package/dist/mcp-data/registry/chart-legend.json +1 -1
  243. package/dist/mcp-data/registry/chart.json +1 -1
  244. package/dist/mcp-data/registry/checkbox.json +1 -1
  245. package/dist/mcp-data/registry/chip.json +1 -1
  246. package/dist/mcp-data/registry/code-block.json +1 -1
  247. package/dist/mcp-data/registry/conversation-list-item.json +1 -1
  248. package/dist/mcp-data/registry/conversation-list.json +2 -2
  249. package/dist/mcp-data/registry/divider.json +1 -1
  250. package/dist/mcp-data/registry/empty-state.json +1 -1
  251. package/dist/mcp-data/registry/field.json +1 -1
  252. package/dist/mcp-data/registry/filter-menu.json +56 -9
  253. package/dist/mcp-data/registry/icon.json +2 -2
  254. package/dist/mcp-data/registry/input.json +2 -2
  255. package/dist/mcp-data/registry/loader.json +1 -1
  256. package/dist/mcp-data/registry/menu.json +3 -3
  257. package/dist/mcp-data/registry/message-bubble.json +1 -1
  258. package/dist/mcp-data/registry/message-composer.json +2 -2
  259. package/dist/mcp-data/registry/message-scroller.json +3 -3
  260. package/dist/mcp-data/registry/mobile-bar-nav.json +1 -1
  261. package/dist/mcp-data/registry/mobile-header.json +2 -2
  262. package/dist/mcp-data/registry/mobile-section-switcher.json +2 -2
  263. package/dist/mcp-data/registry/mobile-sheet-nav.json +1 -1
  264. package/dist/mcp-data/registry/modal.json +2 -2
  265. package/dist/mcp-data/registry/pagination.json +2 -2
  266. package/dist/mcp-data/registry/panel-nav.json +2 -2
  267. package/dist/mcp-data/registry/panel-sub-nav.json +1 -1
  268. package/dist/mcp-data/registry/panel-tool-header.json +1 -1
  269. package/dist/mcp-data/registry/panel-tool-search.json +1 -1
  270. package/dist/mcp-data/registry/panel-tools.json +2 -2
  271. package/dist/mcp-data/registry/radio.json +1 -1
  272. package/dist/mcp-data/registry/registry.json +23 -7
  273. package/dist/mcp-data/registry/select.json +1 -1
  274. package/dist/mcp-data/registry/shell-app.json +2 -2
  275. package/dist/mcp-data/registry/shell-page.json +1 -1
  276. package/dist/mcp-data/registry/shell-tools.json +1 -1
  277. package/dist/mcp-data/registry/skeleton.json +2 -2
  278. package/dist/mcp-data/registry/slider.json +2 -2
  279. package/dist/mcp-data/registry/swatch-picker.json +3 -3
  280. package/dist/mcp-data/registry/switch.json +2 -2
  281. package/dist/mcp-data/registry/tab-group.json +2 -2
  282. package/dist/mcp-data/registry/table-filter.json +5 -3
  283. package/dist/mcp-data/registry/table-sort.json +1 -1
  284. package/dist/mcp-data/registry/table.json +18 -18
  285. package/dist/mcp-data/registry/tag.json +2 -2
  286. package/dist/mcp-data/registry/text.json +4 -4
  287. package/dist/mcp-data/registry/toast.json +2 -2
  288. package/dist/mcp-data/registry/tooltip-chart.json +2 -2
  289. package/dist/mcp-data/registry/tooltip.json +2 -2
  290. package/dist/styles/prose.css +42 -38
  291. package/dist/styles/table.css +39 -67
  292. package/dist/types/components/CardSetting/index.d.ts +1 -1
  293. package/dist/types/components/FilterMenu/FilterMenu.d.ts +20 -1
  294. package/dist/types/components/FilterMenu/filter-menu-calendar.d.ts +13 -0
  295. package/dist/types/components/TooltipChart/index.d.ts +1 -1
  296. package/dist/types/components.d.ts +26 -4
  297. package/dist/types/shell/index.d.ts +2 -2
  298. package/dist/types/utils/date-filter-value.d.ts +58 -0
  299. package/dist/types/utils/index.d.ts +3 -1
  300. package/dist/vue/ds-filter-menu.js +2 -0
  301. package/package.json +11 -1
  302. package/dist/components/p--CRAu9fJ.js +0 -2
  303. package/dist/components/p--CRAu9fJ.js.map +0 -1
  304. package/dist/components/p-BCOl1x0_.js +0 -2
  305. package/dist/components/p-BCOl1x0_.js.map +0 -1
  306. package/dist/components/p-BHVOiWz5.js +0 -2
  307. package/dist/components/p-BHVOiWz5.js.map +0 -1
  308. package/dist/components/p-BKp__CQX.js.map +0 -1
  309. package/dist/components/p-BMc0DE59.js.map +0 -1
  310. package/dist/components/p-BNiuAMeh.js.map +0 -1
  311. package/dist/components/p-BPG0r_mC.js +0 -2
  312. package/dist/components/p-BPG0r_mC.js.map +0 -1
  313. package/dist/components/p-BS0AbMNK.js.map +0 -1
  314. package/dist/components/p-BUDAwJVV.js.map +0 -1
  315. package/dist/components/p-BZVWmzKf.js +0 -2
  316. package/dist/components/p-BZVWmzKf.js.map +0 -1
  317. package/dist/components/p-Bbb9A3NV.js.map +0 -1
  318. package/dist/components/p-BhYS_Ump.js +0 -2
  319. package/dist/components/p-BhYS_Ump.js.map +0 -1
  320. package/dist/components/p-BknXYAEc.js.map +0 -1
  321. package/dist/components/p-BxGQ3cRJ.js +0 -2
  322. package/dist/components/p-BxGQ3cRJ.js.map +0 -1
  323. package/dist/components/p-BysVBVQv.js +0 -2
  324. package/dist/components/p-BysVBVQv.js.map +0 -1
  325. package/dist/components/p-C-Y7gYgz.js.map +0 -1
  326. package/dist/components/p-C6DNhlES.js.map +0 -1
  327. package/dist/components/p-C6R5Lft0.js.map +0 -1
  328. package/dist/components/p-C9gC4haA.js.map +0 -1
  329. package/dist/components/p-CAnnQL1F.js +0 -2
  330. package/dist/components/p-CAnnQL1F.js.map +0 -1
  331. package/dist/components/p-CJq3n5kA.js.map +0 -1
  332. package/dist/components/p-CY4YFAT3.js.map +0 -1
  333. package/dist/components/p-Cg0kimXZ.js +0 -2
  334. package/dist/components/p-Cg0kimXZ.js.map +0 -1
  335. package/dist/components/p-CsBICYmt.js.map +0 -1
  336. package/dist/components/p-CwjVgUqy.js +0 -2
  337. package/dist/components/p-Cxr0IQjC.js.map +0 -1
  338. package/dist/components/p-D3fMaUJ3.js.map +0 -1
  339. package/dist/components/p-D7R4yE2C.js +0 -2
  340. package/dist/components/p-D7R4yE2C.js.map +0 -1
  341. package/dist/components/p-DAvh38E4.js +0 -2
  342. package/dist/components/p-DAvh38E4.js.map +0 -1
  343. package/dist/components/p-DBue3ISk.js.map +0 -1
  344. package/dist/components/p-DDgeXGbB.js.map +0 -1
  345. package/dist/components/p-DFQ9mcbK.js.map +0 -1
  346. package/dist/components/p-DNpLDbWY.js.map +0 -1
  347. package/dist/components/p-DOUDRJbE.js +0 -2
  348. package/dist/components/p-DUdOmSFR.js +0 -2
  349. package/dist/components/p-DaAXY-XS.js.map +0 -1
  350. package/dist/components/p-DlYw4UGQ.js +0 -2
  351. package/dist/components/p-DlYw4UGQ.js.map +0 -1
  352. package/dist/components/p-DtndC9f1.js +0 -2
  353. package/dist/components/p-DtndC9f1.js.map +0 -1
  354. package/dist/components/p-JPTkZ3Ji.js +0 -2
  355. package/dist/components/p-ZpQBAwZC.js.map +0 -1
  356. package/dist/components/p-ag6Io-kL.js +0 -2
  357. package/dist/components/p-ag6Io-kL.js.map +0 -1
  358. package/dist/components/p-fg9EKoZp.js +0 -2
  359. package/dist/components/p-fg9EKoZp.js.map +0 -1
  360. package/dist/components/p-iMuFgSoM.js +0 -2
  361. package/dist/components/p-iMuFgSoM.js.map +0 -1
  362. package/dist/components/p-r8PMqlqm.js.map +0 -1
  363. /package/dist/components/{p-uI6z53BE.js.map → p-Bb4KnRy0.js.map} +0 -0
@@ -1295,22 +1295,22 @@
1295
1295
  },
1296
1296
  {
1297
1297
  "path": "src/wc/components/Table/Table.tsx",
1298
- "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Listen,\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 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 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 {\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;\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 /** 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\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 /** 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 /** 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 truncateTooltipLabel = '';\n @State() private captionCompact = false;\n @State() private columnCustomizerOpen = false;\n @State() private columnCustomizerInitialFocusVisible = false;\n @State() private dataModeSwitcherOpen = 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 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 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.virtualizationEnabled,\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 this.warnModelIssues();\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 }\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.dataMode === 'virtual' &&\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 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 }\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('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 if (this.dataMode === 'virtual') 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) this.closeColumnCustomizer();\n }\n\n @Watch('dataModeSwitcher')\n @Watch('captionVisibility')\n handleDataModeSwitcherAvailability(): void {\n if (!this.showsDataModeSwitcher) this.closeDataModeSwitcher();\n }\n\n @Watch('chromeLoading')\n handleChromeLoadingChange(loading: boolean): void {\n if (!loading) return;\n this.closeColumnCustomizer();\n this.closeDataModeSwitcher();\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 this.showsDataModeSwitcher || this.showsColumnCustomizer;\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 virtualizationEnabled(): boolean {\n return this.dataMode === 'virtual' && this.hasVirtualViewportBounds;\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 syncVirtualItems(model: TableRenderModel): void {\n if (this.dataMode !== 'virtual') {\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 shouldVirtualize(model: TableRenderModel): boolean {\n return (\n this.virtualizationEnabled && (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 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 emitRowActivation(row: TableRow, event: Event): void {\n if (!row.interactive || row.disabled || !this.rowEventOwnsActivation(event)) return;\n 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 }\n\n private renderOverflowActionMenu() {\n const open = this.actionMenu;\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(open && 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 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.virtualizationEnabled) 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(column, segment.sortKey, segment.label)}\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 return (\n <colgroup>\n {model.selectable && <col class=\"ds-table__selection-column\" />}\n {this.visibleColumns.map(column => {\n const width = tableColumnSize(column);\n const flexible = column.id === model.flexibleColumnId;\n return (\n <col\n key={column.id}\n class={{\n 'ds-table__action-column': column.kind === 'action',\n 'ds-table__flexible-column': flexible,\n }}\n style={width && !flexible ? { width } : undefined}\n />\n );\n })}\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 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 {this.visibleColumns.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 ariaRowIndex,\n variableVirtualSize,\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\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 renderGroupContent(groupModel: TableRenderModel['groups'][number]) {\n const {\n group,\n countLabel,\n collapsed: isCollapsed,\n labelColor,\n selection: groupSelection,\n } = groupModel;\n return (\n <span class=\"ds-table__group-content\">\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 <span class=\"ds-table__group-copy\">\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 <ds-text\n class=\"ds-table__group-separator\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n aria-hidden=\"true\"\n >\n ·\n </ds-text>\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-visually-hidden\">{countLabel}</span>\n </span>\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={isCollapsed ? `Expand ${group.label} group` : `Collapse ${group.label} group`}\n hasBorder={false}\n onDsClick={event => {\n event.stopPropagation();\n this.emitGroupCollapse(group);\n }}\n />\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\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) =>\n this.renderGroupLoadRow(group, totalColumns),\n });\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\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\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 </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 && 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 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 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 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 }\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<HTMLElement & { setFocus?: () => void }>(\n `#${CSS.escape(`${this.dataModeSwitcherElementId}-trigger`)}`\n )\n ?.setFocus?.();\n });\n }\n\n private renderColumnCustomizerTrigger() {\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 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 hasMenu={true}\n expanded={this.columnCustomizerOpen}\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 items={tableColumnCustomizerMenuItems(this.columns, this.hiddenColumnIds, this.columnOrder)}\n onDsClose={() => this.closeColumnCustomizer()}\n onDsSelect={event => this.handleColumnCustomizerSelect(event.detail)}\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 }\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 virtualize = this.shouldVirtualize(model);\n const virtualViewportMissing =\n this.virtualViewportMissing && !initialLoading && (model.hasData || hasGroupedStructure);\n const virtualPlan = virtualize\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--virtual': virtualize,\n 'ds-table__table--native-group-sticky':\n (model.grouped && this.stickyHeader && !this.documentStickyHeader) ||\n virtualize,\n }}\n style={model.tableStyle}\n aria-rowcount={virtualize ? 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 virtualize ? 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",
1298
+ "content": "import {\n Component,\n Element,\n Event,\n EventEmitter,\n h,\n Host,\n Listen,\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 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 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 {\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;\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 /** 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\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 /** 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 /** 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 truncateTooltipLabel = '';\n @State() private captionCompact = false;\n @State() private columnCustomizerOpen = false;\n @State() private columnCustomizerInitialFocusVisible = false;\n @State() private dataModeSwitcherOpen = 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 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 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.virtualizationEnabled,\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 this.warnModelIssues();\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 }\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.dataMode === 'virtual' &&\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 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 }\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('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 if (this.dataMode === 'virtual') 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) this.closeColumnCustomizer();\n }\n\n @Watch('dataModeSwitcher')\n @Watch('captionVisibility')\n handleDataModeSwitcherAvailability(): void {\n if (!this.showsDataModeSwitcher) this.closeDataModeSwitcher();\n }\n\n @Watch('chromeLoading')\n handleChromeLoadingChange(loading: boolean): void {\n if (!loading) return;\n this.closeColumnCustomizer();\n this.closeDataModeSwitcher();\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 this.showsDataModeSwitcher || this.showsColumnCustomizer;\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 virtualizationEnabled(): boolean {\n return this.dataMode === 'virtual' && this.hasVirtualViewportBounds;\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 syncVirtualItems(model: TableRenderModel): void {\n if (this.dataMode !== 'virtual') {\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 shouldVirtualize(model: TableRenderModel): boolean {\n return (\n this.virtualizationEnabled && (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 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 emitRowActivation(row: TableRow, event: Event): void {\n if (!row.interactive || row.disabled || !this.rowEventOwnsActivation(event)) return;\n 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 }\n\n private renderOverflowActionMenu() {\n const open = this.actionMenu;\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(open && 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 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.virtualizationEnabled) 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(column, segment.sortKey, segment.label)}\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 return (\n <colgroup>\n {model.selectable && <col class=\"ds-table__selection-column\" />}\n {this.visibleColumns.map(column => {\n const width = tableColumnSize(column);\n const flexible = column.id === model.flexibleColumnId;\n return (\n <col\n key={column.id}\n class={{\n 'ds-table__action-column': column.kind === 'action',\n 'ds-table__flexible-column': flexible,\n }}\n style={width && !flexible ? { width } : undefined}\n />\n );\n })}\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 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 {this.visibleColumns.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 ariaRowIndex,\n variableVirtualSize,\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 renderGroupContent(groupModel: TableRenderModel['groups'][number]) {\n const {\n group,\n countLabel,\n collapsed: isCollapsed,\n labelColor,\n selection: groupSelection,\n } = groupModel;\n return (\n <span class=\"ds-table__group-content\">\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 <span class=\"ds-table__group-copy\">\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 <ds-text\n class=\"ds-table__group-separator\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n aria-hidden=\"true\"\n >\n ·\n </ds-text>\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-visually-hidden\">{countLabel}</span>\n </span>\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={isCollapsed ? `Expand ${group.label} group` : `Collapse ${group.label} group`}\n hasBorder={false}\n onDsClick={event => {\n event.stopPropagation();\n this.emitGroupCollapse(group);\n }}\n />\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 </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 && 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 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 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 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 }\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.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 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 hasMenu={true}\n expanded={this.columnCustomizerOpen}\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 items={tableColumnCustomizerMenuItems(this.columns, this.hiddenColumnIds, this.columnOrder)}\n onDsClose={() => this.closeColumnCustomizer()}\n onDsSelect={event => this.handleColumnCustomizerSelect(event.detail)}\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 }\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 virtualize = this.shouldVirtualize(model);\n const virtualViewportMissing =\n this.virtualViewportMissing && !initialLoading && (model.hasData || hasGroupedStructure);\n const virtualPlan = virtualize\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--virtual': virtualize,\n 'ds-table__table--native-group-sticky':\n (model.grouped && this.stickyHeader && !this.documentStickyHeader) ||\n virtualize,\n }}\n style={model.tableStyle}\n aria-rowcount={virtualize ? 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 virtualize ? 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",
1299
1299
  "type": "registry:ui"
1300
1300
  },
1301
1301
  {
1302
1302
  "path": "src/wc/components/Table/table-action-menu.ts",
1303
- "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(\n items: TableCellActionMenuEntry[],\n): 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",
1303
+ "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",
1304
1304
  "type": "registry:ui"
1305
1305
  },
1306
1306
  {
1307
1307
  "path": "src/wc/components/Table/table-body-renderer.tsx",
1308
- "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(\n row,\n options.model,\n node.index + 2,\n node.item.variableSize,\n rowKey,\n );\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",
1308
+ "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",
1309
1309
  "type": "registry:ui"
1310
1310
  },
1311
1311
  {
1312
1312
  "path": "src/wc/components/Table/table-cell-model.ts",
1313
- "content": "import {\n isTableCellAction,\n isTableCellBlank,\n isTableCellEmpty,\n isTableCellIcon,\n isTableCellIconText,\n isTableCellImage,\n isTableCellPrimaryText,\n isTableCellTag,\n isTableCellText,\n} from './table-model';\nimport type {\n TableCellAction,\n TableCellBlank,\n TableCellEmpty,\n TableCellIcon,\n TableCellImage,\n TableCellImageTracks,\n TableCellTag,\n TableCellTagVariant,\n TableCellTextRun,\n TableCellTextTrack,\n TableCellValue,\n TableCellLinkTarget,\n TableCellLineClamp,\n TableCellMaxLines,\n TableColumn,\n} from './table-types';\nimport type { IconColor } from '../Icon/Icon';\nimport type { TextColor } from '../Text/text-types';\n\n/** Maximum independently colored runs on one secondary or tertiary track. */\nexport const TABLE_CELL_TEXT_RUN_LIMIT = 3;\n\nexport interface ResolvedTableCellText {\n primary: string | number;\n secondary?: TableCellTextRun[];\n tertiary?: TableCellTextRun[];\n secondaryColor?: TextColor;\n tertiaryColor?: TextColor;\n href?: string;\n target?: TableCellLinkTarget;\n wrap?: boolean;\n fontFeature?: 'normal' | 'tabular-nums';\n}\n\nexport type TableCellPresentation =\n | { kind: 'blank'; cellType: 'blank'; value: TableCellBlank }\n | { kind: 'empty'; cellType: 'empty'; value: TableCellEmpty | null | undefined }\n | { kind: 'icon'; cellType: 'icon'; value: TableCellIcon }\n | {\n kind: 'icon-text';\n cellType: 'icon-text';\n icon: string;\n iconColor?: IconColor;\n iconLabel?: string;\n value: ResolvedTableCellText;\n variant: 'single' | 'multi' | 'triple';\n wraps: boolean;\n lineClamp: TableCellLineClamp;\n }\n | {\n kind: 'image';\n cellType: 'image';\n value: TableCellImage;\n variant: 'single' | 'multi' | 'triple';\n }\n | { kind: 'action'; cellType: 'action'; value: TableCellAction }\n | {\n kind: 'tag';\n cellType: 'tag';\n value: TableCellTag;\n variant: TableCellTagVariant;\n }\n | {\n kind: 'text';\n cellType: 'text' | 'primary-text';\n value: ResolvedTableCellText;\n primaryText: boolean;\n singleLine: boolean;\n variant: 'single' | 'multi' | 'triple' | 'primary-pair';\n wraps: boolean;\n lineClamp: TableCellLineClamp;\n };\n\nfunction trackText(value: string | number | TableCellTextRun): string | undefined {\n if (typeof value === 'string' || typeof value === 'number') {\n const text = String(value);\n return text.trim() === '' ? undefined : text;\n }\n const text = value.text?.trim();\n return text ? value.text : undefined;\n}\n\n/** Resolve an image cell's track stack; unknown values fall back to one track. */\nexport function resolveTableCellImageTracks(\n tracks: TableCellImage['tracks'],\n): TableCellImageTracks {\n return tracks === 2 || tracks === 3 ? tracks : 1;\n}\n\n/** Map an image track stack onto the shared single/multi/triple cell variant. */\nexport function tableCellImageVariant(\n tracks: TableCellImageTracks,\n): 'single' | 'multi' | 'triple' {\n return tracks === 3 ? 'triple' : tracks === 2 ? 'multi' : 'single';\n}\n\n/** Collapse a consumer track into at most three non-empty runs. */\nexport function normalizeTableCellTextTrack(\n track: TableCellTextTrack | number | undefined,\n): TableCellTextRun[] | undefined {\n if (track == null) return undefined;\n const items = Array.isArray(track) ? track : [track];\n const runs: TableCellTextRun[] = [];\n for (const item of items) {\n if (runs.length >= TABLE_CELL_TEXT_RUN_LIMIT) break;\n const text = trackText(item);\n if (text === undefined) continue;\n runs.push(\n typeof item === 'object' && item !== null && 'color' in item && item.color\n ? { text, color: item.color }\n : { text },\n );\n }\n return runs.length ? runs : undefined;\n}\n\n/** Map wrap / maxLines onto wrap-to-track geometry and a line clamp. */\nexport function resolveTableCellTextOverflow(\n source: { wrap?: boolean; maxLines?: TableCellMaxLines },\n column: TableColumn,\n): { wraps: boolean; lineClamp: TableCellLineClamp } {\n const wrap = source.wrap ?? column.wrap ?? false;\n const maxLines = source.maxLines ?? (source.wrap === true ? undefined : column.maxLines);\n if (maxLines != null) {\n return { wraps: maxLines > 1, lineClamp: maxLines };\n }\n if (wrap) return { wraps: true, lineClamp: 'none' };\n return { wraps: false, lineClamp: 1 };\n}\n\n/** ds-text overflow props for a resolved clamp. */\nexport function tableCellTextOverflowProps(lineClamp: TableCellLineClamp): {\n lineTruncation: TableCellLineClamp;\n wrap: 'wrap' | 'nowrap';\n} {\n if (lineClamp === 'none') return { lineTruncation: 'none', wrap: 'wrap' };\n if (lineClamp === 1) return { lineTruncation: 1, wrap: 'nowrap' };\n return { lineTruncation: lineClamp, wrap: 'wrap' };\n}\n\nfunction resolveTextPresentation(\n source: {\n primary: string | number;\n secondary?: TableCellTextTrack | number;\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 column: TableColumn,\n options: { primaryText: boolean; allowTertiary: boolean },\n): {\n value: ResolvedTableCellText;\n singleLine: boolean;\n variant: 'single' | 'multi' | 'triple' | 'primary-pair';\n wraps: boolean;\n lineClamp: TableCellLineClamp;\n} {\n const secondary = normalizeTableCellTextTrack(source.secondary);\n const tertiary = options.allowTertiary\n ? normalizeTableCellTextTrack(source.tertiary)\n : undefined;\n const value: ResolvedTableCellText = {\n primary: source.primary,\n ...(secondary ? { secondary } : {}),\n ...(tertiary ? { tertiary } : {}),\n ...(source.secondaryColor ? { secondaryColor: source.secondaryColor } : {}),\n ...(source.tertiaryColor ? { tertiaryColor: source.tertiaryColor } : {}),\n ...(source.href ? { href: source.href } : {}),\n ...(source.target ? { target: source.target } : {}),\n ...(source.wrap ? { wrap: source.wrap } : {}),\n ...(source.fontFeature ? { fontFeature: source.fontFeature } : {}),\n };\n const singleLine = !options.primaryText && !secondary && !tertiary;\n const overflow = resolveTableCellTextOverflow(source, column);\n return {\n value,\n singleLine,\n variant: options.primaryText\n ? 'primary-pair'\n : tertiary\n ? 'triple'\n : singleLine\n ? 'single'\n : 'multi',\n wraps: overflow.wraps,\n lineClamp: overflow.lineClamp,\n };\n}\n\n/** Resolve a cell's semantic and visual recipe once for both markup and classes. */\nexport function resolveTableCellPresentation(\n value: TableCellValue,\n column: TableColumn,\n): TableCellPresentation {\n if (isTableCellBlank(value)) return { kind: 'blank', cellType: 'blank', value };\n if (value == null || isTableCellEmpty(value)) {\n return { kind: 'empty', cellType: 'empty', value };\n }\n if (isTableCellIcon(value)) return { kind: 'icon', cellType: 'icon', value };\n if (isTableCellIconText(value)) {\n const text = resolveTextPresentation(value, column, {\n primaryText: false,\n allowTertiary: true,\n });\n return {\n kind: 'icon-text',\n cellType: 'icon-text',\n icon: value.icon,\n ...(value.iconColor ? { iconColor: value.iconColor } : {}),\n ...(value.iconLabel ? { iconLabel: value.iconLabel } : {}),\n value: text.value,\n variant: text.variant === 'primary-pair' ? 'single' : text.variant,\n wraps: text.wraps,\n lineClamp: text.lineClamp,\n };\n }\n if (isTableCellImage(value)) {\n return {\n kind: 'image',\n cellType: 'image',\n value,\n variant: tableCellImageVariant(resolveTableCellImageTracks(value.tracks)),\n };\n }\n if (isTableCellAction(value)) return { kind: 'action', cellType: 'action', value };\n if (isTableCellTag(value)) {\n return {\n kind: 'tag',\n cellType: 'tag',\n value,\n variant: value.variant ?? 'tag-only',\n };\n }\n\n const primaryText = isTableCellPrimaryText(value);\n const source = isTableCellText(value) || primaryText\n ? value\n : {\n primary: value,\n fontFeature: typeof value === 'number' ? 'tabular-nums' as const : 'normal' as const,\n };\n const text = resolveTextPresentation(\n isTableCellText(value)\n ? value\n : source,\n column,\n { primaryText, allowTertiary: isTableCellText(value) },\n );\n return {\n kind: 'text',\n cellType: primaryText ? 'primary-text' : 'text',\n value: text.value,\n primaryText,\n singleLine: text.singleLine,\n variant: text.variant,\n wraps: text.wraps,\n lineClamp: text.lineClamp,\n };\n}\n",
1313
+ "content": "import {\n isTableCellAction,\n isTableCellBlank,\n isTableCellEmpty,\n isTableCellIcon,\n isTableCellIconText,\n isTableCellImage,\n isTableCellPrimaryText,\n isTableCellTag,\n isTableCellText,\n} from './table-model';\nimport type {\n TableCellAction,\n TableCellBlank,\n TableCellEmpty,\n TableCellIcon,\n TableCellImage,\n TableCellImageTracks,\n TableCellTag,\n TableCellTagVariant,\n TableCellTextRun,\n TableCellTextTrack,\n TableCellValue,\n TableCellLinkTarget,\n TableCellLineClamp,\n TableCellMaxLines,\n TableColumn,\n} from './table-types';\nimport type { IconColor } from '../Icon/Icon';\nimport type { TextColor } from '../Text/text-types';\n\n/** Maximum independently colored runs on one secondary or tertiary track. */\nexport const TABLE_CELL_TEXT_RUN_LIMIT = 3;\n\nexport interface ResolvedTableCellText {\n primary: string | number;\n secondary?: TableCellTextRun[];\n tertiary?: TableCellTextRun[];\n secondaryColor?: TextColor;\n tertiaryColor?: TextColor;\n href?: string;\n target?: TableCellLinkTarget;\n wrap?: boolean;\n fontFeature?: 'normal' | 'tabular-nums';\n}\n\nexport type TableCellPresentation =\n | { kind: 'blank'; cellType: 'blank'; value: TableCellBlank }\n | { kind: 'empty'; cellType: 'empty'; value: TableCellEmpty | null | undefined }\n | { kind: 'icon'; cellType: 'icon'; value: TableCellIcon }\n | {\n kind: 'icon-text';\n cellType: 'icon-text';\n icon: string;\n iconColor?: IconColor;\n iconLabel?: string;\n value: ResolvedTableCellText;\n variant: 'single' | 'multi' | 'triple';\n wraps: boolean;\n lineClamp: TableCellLineClamp;\n }\n | {\n kind: 'image';\n cellType: 'image';\n value: TableCellImage;\n variant: 'single' | 'multi' | 'triple';\n }\n | { kind: 'action'; cellType: 'action'; value: TableCellAction }\n | {\n kind: 'tag';\n cellType: 'tag';\n value: TableCellTag;\n variant: TableCellTagVariant;\n }\n | {\n kind: 'text';\n cellType: 'text' | 'primary-text';\n value: ResolvedTableCellText;\n primaryText: boolean;\n singleLine: boolean;\n variant: 'single' | 'multi' | 'triple' | 'primary-pair';\n wraps: boolean;\n lineClamp: TableCellLineClamp;\n };\n\nfunction trackText(value: string | number | TableCellTextRun): string | undefined {\n if (typeof value === 'string' || typeof value === 'number') {\n const text = String(value);\n return text.trim() === '' ? undefined : text;\n }\n const text = value.text?.trim();\n return text ? value.text : undefined;\n}\n\n/** Resolve an image cell's track stack; unknown values fall back to one track. */\nexport function resolveTableCellImageTracks(\n tracks: TableCellImage['tracks']\n): TableCellImageTracks {\n return tracks === 2 || tracks === 3 ? tracks : 1;\n}\n\n/** Map an image track stack onto the shared single/multi/triple cell variant. */\nexport function tableCellImageVariant(tracks: TableCellImageTracks): 'single' | 'multi' | 'triple' {\n return tracks === 3 ? 'triple' : tracks === 2 ? 'multi' : 'single';\n}\n\n/** Collapse a consumer track into at most three non-empty runs. */\nexport function normalizeTableCellTextTrack(\n track: TableCellTextTrack | number | undefined\n): TableCellTextRun[] | undefined {\n if (track == null) return undefined;\n const items = Array.isArray(track) ? track : [track];\n const runs: TableCellTextRun[] = [];\n for (const item of items) {\n if (runs.length >= TABLE_CELL_TEXT_RUN_LIMIT) break;\n const text = trackText(item);\n if (text === undefined) continue;\n runs.push(\n typeof item === 'object' && item !== null && 'color' in item && item.color\n ? { text, color: item.color }\n : { text }\n );\n }\n return runs.length ? runs : undefined;\n}\n\n/** Map wrap / maxLines onto wrap-to-track geometry and a line clamp. */\nexport function resolveTableCellTextOverflow(\n source: { wrap?: boolean; maxLines?: TableCellMaxLines },\n column: TableColumn\n): { wraps: boolean; lineClamp: TableCellLineClamp } {\n const wrap = source.wrap ?? column.wrap ?? false;\n const maxLines = source.maxLines ?? (source.wrap === true ? undefined : column.maxLines);\n if (maxLines != null) {\n return { wraps: maxLines > 1, lineClamp: maxLines };\n }\n if (wrap) return { wraps: true, lineClamp: 'none' };\n return { wraps: false, lineClamp: 1 };\n}\n\n/** ds-text overflow props for a resolved clamp. */\nexport function tableCellTextOverflowProps(lineClamp: TableCellLineClamp): {\n lineTruncation: TableCellLineClamp;\n wrap: 'wrap' | 'nowrap';\n} {\n if (lineClamp === 'none') return { lineTruncation: 'none', wrap: 'wrap' };\n if (lineClamp === 1) return { lineTruncation: 1, wrap: 'nowrap' };\n return { lineTruncation: lineClamp, wrap: 'wrap' };\n}\n\nfunction resolveTextPresentation(\n source: {\n primary: string | number;\n secondary?: TableCellTextTrack | number;\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 column: TableColumn,\n options: { primaryText: boolean; allowTertiary: boolean }\n): {\n value: ResolvedTableCellText;\n singleLine: boolean;\n variant: 'single' | 'multi' | 'triple' | 'primary-pair';\n wraps: boolean;\n lineClamp: TableCellLineClamp;\n} {\n const secondary = normalizeTableCellTextTrack(source.secondary);\n const tertiary = options.allowTertiary ? normalizeTableCellTextTrack(source.tertiary) : undefined;\n const value: ResolvedTableCellText = {\n primary: source.primary,\n ...(secondary ? { secondary } : {}),\n ...(tertiary ? { tertiary } : {}),\n ...(source.secondaryColor ? { secondaryColor: source.secondaryColor } : {}),\n ...(source.tertiaryColor ? { tertiaryColor: source.tertiaryColor } : {}),\n ...(source.href ? { href: source.href } : {}),\n ...(source.target ? { target: source.target } : {}),\n ...(source.wrap ? { wrap: source.wrap } : {}),\n ...(source.fontFeature ? { fontFeature: source.fontFeature } : {}),\n };\n const singleLine = !options.primaryText && !secondary && !tertiary;\n const overflow = resolveTableCellTextOverflow(source, column);\n return {\n value,\n singleLine,\n variant: options.primaryText\n ? 'primary-pair'\n : tertiary\n ? 'triple'\n : singleLine\n ? 'single'\n : 'multi',\n wraps: overflow.wraps,\n lineClamp: overflow.lineClamp,\n };\n}\n\n/** Resolve a cell's semantic and visual recipe once for both markup and classes. */\nexport function resolveTableCellPresentation(\n value: TableCellValue,\n column: TableColumn\n): TableCellPresentation {\n if (isTableCellBlank(value)) return { kind: 'blank', cellType: 'blank', value };\n if (value == null || isTableCellEmpty(value)) {\n return { kind: 'empty', cellType: 'empty', value };\n }\n if (isTableCellIcon(value)) return { kind: 'icon', cellType: 'icon', value };\n if (isTableCellIconText(value)) {\n const text = resolveTextPresentation(value, column, {\n primaryText: false,\n allowTertiary: true,\n });\n return {\n kind: 'icon-text',\n cellType: 'icon-text',\n icon: value.icon,\n ...(value.iconColor ? { iconColor: value.iconColor } : {}),\n ...(value.iconLabel ? { iconLabel: value.iconLabel } : {}),\n value: text.value,\n variant: text.variant === 'primary-pair' ? 'single' : text.variant,\n wraps: text.wraps,\n lineClamp: text.lineClamp,\n };\n }\n if (isTableCellImage(value)) {\n return {\n kind: 'image',\n cellType: 'image',\n value,\n variant: tableCellImageVariant(resolveTableCellImageTracks(value.tracks)),\n };\n }\n if (isTableCellAction(value)) return { kind: 'action', cellType: 'action', value };\n if (isTableCellTag(value)) {\n return {\n kind: 'tag',\n cellType: 'tag',\n value,\n variant: value.variant ?? 'tag-only',\n };\n }\n\n const primaryText = isTableCellPrimaryText(value);\n const source =\n isTableCellText(value) || primaryText\n ? value\n : {\n primary: value,\n fontFeature: typeof value === 'number' ? ('tabular-nums' as const) : ('normal' as const),\n };\n const text = resolveTextPresentation(isTableCellText(value) ? value : source, column, {\n primaryText,\n allowTertiary: isTableCellText(value),\n });\n return {\n kind: 'text',\n cellType: primaryText ? 'primary-text' : 'text',\n value: text.value,\n primaryText,\n singleLine: text.singleLine,\n variant: text.variant,\n wraps: text.wraps,\n lineClamp: text.lineClamp,\n };\n}\n",
1314
1314
  "type": "registry:ui"
1315
1315
  },
1316
1316
  {
@@ -1320,32 +1320,32 @@
1320
1320
  },
1321
1321
  {
1322
1322
  "path": "src/wc/components/Table/table-data-mode-switcher.ts",
1323
- "content": "import type { MenuItemData } from '../Menu/menu-types';\nimport type { TableDataMode } from './table-types';\n\nlet tableDataModeSwitcherSequence = 0;\n\nexport function nextTableDataModeSwitcherElementId(): string {\n tableDataModeSwitcherSequence += 1;\n return `ds-table-data-mode-${tableDataModeSwitcherSequence}`;\n}\n\n/** Menu of supported data-mode choices. */\nexport function tableDataModeMenuItems(\n dataMode: TableDataMode,\n labels: { infinite: string; pagination: string; virtual: string },\n): MenuItemData[] {\n return [\n {\n label: labels.infinite,\n value: 'infinite',\n isSelected: dataMode === 'infinite',\n },\n {\n label: labels.pagination,\n value: 'pagination',\n isSelected: dataMode === 'pagination',\n },\n {\n label: labels.virtual,\n value: 'virtual',\n isSelected: dataMode === 'virtual',\n },\n ];\n}\n\nexport function tableDataModeFromMenuItem(item: MenuItemData): TableDataMode | null {\n return item.value === 'infinite' || item.value === 'pagination' || item.value === 'virtual'\n ? item.value\n : null;\n}\n",
1323
+ "content": "import type { MenuItemData } from '../Menu/menu-types';\nimport type { TableDataMode } from './table-types';\n\nlet tableDataModeSwitcherSequence = 0;\n\nexport function nextTableDataModeSwitcherElementId(): string {\n tableDataModeSwitcherSequence += 1;\n return `ds-table-data-mode-${tableDataModeSwitcherSequence}`;\n}\n\n/** Menu of supported data-mode choices. */\nexport function tableDataModeMenuItems(\n dataMode: TableDataMode,\n labels: { infinite: string; pagination: string; virtual: string }\n): MenuItemData[] {\n return [\n {\n label: labels.infinite,\n value: 'infinite',\n isSelected: dataMode === 'infinite',\n },\n {\n label: labels.pagination,\n value: 'pagination',\n isSelected: dataMode === 'pagination',\n },\n {\n label: labels.virtual,\n value: 'virtual',\n isSelected: dataMode === 'virtual',\n },\n ];\n}\n\nexport function tableDataModeFromMenuItem(item: MenuItemData): TableDataMode | null {\n return item.value === 'infinite' || item.value === 'pagination' || item.value === 'virtual'\n ? item.value\n : null;\n}\n",
1324
1324
  "type": "registry:ui"
1325
1325
  },
1326
1326
  {
1327
1327
  "path": "src/wc/components/Table/table-group-load-controller.ts",
1328
- "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}').join(group.label)\n .split('{count}').join(String(count ?? group.rows.length))\n .split('{loaded}').join(String(group.rows.length))\n .split('{total}').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 ) 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",
1328
+ "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",
1329
1329
  "type": "registry:ui"
1330
1330
  },
1331
1331
  {
1332
1332
  "path": "src/wc/components/Table/table-layout-controller.ts",
1333
- "content": "export interface TableLayoutElements {\n viewport: HTMLElement | null;\n contentTable: HTMLTableElement | null;\n stickyHeaderTable: HTMLTableElement | null;\n collapseAllOverlay: HTMLElement | null;\n frame: HTMLElement | null;\n interactiveHead: HTMLTableSectionElement | null;\n}\n\nexport interface TableLayoutMode {\n documentStickyHeader: boolean;\n floatingCollapseAll: boolean;\n clampVerticalOverscroll: boolean;\n}\n\nexport interface TableOverflowState {\n start: boolean;\n end: boolean;\n scrollable: boolean;\n}\n\nexport interface TableLayoutControllerOptions {\n elements: () => TableLayoutElements;\n mode: () => TableLayoutMode;\n overflowChanged: (state: TableOverflowState) => void;\n verticalEdgeWheel?: (deltaY: number) => boolean;\n}\n\nexport interface TableLayoutMetricInput {\n viewportInlineSize: number;\n viewportBlockSize: number;\n scrollInlineSize: number;\n scrollBlockSize: number;\n scrollInlineOffset: number;\n collapseHeadBlockStart?: number;\n collapseFrameBlockStart?: number;\n}\n\nexport interface TableLayoutMetrics {\n visibleInlineSize: number;\n overflow: TableOverflowState;\n inlineOffset: number;\n maxInlineOffset: number;\n collapseBlockOffset: number | null;\n}\n\n/** Pure geometry resolution shared by the controller and focused tests. */\nexport function resolveTableLayoutMetrics(input: TableLayoutMetricInput): TableLayoutMetrics {\n const horizontalOverflow = input.scrollInlineSize - input.viewportInlineSize > 1;\n return {\n visibleInlineSize: input.viewportInlineSize,\n overflow: {\n start: horizontalOverflow && input.scrollInlineOffset > 1,\n end: horizontalOverflow &&\n input.scrollInlineOffset + input.viewportInlineSize < input.scrollInlineSize - 1,\n scrollable: horizontalOverflow || input.scrollBlockSize - input.viewportBlockSize > 1,\n },\n inlineOffset: input.scrollInlineOffset,\n maxInlineOffset: Math.max(0, input.scrollInlineSize - input.viewportInlineSize),\n collapseBlockOffset: input.collapseHeadBlockStart == null ||\n input.collapseFrameBlockStart == null\n ? null\n : Math.max(0, input.collapseHeadBlockStart - input.collapseFrameBlockStart),\n };\n}\n\n/**\n * Owns table viewport observation. It resolves newly rendered geometry\n * immediately so the first paint has the correct viewport-width chrome, then\n * batches observer-driven scroll and resize reads/writes into one animation frame.\n * The component remains responsible only for refs and reactive overflow state.\n */\nexport class TableLayoutController {\n private connected = false;\n private connectedViewport: HTMLElement | null = null;\n private connectedContentTable: HTMLTableElement | null = null;\n private resizeObserver: ResizeObserver | null = null;\n private frame: number | null = null;\n private overflow: TableOverflowState | null = null;\n private inlineOffset = 0;\n private maxInlineOffset = 0;\n private maxBlockOffset = 0;\n private lastMode: TableLayoutMode | null = null;\n\n constructor(private readonly options: TableLayoutControllerOptions) {}\n\n connect(): void {\n this.connected = true;\n this.refresh();\n }\n\n disconnect(): void {\n this.connected = false;\n if (this.frame !== null) cancelAnimationFrame(this.frame);\n this.frame = null;\n this.connectedViewport?.removeEventListener('scroll', this.handleScroll);\n this.connectedViewport?.removeEventListener('wheel', this.handleWheel);\n this.connectedViewport = null;\n this.connectedContentTable = null;\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n }\n\n /** Reconcile newly rendered refs, observers, and pending geometry. */\n refresh(forceGeometry = true): void {\n if (!this.connected) return;\n const { viewport, contentTable } = this.options.elements();\n const elementsChanged = viewport !== this.connectedViewport ||\n contentTable !== this.connectedContentTable;\n if (elementsChanged) {\n this.connectedViewport?.removeEventListener('scroll', this.handleScroll);\n this.connectedViewport?.removeEventListener('wheel', this.handleWheel);\n this.resizeObserver?.disconnect();\n this.connectedViewport = viewport;\n this.connectedContentTable = contentTable;\n viewport?.addEventListener('scroll', this.handleScroll, { passive: true });\n viewport?.addEventListener('wheel', this.handleWheel, { passive: false });\n\n if (typeof ResizeObserver !== 'undefined' && viewport) {\n this.resizeObserver ??= new ResizeObserver(this.schedule);\n this.resizeObserver.observe(viewport);\n if (contentTable) this.resizeObserver.observe(contentTable);\n }\n }\n const mode = this.options.mode();\n const modeChanged = !this.lastMode ||\n mode.documentStickyHeader !== this.lastMode.documentStickyHeader ||\n mode.floatingCollapseAll !== this.lastMode.floatingCollapseAll ||\n mode.clampVerticalOverscroll !== this.lastMode.clampVerticalOverscroll;\n this.lastMode = mode;\n // New refs and explicit refreshes resolve before paint. A recycled virtual\n // window keeps the same scroll geometry, so ResizeObserver owns later size\n // changes without forcing layout after every row patch.\n if (forceGeometry || elementsChanged || modeChanged) this.sync();\n }\n\n private schedule = (): void => {\n if (!this.connected || this.frame !== null) return;\n this.frame = requestAnimationFrame(() => {\n this.frame = null;\n this.sync();\n });\n };\n\n private readonly handleScroll = (): void => {\n const viewport = this.connectedViewport;\n if (!viewport) return;\n const mode = this.options.mode();\n this.clampInlineOffset();\n if (mode.clampVerticalOverscroll) this.clampBlockOffset();\n if (\n viewport.scrollLeft !== this.inlineOffset ||\n (mode.floatingCollapseAll && !mode.documentStickyHeader)\n ) {\n this.schedule();\n }\n };\n\n private readonly handleWheel = (event: WheelEvent): void => {\n const viewport = this.connectedViewport;\n if (!viewport) return;\n\n if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) {\n const atStart = viewport.scrollLeft <= 0 && event.deltaX < 0;\n const atEnd = viewport.scrollLeft >= this.maxInlineOffset && event.deltaX > 0;\n if (atStart || atEnd) event.preventDefault();\n return;\n }\n\n if (!this.options.mode().clampVerticalOverscroll || event.deltaY === 0) return;\n const atStart = viewport.scrollTop <= 0 && event.deltaY < 0;\n const atEnd = viewport.scrollTop >= this.maxBlockOffset && event.deltaY > 0;\n if (!atStart && !atEnd) return;\n\n this.options.verticalEdgeWheel?.(event.deltaY);\n event.preventDefault();\n };\n\n private clampInlineOffset(): void {\n const viewport = this.connectedViewport;\n if (!viewport) return;\n const clamped = Math.min(this.maxInlineOffset, Math.max(0, viewport.scrollLeft));\n if (viewport.scrollLeft !== clamped) viewport.scrollLeft = clamped;\n }\n\n private clampBlockOffset(): void {\n const viewport = this.connectedViewport;\n if (!viewport) return;\n const clamped = Math.min(this.maxBlockOffset, Math.max(0, viewport.scrollTop));\n if (viewport.scrollTop !== clamped) viewport.scrollTop = clamped;\n }\n\n private sync(): void {\n const elements = this.options.elements();\n const viewport = elements.viewport;\n if (!viewport) return;\n\n // Read every layout value before writing styles or reactive state.\n const mode = this.options.mode();\n const measureCollapse = mode.floatingCollapseAll && !mode.documentStickyHeader &&\n elements.collapseAllOverlay && elements.frame && elements.interactiveHead;\n const metrics = resolveTableLayoutMetrics({\n viewportInlineSize: viewport.clientWidth,\n viewportBlockSize: viewport.clientHeight,\n scrollInlineSize: viewport.scrollWidth,\n scrollBlockSize: viewport.scrollHeight,\n scrollInlineOffset: viewport.scrollLeft,\n collapseHeadBlockStart: measureCollapse\n ? elements.interactiveHead!.getBoundingClientRect().top\n : undefined,\n collapseFrameBlockStart: measureCollapse\n ? elements.frame!.getBoundingClientRect().top\n : undefined,\n });\n this.inlineOffset = metrics.inlineOffset;\n this.maxInlineOffset = metrics.maxInlineOffset;\n this.maxBlockOffset = Math.max(0, viewport.scrollHeight - viewport.clientHeight);\n\n this.setProperty(\n viewport,\n '--ds-table-visible-inline-size',\n `${metrics.visibleInlineSize}px`,\n );\n if (elements.stickyHeaderTable) {\n this.setProperty(\n elements.stickyHeaderTable,\n '--ds-table-inline-scroll-offset',\n `${metrics.inlineOffset}px`,\n );\n this.setProperty(\n elements.stickyHeaderTable,\n '--ds-table-inline-scroll-max-offset',\n `${metrics.maxInlineOffset}px`,\n );\n }\n if (metrics.collapseBlockOffset !== null && elements.collapseAllOverlay) {\n this.setProperty(\n elements.collapseAllOverlay,\n '--ds-table-collapse-all-block-offset',\n `${metrics.collapseBlockOffset}px`,\n );\n }\n\n if (\n !this.overflow ||\n this.overflow.start !== metrics.overflow.start ||\n this.overflow.end !== metrics.overflow.end ||\n this.overflow.scrollable !== metrics.overflow.scrollable\n ) {\n this.overflow = metrics.overflow;\n this.options.overflowChanged(metrics.overflow);\n }\n }\n\n private setProperty(element: HTMLElement, property: string, value: string): void {\n if (element.style.getPropertyValue(property) !== value) {\n element.style.setProperty(property, value);\n }\n }\n}\n",
1333
+ "content": "export interface TableLayoutElements {\n viewport: HTMLElement | null;\n contentTable: HTMLTableElement | null;\n stickyHeaderTable: HTMLTableElement | null;\n collapseAllOverlay: HTMLElement | null;\n frame: HTMLElement | null;\n interactiveHead: HTMLTableSectionElement | null;\n}\n\nexport interface TableLayoutMode {\n documentStickyHeader: boolean;\n floatingCollapseAll: boolean;\n clampVerticalOverscroll: boolean;\n}\n\nexport interface TableOverflowState {\n start: boolean;\n end: boolean;\n scrollable: boolean;\n}\n\nexport interface TableLayoutControllerOptions {\n elements: () => TableLayoutElements;\n mode: () => TableLayoutMode;\n overflowChanged: (state: TableOverflowState) => void;\n verticalEdgeWheel?: (deltaY: number) => boolean;\n}\n\nexport interface TableLayoutMetricInput {\n viewportInlineSize: number;\n viewportBlockSize: number;\n scrollInlineSize: number;\n scrollBlockSize: number;\n scrollInlineOffset: number;\n collapseHeadBlockStart?: number;\n collapseFrameBlockStart?: number;\n}\n\nexport interface TableLayoutMetrics {\n visibleInlineSize: number;\n overflow: TableOverflowState;\n inlineOffset: number;\n maxInlineOffset: number;\n collapseBlockOffset: number | null;\n}\n\n/** Pure geometry resolution shared by the controller and focused tests. */\nexport function resolveTableLayoutMetrics(input: TableLayoutMetricInput): TableLayoutMetrics {\n const horizontalOverflow = input.scrollInlineSize - input.viewportInlineSize > 1;\n return {\n visibleInlineSize: input.viewportInlineSize,\n overflow: {\n start: horizontalOverflow && input.scrollInlineOffset > 1,\n end:\n horizontalOverflow &&\n input.scrollInlineOffset + input.viewportInlineSize < input.scrollInlineSize - 1,\n scrollable: horizontalOverflow || input.scrollBlockSize - input.viewportBlockSize > 1,\n },\n inlineOffset: input.scrollInlineOffset,\n maxInlineOffset: Math.max(0, input.scrollInlineSize - input.viewportInlineSize),\n collapseBlockOffset:\n input.collapseHeadBlockStart == null || input.collapseFrameBlockStart == null\n ? null\n : Math.max(0, input.collapseHeadBlockStart - input.collapseFrameBlockStart),\n };\n}\n\n/**\n * Owns table viewport observation. It resolves newly rendered geometry\n * immediately so the first paint has the correct viewport-width chrome, then\n * batches observer-driven scroll and resize reads/writes into one animation frame.\n * The component remains responsible only for refs and reactive overflow state.\n */\nexport class TableLayoutController {\n private connected = false;\n private connectedViewport: HTMLElement | null = null;\n private connectedContentTable: HTMLTableElement | null = null;\n private resizeObserver: ResizeObserver | null = null;\n private frame: number | null = null;\n private overflow: TableOverflowState | null = null;\n private inlineOffset = 0;\n private maxInlineOffset = 0;\n private maxBlockOffset = 0;\n private lastMode: TableLayoutMode | null = null;\n\n constructor(private readonly options: TableLayoutControllerOptions) {}\n\n connect(): void {\n this.connected = true;\n this.refresh();\n }\n\n disconnect(): void {\n this.connected = false;\n if (this.frame !== null) cancelAnimationFrame(this.frame);\n this.frame = null;\n this.connectedViewport?.removeEventListener('scroll', this.handleScroll);\n this.connectedViewport?.removeEventListener('wheel', this.handleWheel);\n this.connectedViewport = null;\n this.connectedContentTable = null;\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n }\n\n /** Reconcile newly rendered refs, observers, and pending geometry. */\n refresh(forceGeometry = true): void {\n if (!this.connected) return;\n const { viewport, contentTable } = this.options.elements();\n const elementsChanged =\n viewport !== this.connectedViewport || contentTable !== this.connectedContentTable;\n if (elementsChanged) {\n this.connectedViewport?.removeEventListener('scroll', this.handleScroll);\n this.connectedViewport?.removeEventListener('wheel', this.handleWheel);\n this.resizeObserver?.disconnect();\n this.connectedViewport = viewport;\n this.connectedContentTable = contentTable;\n viewport?.addEventListener('scroll', this.handleScroll, { passive: true });\n viewport?.addEventListener('wheel', this.handleWheel, { passive: false });\n\n if (typeof ResizeObserver !== 'undefined' && viewport) {\n this.resizeObserver ??= new ResizeObserver(this.schedule);\n this.resizeObserver.observe(viewport);\n if (contentTable) this.resizeObserver.observe(contentTable);\n }\n }\n const mode = this.options.mode();\n const modeChanged =\n !this.lastMode ||\n mode.documentStickyHeader !== this.lastMode.documentStickyHeader ||\n mode.floatingCollapseAll !== this.lastMode.floatingCollapseAll ||\n mode.clampVerticalOverscroll !== this.lastMode.clampVerticalOverscroll;\n this.lastMode = mode;\n // New refs and explicit refreshes resolve before paint. A recycled virtual\n // window keeps the same scroll geometry, so ResizeObserver owns later size\n // changes without forcing layout after every row patch.\n if (forceGeometry || elementsChanged || modeChanged) this.sync();\n }\n\n private schedule = (): void => {\n if (!this.connected || this.frame !== null) return;\n this.frame = requestAnimationFrame(() => {\n this.frame = null;\n this.sync();\n });\n };\n\n private readonly handleScroll = (): void => {\n const viewport = this.connectedViewport;\n if (!viewport) return;\n const mode = this.options.mode();\n this.clampInlineOffset();\n if (mode.clampVerticalOverscroll) this.clampBlockOffset();\n if (\n viewport.scrollLeft !== this.inlineOffset ||\n (mode.floatingCollapseAll && !mode.documentStickyHeader)\n ) {\n this.schedule();\n }\n };\n\n private readonly handleWheel = (event: WheelEvent): void => {\n const viewport = this.connectedViewport;\n if (!viewport) return;\n\n if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) {\n const atStart = viewport.scrollLeft <= 0 && event.deltaX < 0;\n const atEnd = viewport.scrollLeft >= this.maxInlineOffset && event.deltaX > 0;\n if (atStart || atEnd) event.preventDefault();\n return;\n }\n\n if (!this.options.mode().clampVerticalOverscroll || event.deltaY === 0) return;\n const atStart = viewport.scrollTop <= 0 && event.deltaY < 0;\n const atEnd = viewport.scrollTop >= this.maxBlockOffset && event.deltaY > 0;\n if (!atStart && !atEnd) return;\n\n this.options.verticalEdgeWheel?.(event.deltaY);\n event.preventDefault();\n };\n\n private clampInlineOffset(): void {\n const viewport = this.connectedViewport;\n if (!viewport) return;\n const clamped = Math.min(this.maxInlineOffset, Math.max(0, viewport.scrollLeft));\n if (viewport.scrollLeft !== clamped) viewport.scrollLeft = clamped;\n }\n\n private clampBlockOffset(): void {\n const viewport = this.connectedViewport;\n if (!viewport) return;\n const clamped = Math.min(this.maxBlockOffset, Math.max(0, viewport.scrollTop));\n if (viewport.scrollTop !== clamped) viewport.scrollTop = clamped;\n }\n\n private sync(): void {\n const elements = this.options.elements();\n const viewport = elements.viewport;\n if (!viewport) return;\n\n // Read every layout value before writing styles or reactive state.\n const mode = this.options.mode();\n const measureCollapse =\n mode.floatingCollapseAll &&\n !mode.documentStickyHeader &&\n elements.collapseAllOverlay &&\n elements.frame &&\n elements.interactiveHead;\n const metrics = resolveTableLayoutMetrics({\n viewportInlineSize: viewport.clientWidth,\n viewportBlockSize: viewport.clientHeight,\n scrollInlineSize: viewport.scrollWidth,\n scrollBlockSize: viewport.scrollHeight,\n scrollInlineOffset: viewport.scrollLeft,\n collapseHeadBlockStart: measureCollapse\n ? elements.interactiveHead!.getBoundingClientRect().top\n : undefined,\n collapseFrameBlockStart: measureCollapse\n ? elements.frame!.getBoundingClientRect().top\n : undefined,\n });\n this.inlineOffset = metrics.inlineOffset;\n this.maxInlineOffset = metrics.maxInlineOffset;\n this.maxBlockOffset = Math.max(0, viewport.scrollHeight - viewport.clientHeight);\n\n this.setProperty(viewport, '--ds-table-visible-inline-size', `${metrics.visibleInlineSize}px`);\n if (elements.stickyHeaderTable) {\n this.setProperty(\n elements.stickyHeaderTable,\n '--ds-table-inline-scroll-offset',\n `${metrics.inlineOffset}px`\n );\n this.setProperty(\n elements.stickyHeaderTable,\n '--ds-table-inline-scroll-max-offset',\n `${metrics.maxInlineOffset}px`\n );\n }\n if (metrics.collapseBlockOffset !== null && elements.collapseAllOverlay) {\n this.setProperty(\n elements.collapseAllOverlay,\n '--ds-table-collapse-all-block-offset',\n `${metrics.collapseBlockOffset}px`\n );\n }\n\n if (\n !this.overflow ||\n this.overflow.start !== metrics.overflow.start ||\n this.overflow.end !== metrics.overflow.end ||\n this.overflow.scrollable !== metrics.overflow.scrollable\n ) {\n this.overflow = metrics.overflow;\n this.options.overflowChanged(metrics.overflow);\n }\n }\n\n private setProperty(element: HTMLElement, property: string, value: string): void {\n if (element.style.getPropertyValue(property) !== value) {\n element.style.setProperty(property, value);\n }\n }\n}\n",
1334
1334
  "type": "registry:ui"
1335
1335
  },
1336
1336
  {
1337
1337
  "path": "src/wc/components/Table/table-load-controller.ts",
1338
- "content": "import type {\n TableLoadMoreDetail,\n TableLoadMoreMode,\n TableLoadMoreReason,\n} from './table-types';\n\nexport interface TableLoadControllerState {\n lazyLoading: boolean;\n loadMoreMode: TableLoadMoreMode;\n hasMore: boolean;\n loadingMore: boolean;\n loadMoreError: string | undefined;\n loadIdentity: string | number;\n loadMoreThreshold: number;\n containedScroll: boolean;\n loadingMoreLabel: string;\n endOfResultsLabel: string;\n rowsLoadedLabel: string;\n loadedRowCount: number;\n viewport: HTMLElement | null;\n sentinel: HTMLElement | null;\n}\n\nexport interface TableLoadControllerOptions {\n state: () => TableLoadControllerState;\n announce: (message: string) => void;\n request: (detail: TableLoadMoreDetail) => void;\n}\n\n/** Owns incremental-loading request guards, announcements, and observation. */\nexport class TableLoadController {\n readonly intersectionSupported = typeof IntersectionObserver !== 'undefined';\n\n private connected = false;\n private intersectionObserver: IntersectionObserver | null = null;\n private observedSentinel: HTMLElement | null = null;\n private requestPending = false;\n private requestedRowCount = 0;\n private previousLoadedRowCount = 0;\n\n constructor(private readonly options: TableLoadControllerOptions) {}\n\n initialize(): void {\n this.previousLoadedRowCount = this.options.state().loadedRowCount;\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.lazyLoading ||\n state.loadMoreMode !== 'auto' ||\n !state.hasMore ||\n !!state.loadMoreError?.trim() ||\n !this.intersectionSupported ||\n !state.viewport ||\n !state.sentinel\n ) {\n this.disconnectObserver();\n return;\n }\n\n if (this.intersectionObserver && this.observedSentinel === state.sentinel) return;\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 if (entries.some(entry => entry.isIntersecting)) this.request('auto');\n },\n {\n root: state.containedScroll ? state.viewport : null,\n rootMargin: `0px 0px ${threshold}px 0px`,\n },\n );\n this.observedSentinel = state.sentinel;\n this.intersectionObserver.observe(state.sentinel);\n }\n\n structureChanged(): void {\n this.resetRequest();\n }\n\n dataChanged(): void {\n const state = this.options.state();\n const nextCount = state.loadedRowCount;\n if (!state.lazyLoading) {\n this.previousLoadedRowCount = nextCount;\n this.resetRequest();\n return;\n }\n if (nextCount > this.previousLoadedRowCount) {\n const added = nextCount - this.previousLoadedRowCount;\n this.options.announce(\n state.rowsLoadedLabel\n .replace('{count}', String(added))\n .replace('{total}', String(nextCount)),\n );\n }\n if (nextCount > this.requestedRowCount) this.requestPending = false;\n this.previousLoadedRowCount = nextCount;\n this.disconnectObserver();\n }\n\n identityChanged(): void {\n this.previousLoadedRowCount = this.options.state().loadedRowCount;\n this.resetRequest();\n }\n\n configurationChanged(): void {\n this.disconnectObserver();\n }\n\n loadingChanged(loading: boolean): void {\n const state = this.options.state();\n if (!state.lazyLoading) {\n this.resetRequest();\n return;\n }\n if (loading) {\n this.requestPending = true;\n this.options.announce(state.loadingMoreLabel);\n } else if (state.loadMoreMode === 'manual') {\n // A completed manual request may append no rows; permit another explicit activation.\n this.requestPending = false;\n }\n this.disconnectObserver();\n }\n\n errorChanged(error: string | undefined): void {\n if (!this.options.state().lazyLoading) {\n this.resetRequest();\n return;\n }\n if (error?.trim()) {\n this.requestPending = false;\n this.options.announce(error);\n }\n this.disconnectObserver();\n }\n\n hasMoreChanged(hasMore: boolean, hadMore: boolean): void {\n if (!this.options.state().lazyLoading) {\n this.resetRequest();\n return;\n }\n if (!hasMore) {\n this.requestPending = false;\n if (hadMore) this.options.announce(this.options.state().endOfResultsLabel);\n }\n this.disconnectObserver();\n }\n\n request(reason: TableLoadMoreReason): void {\n const state = this.options.state();\n if (\n !state.lazyLoading ||\n !state.hasMore ||\n state.loadingMore ||\n (reason !== 'retry' && !!state.loadMoreError?.trim()) ||\n this.requestPending\n ) return;\n\n this.requestPending = true;\n this.requestedRowCount = state.loadedRowCount;\n this.options.announce(state.loadingMoreLabel);\n this.options.request({\n reason,\n loadIdentity: state.loadIdentity,\n loadedRowCount: this.requestedRowCount,\n });\n }\n\n private resetRequest(): void {\n this.requestPending = false;\n this.requestedRowCount = this.options.state().loadedRowCount;\n this.disconnectObserver();\n }\n\n private disconnectObserver(): void {\n this.intersectionObserver?.disconnect();\n this.intersectionObserver = null;\n this.observedSentinel = null;\n }\n}\n",
1338
+ "content": "import type { TableLoadMoreDetail, TableLoadMoreMode, TableLoadMoreReason } from './table-types';\n\nexport interface TableLoadControllerState {\n lazyLoading: boolean;\n loadMoreMode: TableLoadMoreMode;\n hasMore: boolean;\n loadingMore: boolean;\n loadMoreError: string | undefined;\n loadIdentity: string | number;\n loadMoreThreshold: number;\n containedScroll: boolean;\n loadingMoreLabel: string;\n endOfResultsLabel: string;\n rowsLoadedLabel: string;\n loadedRowCount: number;\n viewport: HTMLElement | null;\n sentinel: HTMLElement | null;\n}\n\nexport interface TableLoadControllerOptions {\n state: () => TableLoadControllerState;\n announce: (message: string) => void;\n request: (detail: TableLoadMoreDetail) => void;\n}\n\n/** Owns incremental-loading request guards, announcements, and observation. */\nexport class TableLoadController {\n readonly intersectionSupported = typeof IntersectionObserver !== 'undefined';\n\n private connected = false;\n private intersectionObserver: IntersectionObserver | null = null;\n private observedSentinel: HTMLElement | null = null;\n private requestPending = false;\n private requestedRowCount = 0;\n private previousLoadedRowCount = 0;\n\n constructor(private readonly options: TableLoadControllerOptions) {}\n\n initialize(): void {\n this.previousLoadedRowCount = this.options.state().loadedRowCount;\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.lazyLoading ||\n state.loadMoreMode !== 'auto' ||\n !state.hasMore ||\n !!state.loadMoreError?.trim() ||\n !this.intersectionSupported ||\n !state.viewport ||\n !state.sentinel\n ) {\n this.disconnectObserver();\n return;\n }\n\n if (this.intersectionObserver && this.observedSentinel === state.sentinel) return;\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 if (entries.some(entry => entry.isIntersecting)) this.request('auto');\n },\n {\n root: state.containedScroll ? state.viewport : null,\n rootMargin: `0px 0px ${threshold}px 0px`,\n }\n );\n this.observedSentinel = state.sentinel;\n this.intersectionObserver.observe(state.sentinel);\n }\n\n structureChanged(): void {\n this.resetRequest();\n }\n\n dataChanged(): void {\n const state = this.options.state();\n const nextCount = state.loadedRowCount;\n if (!state.lazyLoading) {\n this.previousLoadedRowCount = nextCount;\n this.resetRequest();\n return;\n }\n if (nextCount > this.previousLoadedRowCount) {\n const added = nextCount - this.previousLoadedRowCount;\n this.options.announce(\n state.rowsLoadedLabel\n .replace('{count}', String(added))\n .replace('{total}', String(nextCount))\n );\n }\n if (nextCount > this.requestedRowCount) this.requestPending = false;\n this.previousLoadedRowCount = nextCount;\n this.disconnectObserver();\n }\n\n identityChanged(): void {\n this.previousLoadedRowCount = this.options.state().loadedRowCount;\n this.resetRequest();\n }\n\n configurationChanged(): void {\n this.disconnectObserver();\n }\n\n loadingChanged(loading: boolean): void {\n const state = this.options.state();\n if (!state.lazyLoading) {\n this.resetRequest();\n return;\n }\n if (loading) {\n this.requestPending = true;\n this.options.announce(state.loadingMoreLabel);\n } else if (state.loadMoreMode === 'manual') {\n // A completed manual request may append no rows; permit another explicit activation.\n this.requestPending = false;\n }\n this.disconnectObserver();\n }\n\n errorChanged(error: string | undefined): void {\n if (!this.options.state().lazyLoading) {\n this.resetRequest();\n return;\n }\n if (error?.trim()) {\n this.requestPending = false;\n this.options.announce(error);\n }\n this.disconnectObserver();\n }\n\n hasMoreChanged(hasMore: boolean, hadMore: boolean): void {\n if (!this.options.state().lazyLoading) {\n this.resetRequest();\n return;\n }\n if (!hasMore) {\n this.requestPending = false;\n if (hadMore) this.options.announce(this.options.state().endOfResultsLabel);\n }\n this.disconnectObserver();\n }\n\n request(reason: TableLoadMoreReason): void {\n const state = this.options.state();\n if (\n !state.lazyLoading ||\n !state.hasMore ||\n state.loadingMore ||\n (reason !== 'retry' && !!state.loadMoreError?.trim()) ||\n this.requestPending\n )\n return;\n\n this.requestPending = true;\n this.requestedRowCount = state.loadedRowCount;\n this.options.announce(state.loadingMoreLabel);\n this.options.request({\n reason,\n loadIdentity: state.loadIdentity,\n loadedRowCount: this.requestedRowCount,\n });\n }\n\n private resetRequest(): void {\n this.requestPending = false;\n this.requestedRowCount = this.options.state().loadedRowCount;\n this.disconnectObserver();\n }\n\n private disconnectObserver(): void {\n this.intersectionObserver?.disconnect();\n this.intersectionObserver = null;\n this.observedSentinel = null;\n }\n}\n",
1339
1339
  "type": "registry:ui"
1340
1340
  },
1341
1341
  {
1342
1342
  "path": "src/wc/components/Table/table-load-view.tsx",
1343
- "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\n class=\"ds-table__load-content\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n >\n {options.endOfResultsLabel}\n </ds-text>\n );\n}\n",
1343
+ "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",
1344
1344
  "type": "registry:ui"
1345
1345
  },
1346
1346
  {
1347
1347
  "path": "src/wc/components/Table/table-model.ts",
1348
- "content": "import type {\n TableCellBlank,\n TableCellAction,\n TableCellEmpty,\n TableCellIcon,\n TableCellIconText,\n TableCellImage,\n TableCellPrimaryText,\n TableCellText,\n TableCellTag,\n TableCellValue,\n TableColumn,\n TableGroup,\n TableRow,\n TableSortState,\n} from './table-types';\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 typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'primary-text';\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 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 typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'icon-text';\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 (isTableCellIcon(value)) return null;\n if (isTableCellIconText(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(\n collapsedGroupIds: string[],\n groupId: string,\n): 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 =\n | { columnId: string; mode: 'action' }\n | { mode: 'floating' };\n\nexport function tableCollapseAllHost(\n columns: TableColumn[],\n): 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 = Number.isFinite(column.minSize) && (column.minSize ?? 0) > 0\n ? column.minSize!\n : 0;\n const maximum = 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 * When every column has an explicit width, one ordinary lane must absorb spare\n * table width so fixed selection and action lanes are not expanded by the\n * native fixed-table layout algorithm.\n */\nexport function tableFlexibleColumnId(columns: TableColumn[]): string | undefined {\n if (columns.some(column => !tableColumnSize(column))) return undefined;\n for (let index = columns.length - 1; index >= 0; index -= 1) {\n const column = columns[index]!;\n if (column.kind !== 'action' && !column.sticky) return column.id;\n }\n for (let index = columns.length - 1; index >= 0; index -= 1) {\n const column = columns[index]!;\n if (column.kind !== 'action') return column.id;\n }\n return columns[columns.length - 1]?.id;\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\nexport const TABLE_GROUP_INTENTS = [\n 'brand',\n 'neutral',\n 'negative',\n 'warning',\n 'caution',\n 'positive',\n] as const;\n\nexport function isTableGroupIntent(\n value: unknown,\n): value is (typeof TABLE_GROUP_INTENTS)[number] {\n return (\n typeof value === 'string' &&\n (TABLE_GROUP_INTENTS as readonly string[]).includes(value)\n );\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 (let ancestor = node.parentElement; ancestor && ancestor !== host; ancestor = ancestor.parentElement) {\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(`Column ${column.id || '(missing id)'} requires a visible header or headerLabel.`);\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 }\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",
1348
+ "content": "import type {\n TableCellBlank,\n TableCellAction,\n TableCellEmpty,\n TableCellIcon,\n TableCellIconText,\n TableCellImage,\n TableCellPrimaryText,\n TableCellText,\n TableCellTag,\n TableCellValue,\n TableColumn,\n TableGroup,\n TableRow,\n TableSortState,\n} from './table-types';\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 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 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 (isTableCellIcon(value)) return null;\n if (isTableCellIconText(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 * When every column has an explicit width, one ordinary lane must absorb spare\n * table width so fixed selection and action lanes are not expanded by the\n * native fixed-table layout algorithm.\n */\nexport function tableFlexibleColumnId(columns: TableColumn[]): string | undefined {\n if (columns.some(column => !tableColumnSize(column))) return undefined;\n for (let index = columns.length - 1; index >= 0; index -= 1) {\n const column = columns[index]!;\n if (column.kind !== 'action' && !column.sticky) return column.id;\n }\n for (let index = columns.length - 1; index >= 0; index -= 1) {\n const column = columns[index]!;\n if (column.kind !== 'action') return column.id;\n }\n return columns[columns.length - 1]?.id;\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\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 }\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",
1349
1349
  "type": "registry:ui"
1350
1350
  },
1351
1351
  {
@@ -1360,42 +1360,42 @@
1360
1360
  },
1361
1361
  {
1362
1362
  "path": "src/wc/components/Table/table-render-model.ts",
1363
- "content": "import {\n deriveTableSelectionState,\n isTableGroupIntent,\n resolvedTableGroupCount,\n tableCollapseAllHost,\n tableColumnSize,\n tableExplicitMinWidth,\n tableFlexibleColumnId,\n tableGroupIntentClass,\n tableGroupLabelColor,\n tableRows,\n type TableCollapseAllHost,\n type TableSelectionState,\n} from './table-model';\nimport type {\n TableColumn,\n TableGroup,\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 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 flexibleColumnId: string | 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 = input.grouped && 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 flexibleColumnId = tableFlexibleColumnId(input.columns);\n const gridTracks = [\n ...(selectable ? ['var(--_table-selection-column-inline-size)'] : []),\n ...input.columns.map(column => {\n const width = tableColumnSize(column);\n if (!width) return 'minmax(0, 1fr)';\n return column.id === flexibleColumnId ? `minmax(${width}, 1fr)` : width;\n }),\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 totalPresentation = input.groupCountPresentation === 'total';\n const count = resolvedTableGroupCount(group);\n const totalLabel = group.countLabel ?? `${count} ${count === 1 ? 'item' : 'items'}`;\n const intent = isTableGroupIntent(group.intent) ? group.intent : undefined;\n return {\n group,\n count,\n loadedCount,\n visibleCountText: totalPresentation ? String(count) : `${loadedCount} of ${count}`,\n countLabel: totalPresentation ? totalLabel : `${loadedCount} of ${totalLabel} loaded`,\n intent,\n intentClass: tableGroupIntentClass(intent),\n labelColor: tableGroupLabelColor(intent),\n collapsed: collapsedGroupIds.has(group.id),\n selection: selectable\n ? deriveTableSelectionState(group.rows, input.selectedRowIds)\n : null,\n };\n }),\n totalColumns: input.columns.length + (selectable ? 1 : 0),\n allGroupsCollapsed,\n showCollapseAll,\n collapseAllHost: showCollapseAll ? tableCollapseAllHost(input.columns) : undefined,\n flexibleColumnId,\n tableStyle,\n };\n}\n",
1363
+ "content": "import {\n deriveTableSelectionState,\n isTableGroupIntent,\n resolvedTableGroupCount,\n tableCollapseAllHost,\n tableColumnSize,\n tableExplicitMinWidth,\n tableFlexibleColumnId,\n tableGroupIntentClass,\n tableGroupLabelColor,\n tableRows,\n type TableCollapseAllHost,\n type TableSelectionState,\n} from './table-model';\nimport type {\n TableColumn,\n TableGroup,\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 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 flexibleColumnId: string | 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 flexibleColumnId = tableFlexibleColumnId(input.columns);\n const gridTracks = [\n ...(selectable ? ['var(--_table-selection-column-inline-size)'] : []),\n ...input.columns.map(column => {\n const width = tableColumnSize(column);\n if (!width) return 'minmax(0, 1fr)';\n return column.id === flexibleColumnId ? `minmax(${width}, 1fr)` : width;\n }),\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 totalPresentation = input.groupCountPresentation === 'total';\n const count = resolvedTableGroupCount(group);\n const totalLabel = group.countLabel ?? `${count} ${count === 1 ? 'item' : 'items'}`;\n const intent = isTableGroupIntent(group.intent) ? group.intent : undefined;\n return {\n group,\n count,\n loadedCount,\n visibleCountText: totalPresentation ? String(count) : `${loadedCount} of ${count}`,\n countLabel: totalPresentation ? totalLabel : `${loadedCount} of ${totalLabel} loaded`,\n intent,\n intentClass: tableGroupIntentClass(intent),\n labelColor: tableGroupLabelColor(intent),\n collapsed: collapsedGroupIds.has(group.id),\n selection: selectable ? deriveTableSelectionState(group.rows, input.selectedRowIds) : null,\n };\n }),\n totalColumns: input.columns.length + (selectable ? 1 : 0),\n allGroupsCollapsed,\n showCollapseAll,\n collapseAllHost: showCollapseAll ? tableCollapseAllHost(input.columns) : undefined,\n flexibleColumnId,\n tableStyle,\n };\n}\n",
1364
1364
  "type": "registry:ui"
1365
1365
  },
1366
1366
  {
1367
1367
  "path": "src/wc/components/Table/table-row-view.tsx",
1368
- "content": "import { h } from '@stencil/core';\nimport {\n resolveTableCellPresentation,\n tableCellTextOverflowProps,\n type TableCellPresentation,\n} from './table-cell-model';\nimport { isRenderableTableActionMenu, isTableCellActionMenu, tableActionTriggerId } from './table-action-menu';\nimport { tableRowSelectionLabel } from './table-model';\nimport type { TableRenderModel } from './table-render-model';\nimport type {\n TableCellActionDetail,\n TableCellTextRun,\n TableColumn,\n TableRow,\n} 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 ariaRowIndex?: number;\n variableVirtualSize?: boolean;\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 ariaRowIndex,\n variableVirtualSize = false,\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 rowSelectable = row.selectable !== false && !row.disabled;\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': !!row.interactive && !row.disabled,\n 'ds-focus-ring': !!row.interactive && !row.disabled,\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 aria-rowindex={ariaRowIndex}\n aria-disabled={row.disabled ? 'true' : undefined}\n tabIndex={row.interactive && !row.disabled ? 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 }}\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 {visibleColumns.map(column =>\n renderTableCell({\n row,\n column,\n selected,\n emptyCellLabel,\n actionMenuElementId,\n actionMenu,\n renderStickyEdge,\n onCellAction,\n onActionMenuToggle,\n }),\n )}\n </tr>\n );\n}\n\ninterface TableCellViewOptions {\n row: TableRow;\n column: TableColumn;\n selected: boolean;\n emptyCellLabel: string;\n actionMenuElementId: string;\n actionMenu: { rowId: string; columnId: string } | null;\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, 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 iconCell = cell.kind === 'icon';\n const iconTextCell = cell.kind === 'icon-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 textVariant = textCell ? cell.variant : undefined;\n const imageVariant = imageCell ? cell.variant : undefined;\n const iconTextVariant = iconTextCell ? cell.variant : undefined;\n const wraps = (textCell || iconTextCell) && 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--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--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 }}\n data-column-id={column.id}\n data-cell-type={cell.cellType}\n data-cell-variant={tagVariant ?? textVariant ?? imageVariant ?? iconTextVariant}\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 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\n name={value.icon}\n size=\"md\"\n color={value.color ?? 'secondary'}\n label={value.label}\n />\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)}\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 class=\"ds-table__cell-image-content\" src={value.src} alt={value.alt} loading=\"lazy\" />\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 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 {value.text}\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 !== 'text') return null;\n return renderTextCopy(cell);\n}\n\nfunction renderTextCopy(cell: Extract<TableCellPresentation, { kind: 'text' | 'icon-text' }>) {\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 {text.primary}\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 })}\n {renderTextTrack(text.tertiary, {\n track: 'tertiary',\n variant: 'text-body-small',\n defaultColor: 'secondary',\n wholeColor: text.tertiaryColor,\n lineClamp: cell.lineClamp,\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<TableCellPresentation, { kind: 'text' | 'icon-text' }>['lineClamp'];\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 if (runs.length === 1) {\n return (\n <ds-text\n class={trackClass}\n as=\"span\"\n variant={options.variant}\n color={colorFor(runs[0])}\n lineTruncation={overflow.lineTruncation}\n wrap={overflow.wrap}\n data-table-truncate={truncateAttr(options.lineClamp)}\n >\n {runs[0].text}\n </ds-text>\n );\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 <ds-text\n key={`${options.track}-run-${index}`}\n class=\"ds-table__cell-run\"\n as=\"span\"\n variant={options.variant}\n color={colorFor(run)}\n lineTruncation={overflow.lineTruncation}\n wrap={overflow.wrap}\n data-table-truncate={truncateAttr(options.lineClamp)}\n >\n {run.text}\n </ds-text>,\n ])}\n </span>\n );\n}\n\nfunction truncateAttr(lineClamp: Extract<TableCellPresentation, { kind: 'text' | 'icon-text' }>['lineClamp']) {\n return lineClamp === 'none' ? undefined : '';\n}\n",
1368
+ "content": "import { h } from '@stencil/core';\nimport {\n resolveTableCellPresentation,\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 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 ariaRowIndex?: number;\n variableVirtualSize?: boolean;\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 ariaRowIndex,\n variableVirtualSize = false,\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 rowSelectable = row.selectable !== false && !row.disabled;\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': !!row.interactive && !row.disabled,\n 'ds-focus-ring': !!row.interactive && !row.disabled,\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 aria-rowindex={ariaRowIndex}\n aria-disabled={row.disabled ? 'true' : undefined}\n tabIndex={row.interactive && !row.disabled ? 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 }}\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 {visibleColumns.map(column =>\n renderTableCell({\n row,\n column,\n selected,\n emptyCellLabel,\n actionMenuElementId,\n actionMenu,\n renderStickyEdge,\n onCellAction,\n onActionMenuToggle,\n })\n )}\n </tr>\n );\n}\n\ninterface TableCellViewOptions {\n row: TableRow;\n column: TableColumn;\n selected: boolean;\n emptyCellLabel: string;\n actionMenuElementId: string;\n actionMenu: { rowId: string; columnId: string } | null;\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, 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 iconCell = cell.kind === 'icon';\n const iconTextCell = cell.kind === 'icon-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 textVariant = textCell ? cell.variant : undefined;\n const imageVariant = imageCell ? cell.variant : undefined;\n const iconTextVariant = iconTextCell ? cell.variant : undefined;\n const wraps = (textCell || iconTextCell) && 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--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--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 }}\n data-column-id={column.id}\n data-cell-type={cell.cellType}\n data-cell-variant={tagVariant ?? textVariant ?? imageVariant ?? iconTextVariant}\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 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)}\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 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 {value.text}\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 !== 'text') return null;\n return renderTextCopy(cell);\n}\n\nfunction renderTextCopy(cell: Extract<TableCellPresentation, { kind: 'text' | 'icon-text' }>) {\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 {text.primary}\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 })}\n {renderTextTrack(text.tertiary, {\n track: 'tertiary',\n variant: 'text-body-small',\n defaultColor: 'secondary',\n wholeColor: text.tertiaryColor,\n lineClamp: cell.lineClamp,\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<TableCellPresentation, { kind: 'text' | 'icon-text' }>['lineClamp'];\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 if (runs.length === 1) {\n return (\n <ds-text\n class={trackClass}\n as=\"span\"\n variant={options.variant}\n color={colorFor(runs[0])}\n lineTruncation={overflow.lineTruncation}\n wrap={overflow.wrap}\n data-table-truncate={truncateAttr(options.lineClamp)}\n >\n {runs[0].text}\n </ds-text>\n );\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 <ds-text\n key={`${options.track}-run-${index}`}\n class=\"ds-table__cell-run\"\n as=\"span\"\n variant={options.variant}\n color={colorFor(run)}\n lineTruncation={overflow.lineTruncation}\n wrap={overflow.wrap}\n data-table-truncate={truncateAttr(options.lineClamp)}\n >\n {run.text}\n </ds-text>,\n ])}\n </span>\n );\n}\n\nfunction truncateAttr(\n lineClamp: Extract<TableCellPresentation, { kind: 'text' | 'icon-text' }>['lineClamp']\n) {\n return lineClamp === 'none' ? undefined : '';\n}\n",
1369
1369
  "type": "registry:ui"
1370
1370
  },
1371
1371
  {
1372
1372
  "path": "src/wc/components/Table/table-skeleton-view.tsx",
1373
- "content": "import { h } from '@stencil/core';\nimport { resolveTableCellImageTracks, tableCellImageVariant } from './table-cell-model';\nimport type { TableRenderModel } from './table-render-model';\nimport type { TableCellSkeleton, TableColumn } from './table-types';\n\ninterface TableSkeletonViewOptions {\n model: TableRenderModel;\n visibleColumns: TableColumn[];\n skeletonRows: number;\n renderStickyEdge: (sticky: TableColumn['sticky']) => unknown;\n}\n\n/** Render the loading body from the same cell recipes used by loaded rows. */\nexport function renderTableSkeletonBody(options: TableSkeletonViewOptions) {\n const { model, visibleColumns, renderStickyEdge } = options;\n const count = Math.min(20, Math.max(1, Math.round(options.skeletonRows) || 1));\n return (\n <tbody class=\"ds-table__body ds-table__skeleton-body\">\n {Array.from({ length: count }, (_, index) => (\n <tr class=\"ds-table__row ds-table__skeleton-row\" key={`skeleton-${index}`}>\n {model.selectable && (\n <td\n class=\"ds-table__cell ds-table__selection-cell ds-table__cell--sticky-start ds-table__skeleton-cell ds-interaction-fill ds-interaction-fill--grouped\"\n data-skeleton-kind=\"checkbox\"\n >\n <span class=\"ds-table__skeleton-checkbox-canvas ds-interaction-fill__content\">\n <ds-skeleton\n class=\"ds-table__skeleton-checkbox\"\n variant=\"control\"\n controlSize=\"xs\"\n width=\"var(--dimension-iconography-sm)\"\n />\n </span>\n {renderStickyEdge('start')}\n </td>\n )}\n {visibleColumns.map(column => renderSkeletonCell(column, index, renderStickyEdge))}\n </tr>\n ))}\n </tbody>\n );\n}\n\nfunction renderSkeletonCell(\n column: TableColumn,\n rowIndex: number,\n renderStickyEdge: TableSkeletonViewOptions['renderStickyEdge'],\n) {\n const skeleton =\n column.skeleton ??\n ((column.kind === 'action'\n ? { kind: 'action', variant: 'icon' }\n : { kind: 'text', lines: 1 }) satisfies TableCellSkeleton);\n const align = column.align ?? 'start';\n const text = skeleton.kind === 'text';\n const iconText = skeleton.kind === 'icon-text';\n const lines = text || iconText ? (skeleton.lines ?? 1) : 1;\n const tag = skeleton.kind === 'tag';\n const icon = skeleton.kind === 'icon';\n const image = skeleton.kind === 'image';\n const imageVariant = image\n ? tableCellImageVariant(resolveTableCellImageTracks(skeleton.tracks))\n : undefined;\n const iconTextVariant = iconText\n ? lines === 3\n ? 'triple'\n : lines === 2\n ? 'multi'\n : 'single'\n : undefined;\n const action = skeleton.kind === 'action';\n const blank = skeleton.kind === 'blank';\n\n return (\n <td\n class={{\n 'ds-table__cell': true,\n [`ds-table__cell--align-${align}`]: true,\n 'ds-table__skeleton-cell': true,\n 'ds-table__cell--text-single': text && lines === 1,\n 'ds-table__cell--text-multi': text && lines === 2,\n 'ds-table__cell--text-triple': text && lines === 3,\n 'ds-table__cell--tag': tag,\n 'ds-table__cell--tag-tag-only': tag,\n 'ds-table__cell--icon': icon,\n 'ds-table__cell--icon-text': iconText,\n [`ds-table__cell--icon-text-${iconTextVariant}`]: iconText,\n 'ds-table__cell--image': image,\n [`ds-table__cell--image-${imageVariant}`]: image,\n 'ds-table__cell--action': action,\n 'ds-table__cell--blank': blank,\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 }}\n data-column-id={column.id}\n data-skeleton-kind={skeleton.kind}\n data-cell-variant={imageVariant ?? iconTextVariant}\n key={`skeleton-${rowIndex}:${column.id}`}\n >\n <span class=\"ds-table__cell-content ds-interaction-fill__content\">\n {renderSkeletonCellContent(skeleton)}\n </span>\n {renderStickyEdge(column.sticky)}\n </td>\n );\n}\n\nfunction renderSkeletonCellContent(skeleton: TableCellSkeleton) {\n if (skeleton.kind === 'blank') return null;\n\n if (skeleton.kind === 'image') {\n return (\n <span class=\"ds-table__cell-image ds-table__skeleton-image\">\n <ds-skeleton\n class=\"ds-table__skeleton-image-shape\"\n variant=\"control\"\n controlSize=\"md\"\n width=\"100%\"\n />\n </span>\n );\n }\n\n if (skeleton.kind === 'icon') {\n return <ds-skeleton variant=\"icon\" iconSize=\"md\" rounded={skeleton.rounded ?? false} />;\n }\n\n if (skeleton.kind === 'tag') {\n return <ds-skeleton variant=\"control\" controlSize=\"sm\" width={skeleton.width ?? '64%'} />;\n }\n\n if (skeleton.kind === 'action') {\n const iconOnly = (skeleton.variant ?? 'icon') === 'icon';\n return (\n <ds-skeleton\n variant=\"control\"\n controlSize=\"sm\"\n width={skeleton.width ?? (iconOnly ? '24px' : '72%')}\n />\n );\n }\n\n const lines = skeleton.lines ?? 1;\n const copy = (\n <span class=\"ds-table__cell-copy\">\n <span class=\"ds-table__cell-primary ds-table__cell-track ds-table__cell-track--text\">\n <ds-skeleton\n variant=\"text\"\n textVariant=\"text-body-medium\"\n width={skeleton.primaryWidth ?? '100%'}\n />\n </span>\n {lines >= 2 && (\n <span class=\"ds-table__cell-secondary ds-table__cell-track ds-table__cell-track--text\">\n <ds-skeleton\n variant=\"text\"\n textVariant=\"text-body-small\"\n width={skeleton.secondaryWidth ?? '72%'}\n />\n </span>\n )}\n {lines === 3 && (\n <span class=\"ds-table__cell-tertiary ds-table__cell-track ds-table__cell-track--text\">\n <ds-skeleton\n variant=\"text\"\n textVariant=\"text-body-small\"\n width={skeleton.tertiaryWidth ?? '56%'}\n />\n </span>\n )}\n </span>\n );\n if (skeleton.kind === 'icon-text') {\n return (\n <span class=\"ds-table__cell-icon-text\">\n <span class=\"ds-table__cell-icon-text-icon\">\n <ds-skeleton variant=\"icon\" iconSize=\"md\" />\n </span>\n {copy}\n </span>\n );\n }\n return copy;\n}\n",
1373
+ "content": "import { h } from '@stencil/core';\nimport { resolveTableCellImageTracks, tableCellImageVariant } from './table-cell-model';\nimport type { TableRenderModel } from './table-render-model';\nimport type { TableCellSkeleton, TableColumn } from './table-types';\n\ninterface TableSkeletonViewOptions {\n model: TableRenderModel;\n visibleColumns: TableColumn[];\n skeletonRows: number;\n renderStickyEdge: (sticky: TableColumn['sticky']) => unknown;\n}\n\n/** Render the loading body from the same cell recipes used by loaded rows. */\nexport function renderTableSkeletonBody(options: TableSkeletonViewOptions) {\n const { model, visibleColumns, renderStickyEdge } = options;\n const count = Math.min(20, Math.max(1, Math.round(options.skeletonRows) || 1));\n return (\n <tbody class=\"ds-table__body ds-table__skeleton-body\">\n {Array.from({ length: count }, (_, index) => (\n <tr class=\"ds-table__row ds-table__skeleton-row\" key={`skeleton-${index}`}>\n {model.selectable && (\n <td\n class=\"ds-table__cell ds-table__selection-cell ds-table__cell--sticky-start ds-table__skeleton-cell ds-interaction-fill ds-interaction-fill--grouped\"\n data-skeleton-kind=\"checkbox\"\n >\n <span class=\"ds-table__skeleton-checkbox-canvas ds-interaction-fill__content\">\n <ds-skeleton\n class=\"ds-table__skeleton-checkbox\"\n variant=\"control\"\n controlSize=\"xs\"\n width=\"var(--dimension-iconography-sm)\"\n />\n </span>\n {renderStickyEdge('start')}\n </td>\n )}\n {visibleColumns.map(column => renderSkeletonCell(column, index, renderStickyEdge))}\n </tr>\n ))}\n </tbody>\n );\n}\n\nfunction renderSkeletonCell(\n column: TableColumn,\n rowIndex: number,\n renderStickyEdge: TableSkeletonViewOptions['renderStickyEdge']\n) {\n const skeleton =\n column.skeleton ??\n ((column.kind === 'action'\n ? { kind: 'action', variant: 'icon' }\n : { kind: 'text', lines: 1 }) satisfies TableCellSkeleton);\n const align = column.align ?? 'start';\n const text = skeleton.kind === 'text';\n const iconText = skeleton.kind === 'icon-text';\n const lines = text || iconText ? (skeleton.lines ?? 1) : 1;\n const tag = skeleton.kind === 'tag';\n const icon = skeleton.kind === 'icon';\n const image = skeleton.kind === 'image';\n const imageVariant = image\n ? tableCellImageVariant(resolveTableCellImageTracks(skeleton.tracks))\n : undefined;\n const iconTextVariant = iconText\n ? lines === 3\n ? 'triple'\n : lines === 2\n ? 'multi'\n : 'single'\n : undefined;\n const action = skeleton.kind === 'action';\n const blank = skeleton.kind === 'blank';\n\n return (\n <td\n class={{\n 'ds-table__cell': true,\n [`ds-table__cell--align-${align}`]: true,\n 'ds-table__skeleton-cell': true,\n 'ds-table__cell--text-single': text && lines === 1,\n 'ds-table__cell--text-multi': text && lines === 2,\n 'ds-table__cell--text-triple': text && lines === 3,\n 'ds-table__cell--tag': tag,\n 'ds-table__cell--tag-tag-only': tag,\n 'ds-table__cell--icon': icon,\n 'ds-table__cell--icon-text': iconText,\n [`ds-table__cell--icon-text-${iconTextVariant}`]: iconText,\n 'ds-table__cell--image': image,\n [`ds-table__cell--image-${imageVariant}`]: image,\n 'ds-table__cell--action': action,\n 'ds-table__cell--blank': blank,\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 }}\n data-column-id={column.id}\n data-skeleton-kind={skeleton.kind}\n data-cell-variant={imageVariant ?? iconTextVariant}\n key={`skeleton-${rowIndex}:${column.id}`}\n >\n <span class=\"ds-table__cell-content ds-interaction-fill__content\">\n {renderSkeletonCellContent(skeleton)}\n </span>\n {renderStickyEdge(column.sticky)}\n </td>\n );\n}\n\nfunction renderSkeletonCellContent(skeleton: TableCellSkeleton) {\n if (skeleton.kind === 'blank') return null;\n\n if (skeleton.kind === 'image') {\n return (\n <span class=\"ds-table__cell-image ds-table__skeleton-image\">\n <ds-skeleton\n class=\"ds-table__skeleton-image-shape\"\n variant=\"control\"\n controlSize=\"md\"\n width=\"100%\"\n />\n </span>\n );\n }\n\n if (skeleton.kind === 'icon') {\n return <ds-skeleton variant=\"icon\" iconSize=\"md\" rounded={skeleton.rounded ?? false} />;\n }\n\n if (skeleton.kind === 'tag') {\n return <ds-skeleton variant=\"control\" controlSize=\"sm\" width={skeleton.width ?? '64%'} />;\n }\n\n if (skeleton.kind === 'action') {\n const iconOnly = (skeleton.variant ?? 'icon') === 'icon';\n return (\n <ds-skeleton\n variant=\"control\"\n controlSize=\"sm\"\n width={skeleton.width ?? (iconOnly ? '24px' : '72%')}\n />\n );\n }\n\n const lines = skeleton.lines ?? 1;\n const copy = (\n <span class=\"ds-table__cell-copy\">\n <span class=\"ds-table__cell-primary ds-table__cell-track ds-table__cell-track--text\">\n <ds-skeleton\n variant=\"text\"\n textVariant=\"text-body-medium\"\n width={skeleton.primaryWidth ?? '100%'}\n />\n </span>\n {lines >= 2 && (\n <span class=\"ds-table__cell-secondary ds-table__cell-track ds-table__cell-track--text\">\n <ds-skeleton\n variant=\"text\"\n textVariant=\"text-body-small\"\n width={skeleton.secondaryWidth ?? '72%'}\n />\n </span>\n )}\n {lines === 3 && (\n <span class=\"ds-table__cell-tertiary ds-table__cell-track ds-table__cell-track--text\">\n <ds-skeleton\n variant=\"text\"\n textVariant=\"text-body-small\"\n width={skeleton.tertiaryWidth ?? '56%'}\n />\n </span>\n )}\n </span>\n );\n if (skeleton.kind === 'icon-text') {\n return (\n <span class=\"ds-table__cell-icon-text\">\n <span class=\"ds-table__cell-icon-text-icon\">\n <ds-skeleton variant=\"icon\" iconSize=\"md\" />\n </span>\n {copy}\n </span>\n );\n }\n return copy;\n}\n",
1374
1374
  "type": "registry:ui"
1375
1375
  },
1376
1376
  {
1377
1377
  "path": "src/wc/components/Table/table-truncate.ts",
1378
- "content": "/** Marks text tracks that may present the table-owned truncation tooltip. */\nexport const TABLE_TRUNCATE_ATTR = 'data-table-truncate';\n\nfunction isValidTruncateTrack(track: HTMLElement): boolean {\n return !track.closest('.ds-table__row--disabled, thead, .ds-table__header-row');\n}\n\n/** Resolve the overflowing text track under a pointer or focus target. */\nexport function resolveTableTruncateTrack(target: EventTarget | null): HTMLElement | null {\n const element =\n target instanceof Element\n ? target\n : target instanceof Node\n ? target.parentElement\n : null;\n if (!element) return null;\n\n const fromAncestor = element.closest<HTMLElement>(`[${TABLE_TRUNCATE_ATTR}]`);\n if (fromAncestor && isValidTruncateTrack(fromAncestor)) return fromAncestor;\n\n const link = element.closest('a.ds-table__cell-link');\n const nested = link?.querySelector<HTMLElement>(`[${TABLE_TRUNCATE_ATTR}]`);\n if (nested && isValidTruncateTrack(nested)) return nested;\n return null;\n}\n\nexport function tableTruncateLabel(track: HTMLElement): string {\n return (track.textContent ?? '').replace(/\\s+/g, ' ').trim();\n}\n",
1378
+ "content": "/** Marks text tracks that may present the table-owned truncation tooltip. */\nexport const TABLE_TRUNCATE_ATTR = 'data-table-truncate';\n\nfunction isValidTruncateTrack(track: HTMLElement): boolean {\n return !track.closest('.ds-table__row--disabled, thead, .ds-table__header-row');\n}\n\n/** Resolve the overflowing text track under a pointer or focus target. */\nexport function resolveTableTruncateTrack(target: EventTarget | null): HTMLElement | null {\n const element =\n target instanceof Element ? target : target instanceof Node ? target.parentElement : null;\n if (!element) return null;\n\n const fromAncestor = element.closest<HTMLElement>(`[${TABLE_TRUNCATE_ATTR}]`);\n if (fromAncestor && isValidTruncateTrack(fromAncestor)) return fromAncestor;\n\n const link = element.closest('a.ds-table__cell-link');\n const nested = link?.querySelector<HTMLElement>(`[${TABLE_TRUNCATE_ATTR}]`);\n if (nested && isValidTruncateTrack(nested)) return nested;\n return null;\n}\n\nexport function tableTruncateLabel(track: HTMLElement): string {\n return (track.textContent ?? '').replace(/\\s+/g, ' ').trim();\n}\n",
1379
1379
  "type": "registry:ui"
1380
1380
  },
1381
1381
  {
1382
1382
  "path": "src/wc/components/Table/table-types.ts",
1383
- "content": "import type { TagContrast, TagIntent } from '../Tag/Tag';\nimport type { IconColor } from '../Icon/Icon';\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 label: string;\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}\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}\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/** 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 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\ninterface TableCellTagBase {\n kind: 'tag';\n label: string;\n intent?: TagIntent;\n contrast?: TagContrast;\n icon?: string;\n rounded?: boolean;\n}\n\nexport type TableCellTag = TableCellTagBase & (\n | {\n variant?: 'tag-only';\n text?: never;\n }\n | {\n variant: Exclude<TableCellTagVariant, 'tag-only'>;\n text: string;\n }\n);\n\nexport type TableCellValue =\n | string\n | number\n | null\n | undefined\n | TableCellText\n | TableCellPrimaryText\n | TableCellTag\n | TableCellIcon\n | TableCellIconText\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: '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 /** 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\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 /** 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",
1383
+ "content": "import type { TagContrast, TagIntent } from '../Tag/Tag';\nimport type { IconColor } from '../Icon/Icon';\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 label: string;\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}\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}\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/** 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\ninterface TableCellTagBase {\n kind: 'tag';\n label: string;\n intent?: TagIntent;\n contrast?: TagContrast;\n icon?: string;\n rounded?: boolean;\n}\n\nexport type TableCellTag = TableCellTagBase &\n (\n | {\n variant?: 'tag-only';\n text?: never;\n }\n | {\n variant: Exclude<TableCellTagVariant, 'tag-only'>;\n text: string;\n }\n );\n\nexport type TableCellValue =\n | string\n | number\n | null\n | undefined\n | TableCellText\n | TableCellPrimaryText\n | TableCellTag\n | TableCellIcon\n | TableCellIconText\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: '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 /** 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\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 /** 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",
1384
1384
  "type": "registry:ui"
1385
1385
  },
1386
1386
  {
1387
1387
  "path": "src/wc/components/Table/table-viewport-fit-controller.ts",
1388
- "content": "import { resolveCssLengthPx } from '../../utils/resolve-css-length-px';\n\nexport interface TableViewportFitMetricInput {\n scrollportBlockStart: number;\n scrollportBlockSize: number;\n hostBlockStart: number;\n insetBlockStart: number;\n insetBlockEnd: number;\n}\n\nexport interface TableViewportFitMetrics {\n reservedBlockSize: number;\n currentBlockSize: number;\n settled: boolean;\n}\n\nexport interface TableViewportFitElements {\n host: HTMLElement | null;\n surface: HTMLElement | null;\n}\n\nexport interface TableViewportFitControllerOptions {\n enabled: () => boolean;\n elements: () => TableViewportFitElements;\n insets: () => { blockStart: string | number; blockEnd: string | number };\n fitChanged: (metrics: TableViewportFitMetrics | null) => void;\n}\n\n/** Resolve the reserved compact-state height and the height currently visible. */\nexport function resolveTableViewportFitMetrics(\n input: TableViewportFitMetricInput,\n): TableViewportFitMetrics {\n const insetBlockStart = Math.max(0, input.insetBlockStart);\n const insetBlockEnd = Math.max(0, input.insetBlockEnd);\n const scrollportBlockEnd = input.scrollportBlockStart + input.scrollportBlockSize;\n const settledBlockStart = input.scrollportBlockStart + insetBlockStart;\n const reservedBlockSize = Math.max(\n 0,\n input.scrollportBlockSize - insetBlockStart - insetBlockEnd,\n );\n const currentBlockStart = Math.max(input.hostBlockStart, settledBlockStart);\n const currentBlockSize = Math.min(\n reservedBlockSize,\n Math.max(0, scrollportBlockEnd - insetBlockEnd - currentBlockStart),\n );\n\n return {\n reservedBlockSize,\n currentBlockSize,\n settled: input.hostBlockStart <= settledBlockStart + 0.5,\n };\n}\n\n/**\n * Fits the complete table chrome to its nearest vertical scrollport. The host\n * reserves its compact-state height so the outer page can consume collapsing\n * header travel without changing document height. The visible table surface\n * grows into that reservation until it reaches the supplied sticky inset.\n */\nexport class TableViewportFitController {\n private connected = false;\n private connectedHost: HTMLElement | null = null;\n private connectedSurface: HTMLElement | null = null;\n private scrollRoot: HTMLElement | Window | null = null;\n private resizeObserver: ResizeObserver | null = null;\n private metrics: TableViewportFitMetrics | null = null;\n private insetBlockStart: string | number | null = null;\n private insetBlockEnd: string | number | null = null;\n\n constructor(private readonly options: TableViewportFitControllerOptions) {}\n\n connect(): void {\n this.connected = true;\n this.refresh();\n }\n\n disconnect(): void {\n this.connected = false;\n this.disconnectRuntime();\n this.clearGeometry();\n }\n\n refresh(forceGeometry = true): void {\n if (!this.connected) return;\n if (!this.options.enabled()) {\n this.disconnectRuntime();\n this.clearGeometry();\n return;\n }\n\n const { host, surface } = this.options.elements();\n if (!host) return;\n const elementsChanged = host !== this.connectedHost || surface !== this.connectedSurface;\n const nextScrollRoot = elementsChanged || !this.scrollRoot\n ? this.findScrollRoot(host)\n : this.scrollRoot;\n const rootChanged = nextScrollRoot !== this.scrollRoot;\n if (nextScrollRoot !== this.scrollRoot) {\n this.disconnectRuntime();\n this.scrollRoot = nextScrollRoot;\n this.scrollRoot.addEventListener('scroll', this.sync, { passive: true });\n window.addEventListener('resize', this.sync, { passive: true });\n if (typeof ResizeObserver !== 'undefined' && this.scrollRoot instanceof HTMLElement) {\n this.resizeObserver = new ResizeObserver(this.sync);\n this.resizeObserver.observe(this.scrollRoot);\n }\n }\n this.connectedHost = host;\n this.connectedSurface = surface;\n const insets = this.options.insets();\n const insetsChanged = insets.blockStart !== this.insetBlockStart ||\n insets.blockEnd !== this.insetBlockEnd;\n this.insetBlockStart = insets.blockStart;\n this.insetBlockEnd = insets.blockEnd;\n if (forceGeometry || elementsChanged || rootChanged || insetsChanged) this.sync();\n }\n\n /** Pass a fitted table's boundary wheel delta to its owning page scrollport. */\n scrollOuterBy(delta: number): boolean {\n if (!this.scrollRoot || delta === 0) return false;\n if (this.scrollRoot instanceof HTMLElement) {\n const previous = this.scrollRoot.scrollTop;\n const max = Math.max(0, this.scrollRoot.scrollHeight - this.scrollRoot.clientHeight);\n this.scrollRoot.scrollTop = Math.min(max, Math.max(0, previous + delta));\n return this.scrollRoot.scrollTop !== previous;\n }\n\n const scrollingElement = document.scrollingElement;\n const previous = window.scrollY;\n const max = Math.max(\n 0,\n (scrollingElement?.scrollHeight ?? document.documentElement.scrollHeight) -\n window.innerHeight,\n );\n const next = Math.min(max, Math.max(0, previous + delta));\n if (next !== previous) window.scrollTo({ top: next, behavior: 'auto' });\n return next !== previous;\n }\n\n private disconnectRuntime(): void {\n this.scrollRoot?.removeEventListener('scroll', this.sync);\n window.removeEventListener('resize', this.sync);\n this.scrollRoot = null;\n this.connectedHost = null;\n this.connectedSurface = null;\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n this.insetBlockStart = null;\n this.insetBlockEnd = null;\n }\n\n private clearGeometry(): void {\n const { host, surface } = this.options.elements();\n host?.style.removeProperty('--_table-viewport-fit-reserved-block-size');\n surface?.style.removeProperty('--_table-viewport-fit-current-block-size');\n if (this.metrics !== null) {\n this.metrics = null;\n this.options.fitChanged(null);\n }\n }\n\n private readonly sync = (): void => {\n if (!this.connected || !this.options.enabled() || !this.scrollRoot) return;\n const { host, surface } = this.options.elements();\n if (!host) return;\n\n const hostRect = host.getBoundingClientRect();\n const scrollportRect = this.scrollRoot instanceof HTMLElement\n ? this.scrollRoot.getBoundingClientRect()\n : { top: 0, height: window.innerHeight };\n const insets = this.options.insets();\n const next = resolveTableViewportFitMetrics({\n scrollportBlockStart: scrollportRect.top,\n scrollportBlockSize: scrollportRect.height,\n hostBlockStart: hostRect.top,\n insetBlockStart: resolveCssLengthPx(insets.blockStart, 0, host),\n insetBlockEnd: resolveCssLengthPx(insets.blockEnd, 0, host),\n });\n\n this.setProperty(\n host,\n '--_table-viewport-fit-reserved-block-size',\n `${next.reservedBlockSize}px`,\n );\n if (surface) {\n this.setProperty(\n surface,\n '--_table-viewport-fit-current-block-size',\n `${next.currentBlockSize}px`,\n );\n }\n if (\n !this.metrics ||\n this.metrics.reservedBlockSize !== next.reservedBlockSize ||\n this.metrics.currentBlockSize !== next.currentBlockSize ||\n this.metrics.settled !== next.settled\n ) {\n this.metrics = next;\n this.options.fitChanged(next);\n }\n };\n\n private findScrollRoot(host: HTMLElement): HTMLElement | Window {\n let ancestor = this.composedParent(host);\n while (ancestor && ancestor !== document.documentElement) {\n const overflow = getComputedStyle(ancestor).overflowY;\n if (overflow === 'auto' || overflow === 'scroll' || overflow === 'overlay') {\n return ancestor as HTMLElement;\n }\n ancestor = this.composedParent(ancestor);\n }\n return window;\n }\n\n private composedParent(element: Element): Element | null {\n return (element as HTMLElement).assignedSlot?.parentElement ?? element.parentElement;\n }\n\n private setProperty(element: HTMLElement, property: string, value: string): void {\n if (element.style.getPropertyValue(property) !== value) {\n element.style.setProperty(property, value);\n }\n }\n}\n",
1388
+ "content": "import { resolveCssLengthPx } from '../../utils/resolve-css-length-px';\n\nexport interface TableViewportFitMetricInput {\n scrollportBlockStart: number;\n scrollportBlockSize: number;\n hostBlockStart: number;\n insetBlockStart: number;\n insetBlockEnd: number;\n}\n\nexport interface TableViewportFitMetrics {\n reservedBlockSize: number;\n currentBlockSize: number;\n settled: boolean;\n}\n\nexport interface TableViewportFitElements {\n host: HTMLElement | null;\n surface: HTMLElement | null;\n}\n\nexport interface TableViewportFitControllerOptions {\n enabled: () => boolean;\n elements: () => TableViewportFitElements;\n insets: () => { blockStart: string | number; blockEnd: string | number };\n fitChanged: (metrics: TableViewportFitMetrics | null) => void;\n}\n\n/** Resolve the reserved compact-state height and the height currently visible. */\nexport function resolveTableViewportFitMetrics(\n input: TableViewportFitMetricInput\n): TableViewportFitMetrics {\n const insetBlockStart = Math.max(0, input.insetBlockStart);\n const insetBlockEnd = Math.max(0, input.insetBlockEnd);\n const scrollportBlockEnd = input.scrollportBlockStart + input.scrollportBlockSize;\n const settledBlockStart = input.scrollportBlockStart + insetBlockStart;\n const reservedBlockSize = Math.max(\n 0,\n input.scrollportBlockSize - insetBlockStart - insetBlockEnd\n );\n const currentBlockStart = Math.max(input.hostBlockStart, settledBlockStart);\n const currentBlockSize = Math.min(\n reservedBlockSize,\n Math.max(0, scrollportBlockEnd - insetBlockEnd - currentBlockStart)\n );\n\n return {\n reservedBlockSize,\n currentBlockSize,\n settled: input.hostBlockStart <= settledBlockStart + 0.5,\n };\n}\n\n/**\n * Fits the complete table chrome to its nearest vertical scrollport. The host\n * reserves its compact-state height so the outer page can consume collapsing\n * header travel without changing document height. The visible table surface\n * grows into that reservation until it reaches the supplied sticky inset.\n */\nexport class TableViewportFitController {\n private connected = false;\n private connectedHost: HTMLElement | null = null;\n private connectedSurface: HTMLElement | null = null;\n private scrollRoot: HTMLElement | Window | null = null;\n private resizeObserver: ResizeObserver | null = null;\n private metrics: TableViewportFitMetrics | null = null;\n private insetBlockStart: string | number | null = null;\n private insetBlockEnd: string | number | null = null;\n\n constructor(private readonly options: TableViewportFitControllerOptions) {}\n\n connect(): void {\n this.connected = true;\n this.refresh();\n }\n\n disconnect(): void {\n this.connected = false;\n this.disconnectRuntime();\n this.clearGeometry();\n }\n\n refresh(forceGeometry = true): void {\n if (!this.connected) return;\n if (!this.options.enabled()) {\n this.disconnectRuntime();\n this.clearGeometry();\n return;\n }\n\n const { host, surface } = this.options.elements();\n if (!host) return;\n const elementsChanged = host !== this.connectedHost || surface !== this.connectedSurface;\n const nextScrollRoot =\n elementsChanged || !this.scrollRoot ? this.findScrollRoot(host) : this.scrollRoot;\n const rootChanged = nextScrollRoot !== this.scrollRoot;\n if (nextScrollRoot !== this.scrollRoot) {\n this.disconnectRuntime();\n this.scrollRoot = nextScrollRoot;\n this.scrollRoot.addEventListener('scroll', this.sync, { passive: true });\n window.addEventListener('resize', this.sync, { passive: true });\n if (typeof ResizeObserver !== 'undefined' && this.scrollRoot instanceof HTMLElement) {\n this.resizeObserver = new ResizeObserver(this.sync);\n this.resizeObserver.observe(this.scrollRoot);\n }\n }\n this.connectedHost = host;\n this.connectedSurface = surface;\n const insets = this.options.insets();\n const insetsChanged =\n insets.blockStart !== this.insetBlockStart || insets.blockEnd !== this.insetBlockEnd;\n this.insetBlockStart = insets.blockStart;\n this.insetBlockEnd = insets.blockEnd;\n if (forceGeometry || elementsChanged || rootChanged || insetsChanged) this.sync();\n }\n\n /** Pass a fitted table's boundary wheel delta to its owning page scrollport. */\n scrollOuterBy(delta: number): boolean {\n if (!this.scrollRoot || delta === 0) return false;\n if (this.scrollRoot instanceof HTMLElement) {\n const previous = this.scrollRoot.scrollTop;\n const max = Math.max(0, this.scrollRoot.scrollHeight - this.scrollRoot.clientHeight);\n this.scrollRoot.scrollTop = Math.min(max, Math.max(0, previous + delta));\n return this.scrollRoot.scrollTop !== previous;\n }\n\n const scrollingElement = document.scrollingElement;\n const previous = window.scrollY;\n const max = Math.max(\n 0,\n (scrollingElement?.scrollHeight ?? document.documentElement.scrollHeight) - window.innerHeight\n );\n const next = Math.min(max, Math.max(0, previous + delta));\n if (next !== previous) window.scrollTo({ top: next, behavior: 'auto' });\n return next !== previous;\n }\n\n private disconnectRuntime(): void {\n this.scrollRoot?.removeEventListener('scroll', this.sync);\n window.removeEventListener('resize', this.sync);\n this.scrollRoot = null;\n this.connectedHost = null;\n this.connectedSurface = null;\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n this.insetBlockStart = null;\n this.insetBlockEnd = null;\n }\n\n private clearGeometry(): void {\n const { host, surface } = this.options.elements();\n host?.style.removeProperty('--_table-viewport-fit-reserved-block-size');\n surface?.style.removeProperty('--_table-viewport-fit-current-block-size');\n if (this.metrics !== null) {\n this.metrics = null;\n this.options.fitChanged(null);\n }\n }\n\n private readonly sync = (): void => {\n if (!this.connected || !this.options.enabled() || !this.scrollRoot) return;\n const { host, surface } = this.options.elements();\n if (!host) return;\n\n const hostRect = host.getBoundingClientRect();\n const scrollportRect =\n this.scrollRoot instanceof HTMLElement\n ? this.scrollRoot.getBoundingClientRect()\n : { top: 0, height: window.innerHeight };\n const insets = this.options.insets();\n const next = resolveTableViewportFitMetrics({\n scrollportBlockStart: scrollportRect.top,\n scrollportBlockSize: scrollportRect.height,\n hostBlockStart: hostRect.top,\n insetBlockStart: resolveCssLengthPx(insets.blockStart, 0, host),\n insetBlockEnd: resolveCssLengthPx(insets.blockEnd, 0, host),\n });\n\n this.setProperty(\n host,\n '--_table-viewport-fit-reserved-block-size',\n `${next.reservedBlockSize}px`\n );\n if (surface) {\n this.setProperty(\n surface,\n '--_table-viewport-fit-current-block-size',\n `${next.currentBlockSize}px`\n );\n }\n if (\n !this.metrics ||\n this.metrics.reservedBlockSize !== next.reservedBlockSize ||\n this.metrics.currentBlockSize !== next.currentBlockSize ||\n this.metrics.settled !== next.settled\n ) {\n this.metrics = next;\n this.options.fitChanged(next);\n }\n };\n\n private findScrollRoot(host: HTMLElement): HTMLElement | Window {\n let ancestor = this.composedParent(host);\n while (ancestor && ancestor !== document.documentElement) {\n const overflow = getComputedStyle(ancestor).overflowY;\n if (overflow === 'auto' || overflow === 'scroll' || overflow === 'overlay') {\n return ancestor as HTMLElement;\n }\n ancestor = this.composedParent(ancestor);\n }\n return window;\n }\n\n private composedParent(element: Element): Element | null {\n return (element as HTMLElement).assignedSlot?.parentElement ?? element.parentElement;\n }\n\n private setProperty(element: HTMLElement, property: string, value: string): void {\n if (element.style.getPropertyValue(property) !== value) {\n element.style.setProperty(property, value);\n }\n }\n}\n",
1389
1389
  "type": "registry:ui"
1390
1390
  },
1391
1391
  {
1392
1392
  "path": "src/wc/components/Table/table-virtual-controller.ts",
1393
- "content": "import {\n createTableVirtualIndex,\n findTableVirtualIndexAtOffset,\n resolveTableVirtualPlanFromIndex,\n sameTableVirtualPlan,\n tableVirtualItemSizes,\n type TableVirtualItem,\n type TableVirtualIndex,\n type TableVirtualPlan,\n} from './table-virtual-model';\n\nexport interface TableVirtualControllerState {\n enabled: boolean;\n items: readonly TableVirtualItem[];\n pinnedRowIds: ReadonlySet<string>;\n viewport: HTMLElement | null;\n viewportSize: number;\n}\n\nexport interface TableVirtualControllerOptions {\n state: () => TableVirtualControllerState;\n windowChanged: (plan: TableVirtualPlan | null) => void;\n requestAnimationFrame?: (callback: FrameRequestCallback) => number;\n cancelAnimationFrame?: (handle: number) => void;\n}\n\nconst MEASURE_EPSILON_PX = 0.5;\n\n/** Binds viewport scroll/resize to a recycled row window. Scroll offset stays off render state. */\nexport class TableVirtualController {\n private connected = false;\n private connectedViewport: HTMLElement | null = null;\n private resizeObserver: ResizeObserver | null = null;\n private measurementObserver: ResizeObserver | null = null;\n private measurementTargets = new Set<HTMLElement>();\n private viewportWidth = 0;\n private viewportSize = 0;\n private frame: number | null = null;\n private measures = new Map<string, number>();\n private measuredIds = new Set<string>();\n private plan: TableVirtualPlan | null = null;\n private index: TableVirtualIndex | null = null;\n private indexedItems: readonly TableVirtualItem[] | null = null;\n private indexDirty = true;\n private scrollOffset = 0;\n private scrollDirection: 'backward' | 'forward' | 'none' = 'none';\n private pendingReset = false;\n private readonly raf: (callback: FrameRequestCallback) => number;\n private readonly caf: (handle: number) => void;\n\n constructor(private readonly options: TableVirtualControllerOptions) {\n this.raf = options.requestAnimationFrame ?? (typeof requestAnimationFrame === 'function'\n ? callback => requestAnimationFrame(callback)\n : callback => setTimeout(callback, 16) as unknown as number);\n this.caf = options.cancelAnimationFrame ?? (typeof cancelAnimationFrame === 'function'\n ? handle => cancelAnimationFrame(handle)\n : handle => clearTimeout(handle));\n }\n\n connect(): void {\n this.connected = true;\n this.refresh();\n }\n\n disconnect(): void {\n this.connected = false;\n this.cancelFrame();\n this.unbindViewport();\n }\n\n refresh(): void {\n if (!this.connected) return;\n const { viewport, enabled } = this.options.state();\n if (!enabled || !viewport) {\n this.unbindViewport();\n this.emit(null);\n return;\n }\n if (viewport !== this.connectedViewport) {\n this.unbindViewport();\n this.connectedViewport = viewport;\n this.viewportWidth = viewport.clientWidth;\n this.viewportSize = viewport.clientHeight;\n this.scrollOffset = viewport.scrollTop;\n viewport.addEventListener('scroll', this.handleScroll, { passive: true });\n if (typeof ResizeObserver !== 'undefined') {\n this.resizeObserver = new ResizeObserver(this.handleViewportResize);\n this.resizeObserver.observe(viewport);\n }\n }\n this.sync();\n }\n\n schedule = (): void => {\n if (!this.connected || this.frame !== null) return;\n this.frame = this.raf(() => {\n this.frame = null;\n this.sync();\n });\n };\n\n /** Drop cached heights after column, density, or wrap geometry changes. */\n invalidateMeasures(): void {\n this.measures.clear();\n this.measuredIds.clear();\n this.indexDirty = true;\n this.schedule();\n }\n\n /** Rebuild from a new index and pin the viewport to the start. */\n resetToTop(): void {\n this.pendingReset = true;\n this.measures.clear();\n this.measuredIds.clear();\n this.indexDirty = true;\n this.scrollOffset = 0;\n this.scrollDirection = 'none';\n const viewport = this.connectedViewport ?? this.options.state().viewport;\n if (viewport) viewport.scrollTop = 0;\n this.schedule();\n }\n\n sizeFor(item: TableVirtualItem): number {\n if (!item.variableSize) return item.estimatedSize;\n return this.measures.get(item.id) ?? item.estimatedSize;\n }\n\n currentPlan(): TableVirtualPlan | null {\n return this.plan;\n }\n\n currentViewportSize(): number {\n return this.viewportSize;\n }\n\n /** Measure painted rows and correct scroll when estimates were wrong. */\n collectMeasurements(root: ParentNode | null): void {\n if (!root || !this.connected || !this.options.state().enabled) return;\n const painted = root.querySelectorAll<HTMLElement>('[data-virtual-measure=\"true\"]');\n this.syncMeasurementTargets(painted);\n if (painted.length === 0) return;\n\n const state = this.options.state();\n const measurements: Array<{ id: string; height: number }> = [];\n painted.forEach(element => {\n const id = element.getAttribute('data-virtual-id');\n if (!id || this.measuredIds.has(id)) return;\n measurements.push({ id, height: element.getBoundingClientRect().height });\n });\n this.applyMeasurements(state.items, measurements);\n }\n\n private applyMeasurements(\n items: readonly TableVirtualItem[],\n measurements: readonly { id: string; height: number }[],\n ): void {\n if (measurements.length === 0) return;\n const currentIndex = this.ensureIndex(items);\n const prefixBefore = currentIndex.prefix;\n const viewport = this.connectedViewport;\n const scrollTop = viewport?.scrollTop ?? this.scrollOffset;\n const anchorIndex = findTableVirtualIndexAtOffset(prefixBefore, scrollTop);\n const anchorOffset = prefixBefore[anchorIndex] ?? 0;\n let changed = false;\n for (const { id, height } of measurements) {\n if (!Number.isFinite(height) || height <= 0) continue;\n const itemIndex = currentIndex.itemIndexById.get(id);\n const item = itemIndex == null ? undefined : items[itemIndex];\n if (!item?.variableSize) continue;\n this.measuredIds.add(id);\n const previous = this.measures.get(id);\n if (Math.abs(item.estimatedSize - height) < MEASURE_EPSILON_PX) {\n if (previous != null) {\n this.measures.delete(id);\n changed = true;\n }\n continue;\n }\n if (previous != null && Math.abs(previous - height) < MEASURE_EPSILON_PX) continue;\n this.measures.set(id, height);\n changed = true;\n }\n if (!changed) return;\n this.indexDirty = true;\n const prefixAfter = this.ensureIndex(items).prefix;\n const nextAnchor = prefixAfter[anchorIndex] ?? 0;\n const delta = nextAnchor - anchorOffset;\n if (viewport && Math.abs(delta) >= MEASURE_EPSILON_PX) {\n viewport.scrollTop = Math.max(0, viewport.scrollTop + delta);\n this.scrollOffset = viewport.scrollTop;\n }\n this.sync();\n }\n\n private syncMeasurementTargets(elements: NodeListOf<HTMLElement>): void {\n if (typeof ResizeObserver === 'undefined') return;\n if (!this.measurementObserver) {\n this.measurementObserver = new ResizeObserver(entries => {\n const state = this.options.state();\n const measurements = entries.map(entry => ({\n id: (entry.target as HTMLElement).getAttribute('data-virtual-id') ?? '',\n height: entry.borderBoxSize[0]?.blockSize ?? entry.contentRect.height,\n }));\n this.applyMeasurements(state.items, measurements);\n });\n }\n const next = new Set(elements);\n for (const element of this.measurementTargets) {\n if (!next.has(element)) this.measurementObserver.unobserve(element);\n }\n for (const element of next) {\n if (!this.measurementTargets.has(element)) this.measurementObserver.observe(element);\n }\n this.measurementTargets = next;\n }\n\n private readonly handleViewportResize = (entries: ResizeObserverEntry[]): void => {\n const entry = entries.find(candidate => candidate.target === this.connectedViewport);\n const width = entry?.contentRect.width ?? this.connectedViewport?.clientWidth ?? 0;\n const size = entry?.contentRect.height ?? this.connectedViewport?.clientHeight ?? 0;\n const sizeChanged = size !== this.viewportSize;\n this.viewportSize = size;\n if (width !== this.viewportWidth) {\n this.viewportWidth = width;\n const items = this.options.state().items;\n const hasVariableSize = this.indexedItems === items && this.index\n ? this.index.hasVariableSize\n : items.some(item => item.variableSize);\n if (hasVariableSize) this.invalidateMeasures();\n else if (sizeChanged) this.schedule();\n return;\n }\n if (sizeChanged) this.schedule();\n };\n\n private readonly handleScroll = (): void => {\n const viewport = this.connectedViewport;\n if (viewport) {\n const nextOffset = viewport.scrollTop;\n this.scrollDirection = nextOffset > this.scrollOffset\n ? 'forward'\n : nextOffset < this.scrollOffset\n ? 'backward'\n : this.scrollDirection;\n this.scrollOffset = nextOffset;\n }\n this.schedule();\n };\n\n private sync(): void {\n if (!this.connected) return;\n const state = this.options.state();\n if (!state.enabled) {\n this.emit(null);\n return;\n }\n\n if (this.pendingReset) {\n this.scrollOffset = 0;\n if (state.viewport) state.viewport.scrollTop = 0;\n this.pendingReset = false;\n }\n const index = this.ensureIndex(state.items);\n const plan = resolveTableVirtualPlanFromIndex(index, {\n scrollOffset: this.scrollOffset,\n viewportSize: this.viewportSize || state.viewportSize,\n scrollDirection: this.scrollDirection,\n pinnedRowIds: state.pinnedRowIds,\n });\n this.emit(plan);\n }\n\n private emit(plan: TableVirtualPlan | null): void {\n if (sameTableVirtualPlan(this.plan, plan)) return;\n this.plan = plan;\n this.options.windowChanged(plan);\n }\n\n private ensureIndex(items: readonly TableVirtualItem[]): TableVirtualIndex {\n if (!this.indexDirty && this.index && this.indexedItems === items) return this.index;\n if (this.indexedItems !== items && (this.measures.size > 0 || this.measuredIds.size > 0)) {\n const itemIds = new Set(items.map(item => item.id));\n for (const id of this.measures.keys()) {\n if (!itemIds.has(id)) this.measures.delete(id);\n }\n for (const id of this.measuredIds) {\n if (!itemIds.has(id)) this.measuredIds.delete(id);\n }\n }\n this.indexedItems = items;\n this.index = createTableVirtualIndex(items, tableVirtualItemSizes(items, this.measures));\n this.indexDirty = false;\n return this.index;\n }\n\n private unbindViewport(): void {\n this.connectedViewport?.removeEventListener('scroll', this.handleScroll);\n this.connectedViewport = null;\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n this.measurementObserver?.disconnect();\n this.measurementObserver = null;\n this.measurementTargets.clear();\n this.viewportWidth = 0;\n this.viewportSize = 0;\n }\n\n private cancelFrame(): void {\n if (this.frame === null) return;\n this.caf(this.frame);\n this.frame = null;\n }\n}\n",
1393
+ "content": "import {\n createTableVirtualIndex,\n findTableVirtualIndexAtOffset,\n resolveTableVirtualPlanFromIndex,\n sameTableVirtualPlan,\n tableVirtualItemSizes,\n type TableVirtualItem,\n type TableVirtualIndex,\n type TableVirtualPlan,\n} from './table-virtual-model';\n\nexport interface TableVirtualControllerState {\n enabled: boolean;\n items: readonly TableVirtualItem[];\n pinnedRowIds: ReadonlySet<string>;\n viewport: HTMLElement | null;\n viewportSize: number;\n}\n\nexport interface TableVirtualControllerOptions {\n state: () => TableVirtualControllerState;\n windowChanged: (plan: TableVirtualPlan | null) => void;\n requestAnimationFrame?: (callback: FrameRequestCallback) => number;\n cancelAnimationFrame?: (handle: number) => void;\n}\n\nconst MEASURE_EPSILON_PX = 0.5;\n\n/** Binds viewport scroll/resize to a recycled row window. Scroll offset stays off render state. */\nexport class TableVirtualController {\n private connected = false;\n private connectedViewport: HTMLElement | null = null;\n private resizeObserver: ResizeObserver | null = null;\n private measurementObserver: ResizeObserver | null = null;\n private measurementTargets = new Set<HTMLElement>();\n private viewportWidth = 0;\n private viewportSize = 0;\n private frame: number | null = null;\n private measures = new Map<string, number>();\n private measuredIds = new Set<string>();\n private plan: TableVirtualPlan | null = null;\n private index: TableVirtualIndex | null = null;\n private indexedItems: readonly TableVirtualItem[] | null = null;\n private indexDirty = true;\n private scrollOffset = 0;\n private scrollDirection: 'backward' | 'forward' | 'none' = 'none';\n private pendingReset = false;\n private readonly raf: (callback: FrameRequestCallback) => number;\n private readonly caf: (handle: number) => void;\n\n constructor(private readonly options: TableVirtualControllerOptions) {\n this.raf =\n options.requestAnimationFrame ??\n (typeof requestAnimationFrame === 'function'\n ? callback => requestAnimationFrame(callback)\n : callback => setTimeout(callback, 16) as unknown as number);\n this.caf =\n options.cancelAnimationFrame ??\n (typeof cancelAnimationFrame === 'function'\n ? handle => cancelAnimationFrame(handle)\n : handle => clearTimeout(handle));\n }\n\n connect(): void {\n this.connected = true;\n this.refresh();\n }\n\n disconnect(): void {\n this.connected = false;\n this.cancelFrame();\n this.unbindViewport();\n }\n\n refresh(): void {\n if (!this.connected) return;\n const { viewport, enabled } = this.options.state();\n if (!enabled || !viewport) {\n this.unbindViewport();\n this.emit(null);\n return;\n }\n if (viewport !== this.connectedViewport) {\n this.unbindViewport();\n this.connectedViewport = viewport;\n this.viewportWidth = viewport.clientWidth;\n this.viewportSize = viewport.clientHeight;\n this.scrollOffset = viewport.scrollTop;\n viewport.addEventListener('scroll', this.handleScroll, { passive: true });\n if (typeof ResizeObserver !== 'undefined') {\n this.resizeObserver = new ResizeObserver(this.handleViewportResize);\n this.resizeObserver.observe(viewport);\n }\n }\n this.sync();\n }\n\n schedule = (): void => {\n if (!this.connected || this.frame !== null) return;\n this.frame = this.raf(() => {\n this.frame = null;\n this.sync();\n });\n };\n\n /** Drop cached heights after column, density, or wrap geometry changes. */\n invalidateMeasures(): void {\n this.measures.clear();\n this.measuredIds.clear();\n this.indexDirty = true;\n this.schedule();\n }\n\n /** Rebuild from a new index and pin the viewport to the start. */\n resetToTop(): void {\n this.pendingReset = true;\n this.measures.clear();\n this.measuredIds.clear();\n this.indexDirty = true;\n this.scrollOffset = 0;\n this.scrollDirection = 'none';\n const viewport = this.connectedViewport ?? this.options.state().viewport;\n if (viewport) viewport.scrollTop = 0;\n this.schedule();\n }\n\n sizeFor(item: TableVirtualItem): number {\n if (!item.variableSize) return item.estimatedSize;\n return this.measures.get(item.id) ?? item.estimatedSize;\n }\n\n currentPlan(): TableVirtualPlan | null {\n return this.plan;\n }\n\n currentViewportSize(): number {\n return this.viewportSize;\n }\n\n /** Measure painted rows and correct scroll when estimates were wrong. */\n collectMeasurements(root: ParentNode | null): void {\n if (!root || !this.connected || !this.options.state().enabled) return;\n const painted = root.querySelectorAll<HTMLElement>('[data-virtual-measure=\"true\"]');\n this.syncMeasurementTargets(painted);\n if (painted.length === 0) return;\n\n const state = this.options.state();\n const measurements: Array<{ id: string; height: number }> = [];\n painted.forEach(element => {\n const id = element.getAttribute('data-virtual-id');\n if (!id || this.measuredIds.has(id)) return;\n measurements.push({ id, height: element.getBoundingClientRect().height });\n });\n this.applyMeasurements(state.items, measurements);\n }\n\n private applyMeasurements(\n items: readonly TableVirtualItem[],\n measurements: readonly { id: string; height: number }[]\n ): void {\n if (measurements.length === 0) return;\n const currentIndex = this.ensureIndex(items);\n const prefixBefore = currentIndex.prefix;\n const viewport = this.connectedViewport;\n const scrollTop = viewport?.scrollTop ?? this.scrollOffset;\n const anchorIndex = findTableVirtualIndexAtOffset(prefixBefore, scrollTop);\n const anchorOffset = prefixBefore[anchorIndex] ?? 0;\n let changed = false;\n for (const { id, height } of measurements) {\n if (!Number.isFinite(height) || height <= 0) continue;\n const itemIndex = currentIndex.itemIndexById.get(id);\n const item = itemIndex == null ? undefined : items[itemIndex];\n if (!item?.variableSize) continue;\n this.measuredIds.add(id);\n const previous = this.measures.get(id);\n if (Math.abs(item.estimatedSize - height) < MEASURE_EPSILON_PX) {\n if (previous != null) {\n this.measures.delete(id);\n changed = true;\n }\n continue;\n }\n if (previous != null && Math.abs(previous - height) < MEASURE_EPSILON_PX) continue;\n this.measures.set(id, height);\n changed = true;\n }\n if (!changed) return;\n this.indexDirty = true;\n const prefixAfter = this.ensureIndex(items).prefix;\n const nextAnchor = prefixAfter[anchorIndex] ?? 0;\n const delta = nextAnchor - anchorOffset;\n if (viewport && Math.abs(delta) >= MEASURE_EPSILON_PX) {\n viewport.scrollTop = Math.max(0, viewport.scrollTop + delta);\n this.scrollOffset = viewport.scrollTop;\n }\n this.sync();\n }\n\n private syncMeasurementTargets(elements: NodeListOf<HTMLElement>): void {\n if (typeof ResizeObserver === 'undefined') return;\n if (!this.measurementObserver) {\n this.measurementObserver = new ResizeObserver(entries => {\n const state = this.options.state();\n const measurements = entries.map(entry => ({\n id: (entry.target as HTMLElement).getAttribute('data-virtual-id') ?? '',\n height: entry.borderBoxSize[0]?.blockSize ?? entry.contentRect.height,\n }));\n this.applyMeasurements(state.items, measurements);\n });\n }\n const next = new Set(elements);\n for (const element of this.measurementTargets) {\n if (!next.has(element)) this.measurementObserver.unobserve(element);\n }\n for (const element of next) {\n if (!this.measurementTargets.has(element)) this.measurementObserver.observe(element);\n }\n this.measurementTargets = next;\n }\n\n private readonly handleViewportResize = (entries: ResizeObserverEntry[]): void => {\n const entry = entries.find(candidate => candidate.target === this.connectedViewport);\n const width = entry?.contentRect.width ?? this.connectedViewport?.clientWidth ?? 0;\n const size = entry?.contentRect.height ?? this.connectedViewport?.clientHeight ?? 0;\n const sizeChanged = size !== this.viewportSize;\n this.viewportSize = size;\n if (width !== this.viewportWidth) {\n this.viewportWidth = width;\n const items = this.options.state().items;\n const hasVariableSize =\n this.indexedItems === items && this.index\n ? this.index.hasVariableSize\n : items.some(item => item.variableSize);\n if (hasVariableSize) this.invalidateMeasures();\n else if (sizeChanged) this.schedule();\n return;\n }\n if (sizeChanged) this.schedule();\n };\n\n private readonly handleScroll = (): void => {\n const viewport = this.connectedViewport;\n if (viewport) {\n const nextOffset = viewport.scrollTop;\n this.scrollDirection =\n nextOffset > this.scrollOffset\n ? 'forward'\n : nextOffset < this.scrollOffset\n ? 'backward'\n : this.scrollDirection;\n this.scrollOffset = nextOffset;\n }\n this.schedule();\n };\n\n private sync(): void {\n if (!this.connected) return;\n const state = this.options.state();\n if (!state.enabled) {\n this.emit(null);\n return;\n }\n\n if (this.pendingReset) {\n this.scrollOffset = 0;\n if (state.viewport) state.viewport.scrollTop = 0;\n this.pendingReset = false;\n }\n const index = this.ensureIndex(state.items);\n const plan = resolveTableVirtualPlanFromIndex(index, {\n scrollOffset: this.scrollOffset,\n viewportSize: this.viewportSize || state.viewportSize,\n scrollDirection: this.scrollDirection,\n pinnedRowIds: state.pinnedRowIds,\n });\n this.emit(plan);\n }\n\n private emit(plan: TableVirtualPlan | null): void {\n if (sameTableVirtualPlan(this.plan, plan)) return;\n this.plan = plan;\n this.options.windowChanged(plan);\n }\n\n private ensureIndex(items: readonly TableVirtualItem[]): TableVirtualIndex {\n if (!this.indexDirty && this.index && this.indexedItems === items) return this.index;\n if (this.indexedItems !== items && (this.measures.size > 0 || this.measuredIds.size > 0)) {\n const itemIds = new Set(items.map(item => item.id));\n for (const id of this.measures.keys()) {\n if (!itemIds.has(id)) this.measures.delete(id);\n }\n for (const id of this.measuredIds) {\n if (!itemIds.has(id)) this.measuredIds.delete(id);\n }\n }\n this.indexedItems = items;\n this.index = createTableVirtualIndex(items, tableVirtualItemSizes(items, this.measures));\n this.indexDirty = false;\n return this.index;\n }\n\n private unbindViewport(): void {\n this.connectedViewport?.removeEventListener('scroll', this.handleScroll);\n this.connectedViewport = null;\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n this.measurementObserver?.disconnect();\n this.measurementObserver = null;\n this.measurementTargets.clear();\n this.viewportWidth = 0;\n this.viewportSize = 0;\n }\n\n private cancelFrame(): void {\n if (this.frame === null) return;\n this.caf(this.frame);\n this.frame = null;\n }\n}\n",
1394
1394
  "type": "registry:ui"
1395
1395
  },
1396
1396
  {
1397
1397
  "path": "src/wc/components/Table/table-virtual-model.ts",
1398
- "content": "import { resolveTableCellPresentation } from './table-cell-model';\nimport type { TableColumn, TableGroup, TableRow } from './table-types';\n\n/**\n * Token-aligned body-row track contracts: 40 / 62 / 84, plus wrapping-secondary\n * 106. Estimates only; measured heights replace these after paint.\n */\nexport const TABLE_VIRTUAL_ROW_TRACK_SIZE = {\n 1: 40,\n 2: 62,\n 3: 84,\n 4: 106,\n} as const;\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 !== 'text' && presentation.kind !== 'icon-text') return 1;\n\n const variantTracks = 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 | 2 | 3 | 4 = 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' || presentation.kind === 'icon-text') &&\n presentation.wraps &&\n presentation.lineClamp === 'none'\n ) {\n variableSize = true;\n }\n const count = tableCellTrackCount(presentation);\n if (count === 4) {\n tracks = 4;\n } else if (count > tracks) tracks = count as 1 | 2 | 3;\n }\n return { estimatedSize: TABLE_VIRTUAL_ROW_TRACK_SIZE[tracks], variableSize };\n}\n\nexport function flattenTableVirtualItems(input: FlattenTableVirtualItemsInput): TableVirtualItem[] {\n const collapsed = 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: TABLE_VIRTUAL_GROUP_HEADER_SIZE,\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(left: readonly TableVirtualNode[], right: readonly TableVirtualNode[]): 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 *\n 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(indexes: readonly number[], target: number): 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 = 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",
1398
+ "content": "import { resolveTableCellPresentation } from './table-cell-model';\nimport type { TableColumn, TableGroup, TableRow } from './table-types';\n\n/**\n * Token-aligned body-row track contracts: 40 / 62 / 84, plus wrapping-secondary\n * 106. Estimates only; measured heights replace these after paint.\n */\nexport const TABLE_VIRTUAL_ROW_TRACK_SIZE = {\n 1: 40,\n 2: 62,\n 3: 84,\n 4: 106,\n} as const;\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 !== 'text' && presentation.kind !== 'icon-text') 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 | 2 | 3 | 4 = 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' || presentation.kind === 'icon-text') &&\n presentation.wraps &&\n presentation.lineClamp === 'none'\n ) {\n variableSize = true;\n }\n const count = tableCellTrackCount(presentation);\n if (count === 4) {\n tracks = 4;\n } else if (count > tracks) tracks = count as 1 | 2 | 3;\n }\n return { estimatedSize: TABLE_VIRTUAL_ROW_TRACK_SIZE[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: TABLE_VIRTUAL_GROUP_HEADER_SIZE,\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",
1399
1399
  "type": "registry:ui"
1400
1400
  }
1401
1401
  ]