@ds-mo/ui 13.12.0 → 13.14.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 (222) hide show
  1. package/dist/.package-ready.json +1 -1
  2. package/dist/agent-patterns.json +1 -1
  3. package/dist/agent.json +68 -16
  4. package/dist/angular/ds-button-filled.d.ts +1 -1
  5. package/dist/angular/ds-button-filled.js +3 -3
  6. package/dist/angular/ds-button-unfilled.d.ts +1 -1
  7. package/dist/angular/ds-button-unfilled.js +3 -3
  8. package/dist/angular/ds-table.d.ts +7 -1
  9. package/dist/angular/ds-table.js +10 -4
  10. package/dist/angular/ds-tag.d.ts +1 -1
  11. package/dist/angular/ds-tag.js +3 -3
  12. package/dist/components/ds-agent-activity.js +1 -1
  13. package/dist/components/ds-agent-response.js +1 -1
  14. package/dist/components/ds-agent-source-list.js +1 -1
  15. package/dist/components/ds-agent-tool-call.js +1 -1
  16. package/dist/components/ds-attachment-list.js +1 -1
  17. package/dist/components/ds-avatar.js +1 -1
  18. package/dist/components/ds-banner.js +1 -1
  19. package/dist/components/ds-bar-nav.js +1 -1
  20. package/dist/components/ds-bar-title.js +1 -1
  21. package/dist/components/ds-bar-workflow.js +1 -1
  22. package/dist/components/ds-button-filled.js +1 -1
  23. package/dist/components/ds-button-unfilled.js +1 -1
  24. package/dist/components/ds-card-chart.js +1 -1
  25. package/dist/components/ds-card-overview.js +1 -1
  26. package/dist/components/ds-card-overview.js.map +1 -1
  27. package/dist/components/ds-card-setting.js +1 -1
  28. package/dist/components/ds-chart-legend.js +1 -1
  29. package/dist/components/ds-chart-legend.js.map +1 -1
  30. package/dist/components/ds-checkbox.js +1 -1
  31. package/dist/components/ds-chip.js +1 -1
  32. package/dist/components/ds-chip.js.map +1 -1
  33. package/dist/components/ds-code-block.js +1 -1
  34. package/dist/components/ds-conversation-list-item.js +1 -1
  35. package/dist/components/ds-conversation-list-item.js.map +1 -1
  36. package/dist/components/ds-empty-state.js +1 -1
  37. package/dist/components/ds-icon.js +1 -1
  38. package/dist/components/ds-input.js +1 -1
  39. package/dist/components/ds-input.js.map +1 -1
  40. package/dist/components/ds-markdown.js +1 -1
  41. package/dist/components/ds-menu.js +1 -1
  42. package/dist/components/ds-message-actions.js +1 -1
  43. package/dist/components/ds-message-composer.js +1 -1
  44. package/dist/components/ds-message-scroller.js +1 -1
  45. package/dist/components/ds-mobile-bar-nav.js +1 -1
  46. package/dist/components/ds-mobile-header.js +1 -1
  47. package/dist/components/ds-mobile-section-switcher.js +1 -1
  48. package/dist/components/ds-mobile-sheet-nav.js +1 -1
  49. package/dist/components/ds-modal.js +1 -1
  50. package/dist/components/ds-panel-nav.js +1 -1
  51. package/dist/components/ds-panel-sub-nav.js +1 -1
  52. package/dist/components/ds-panel-sub-nav.js.map +1 -1
  53. package/dist/components/ds-panel-tool-header.js +1 -1
  54. package/dist/components/ds-panel-tool-search.js +1 -1
  55. package/dist/components/ds-panel-tools.js +1 -1
  56. package/dist/components/ds-radio.js +1 -1
  57. package/dist/components/ds-radio.js.map +1 -1
  58. package/dist/components/ds-select.js +1 -1
  59. package/dist/components/ds-select.js.map +1 -1
  60. package/dist/components/ds-shell-app.js +1 -1
  61. package/dist/components/ds-shell-tools.js +1 -1
  62. package/dist/components/ds-tab-group.js +1 -1
  63. package/dist/components/ds-table.js +1 -1
  64. package/dist/components/ds-table.js.map +1 -1
  65. package/dist/components/ds-tag.js +1 -1
  66. package/dist/components/ds-toast.js +1 -1
  67. package/dist/components/ds-typing-indicator.js +1 -1
  68. package/dist/components/p-Azjx2SsL.js +2 -0
  69. package/dist/components/p-Azjx2SsL.js.map +1 -0
  70. package/dist/components/p-B1JB0Zn-.js +2 -0
  71. package/dist/components/{p-BYre-ZDg.js.map → p-B1JB0Zn-.js.map} +1 -1
  72. package/dist/components/p-B2t8alvH.js +2 -0
  73. package/dist/components/{p-DK4PGrHm.js.map → p-B2t8alvH.js.map} +1 -1
  74. package/dist/components/{p-DBchq8Zw.js → p-B6linhwY.js} +2 -2
  75. package/dist/components/p-B8HG9OFh.js +2 -0
  76. package/dist/components/p-B8HG9OFh.js.map +1 -0
  77. package/dist/components/{p-DcmqsAMP.js → p-BJVzrR_P.js} +2 -2
  78. package/dist/components/p-BL0JHLJp.js +2 -0
  79. package/dist/components/{p-BFXN4ioL.js.map → p-BL0JHLJp.js.map} +1 -1
  80. package/dist/components/p-BPS17WvI.js +2 -0
  81. package/dist/components/{p-x9jO4lZ7.js.map → p-BPS17WvI.js.map} +1 -1
  82. package/dist/components/{p-BX57q1Lo.js → p-BSdAFIcG.js} +2 -2
  83. package/dist/components/{p-DGkWMhHO.js → p-BULWzaBs.js} +2 -2
  84. package/dist/components/{p-B_eqa09C.js → p-C-aCs6ID.js} +2 -2
  85. package/dist/components/{p-BRajO-Za.js → p-C5ecLNZA.js} +2 -2
  86. package/dist/components/p-C8U5BsJf.js +2 -0
  87. package/dist/components/p-C8U5BsJf.js.map +1 -0
  88. package/dist/components/{p-DKftz0O8.js → p-COmqMfQo.js} +2 -2
  89. package/dist/components/{p-DxygUurH.js → p-CeyoHpPn.js} +2 -2
  90. package/dist/components/{p-CdFnvBem.js → p-CiUciD5Z.js} +2 -2
  91. package/dist/components/p-CuwopElg.js.map +1 -1
  92. package/dist/components/p-DAM4CDXT.js +2 -0
  93. package/dist/components/{p-BxJurXEn.js.map → p-DAM4CDXT.js.map} +1 -1
  94. package/dist/components/{p-DBJJiSP0.js → p-DMJEOAwP.js} +2 -2
  95. package/dist/components/p-DYAQVvOu.js +2 -0
  96. package/dist/components/{p-CIlO_j3q.js.map → p-DYAQVvOu.js.map} +1 -1
  97. package/dist/components/p-DyDkQ8H9.js +2 -0
  98. package/dist/components/p-DyDkQ8H9.js.map +1 -0
  99. package/dist/components/{p-DkhE3BsH.js → p-Dz3Ufmr4.js} +2 -2
  100. package/dist/components/p-cPh32CFB.js +2 -0
  101. package/dist/components/{p-Bj7KrXzw.js.map → p-cPh32CFB.js.map} +1 -1
  102. package/dist/components/{p-Doz2uwRv.js → p-fkQhr5s9.js} +2 -2
  103. package/dist/components/p-oOKTL-yx.js +2 -0
  104. package/dist/components/{p-C5gZHZHU.js.map → p-oOKTL-yx.js.map} +1 -1
  105. package/dist/docs/components.json +362 -10
  106. package/dist/lib/components/ButtonFilled/ButtonFilled.d.ts +6 -0
  107. package/dist/lib/utils/control-text.d.ts +2 -0
  108. package/dist/lib/utils/index.d.ts +1 -1
  109. package/dist/lib/utils/index.js.map +2 -2
  110. package/dist/mcp-data/registry/agent-activity.json +1 -1
  111. package/dist/mcp-data/registry/agent-response.json +1 -1
  112. package/dist/mcp-data/registry/agent-source-list.json +1 -1
  113. package/dist/mcp-data/registry/agent-tool-call.json +1 -1
  114. package/dist/mcp-data/registry/attachment-list.json +1 -1
  115. package/dist/mcp-data/registry/avatar.json +1 -1
  116. package/dist/mcp-data/registry/badge.json +1 -1
  117. package/dist/mcp-data/registry/banner.json +1 -1
  118. package/dist/mcp-data/registry/bar-nav.json +1 -1
  119. package/dist/mcp-data/registry/bar-title.json +1 -1
  120. package/dist/mcp-data/registry/bar-workflow.json +1 -1
  121. package/dist/mcp-data/registry/breadcrumb.json +1 -1
  122. package/dist/mcp-data/registry/button-filled.json +39 -2
  123. package/dist/mcp-data/registry/button-unfilled.json +45 -6
  124. package/dist/mcp-data/registry/card-chart.json +1 -1
  125. package/dist/mcp-data/registry/card-overview.json +1 -1
  126. package/dist/mcp-data/registry/card-setting.json +1 -1
  127. package/dist/mcp-data/registry/chart-legend.json +1 -1
  128. package/dist/mcp-data/registry/chart.json +1 -1
  129. package/dist/mcp-data/registry/checkbox.json +1 -1
  130. package/dist/mcp-data/registry/chip.json +1 -1
  131. package/dist/mcp-data/registry/code-block.json +1 -1
  132. package/dist/mcp-data/registry/conversation-list-item.json +1 -1
  133. package/dist/mcp-data/registry/conversation-list-section.json +1 -1
  134. package/dist/mcp-data/registry/conversation-list.json +1 -1
  135. package/dist/mcp-data/registry/divider.json +1 -1
  136. package/dist/mcp-data/registry/empty-state.json +1 -1
  137. package/dist/mcp-data/registry/field.json +1 -1
  138. package/dist/mcp-data/registry/icon.json +2 -2
  139. package/dist/mcp-data/registry/input.json +1 -1
  140. package/dist/mcp-data/registry/loader.json +1 -1
  141. package/dist/mcp-data/registry/markdown.json +1 -1
  142. package/dist/mcp-data/registry/menu.json +1 -1
  143. package/dist/mcp-data/registry/message-actions.json +1 -1
  144. package/dist/mcp-data/registry/message-bubble.json +1 -1
  145. package/dist/mcp-data/registry/message-composer.json +1 -1
  146. package/dist/mcp-data/registry/message-scroller.json +1 -1
  147. package/dist/mcp-data/registry/message.json +1 -1
  148. package/dist/mcp-data/registry/mobile-bar-nav.json +1 -1
  149. package/dist/mcp-data/registry/mobile-header.json +1 -1
  150. package/dist/mcp-data/registry/mobile-section-switcher.json +1 -1
  151. package/dist/mcp-data/registry/mobile-sheet-nav.json +1 -1
  152. package/dist/mcp-data/registry/modal.json +1 -1
  153. package/dist/mcp-data/registry/panel-nav.json +1 -1
  154. package/dist/mcp-data/registry/panel-sub-nav.json +1 -1
  155. package/dist/mcp-data/registry/panel-tool-header.json +1 -1
  156. package/dist/mcp-data/registry/panel-tool-search.json +1 -1
  157. package/dist/mcp-data/registry/panel-tools.json +1 -1
  158. package/dist/mcp-data/registry/radio.json +1 -1
  159. package/dist/mcp-data/registry/registry.json +68 -16
  160. package/dist/mcp-data/registry/scroll-overlay.json +1 -1
  161. package/dist/mcp-data/registry/select.json +1 -1
  162. package/dist/mcp-data/registry/shell-app.json +1 -1
  163. package/dist/mcp-data/registry/shell-page.json +1 -1
  164. package/dist/mcp-data/registry/shell-tools.json +1 -1
  165. package/dist/mcp-data/registry/skeleton.json +1 -1
  166. package/dist/mcp-data/registry/slider.json +1 -1
  167. package/dist/mcp-data/registry/swatch-picker.json +1 -1
  168. package/dist/mcp-data/registry/switch.json +1 -1
  169. package/dist/mcp-data/registry/tab-group.json +1 -1
  170. package/dist/mcp-data/registry/table.json +141 -21
  171. package/dist/mcp-data/registry/tag.json +23 -5
  172. package/dist/mcp-data/registry/text.json +1 -1
  173. package/dist/mcp-data/registry/toast.json +1 -1
  174. package/dist/mcp-data/registry/tooltip-chart.json +1 -1
  175. package/dist/mcp-data/registry/tooltip.json +1 -1
  176. package/dist/mcp-data/registry/typing-indicator.json +1 -1
  177. package/dist/react/ds-table.d.ts +3 -1
  178. package/dist/react/ds-table.js +3 -1
  179. package/dist/styles/table.css +491 -76
  180. package/dist/types/components/ButtonFilled/ButtonFilled.d.ts +6 -0
  181. package/dist/types/components/ButtonUnfilled/ButtonUnfilled.d.ts +13 -1
  182. package/dist/types/components/Table/Table.d.ts +41 -22
  183. package/dist/types/components/Table/index.d.ts +1 -1
  184. package/dist/types/components/Table/table-cell-model.d.ts +44 -0
  185. package/dist/types/components/Table/table-layout-controller.d.ts +65 -0
  186. package/dist/types/components/Table/table-load-controller.d.ts +49 -0
  187. package/dist/types/components/Table/table-model.d.ts +25 -0
  188. package/dist/types/components/Table/table-render-model.d.ts +41 -0
  189. package/dist/types/components/Table/table-types.d.ts +34 -2
  190. package/dist/types/components/Tag/Tag.d.ts +3 -0
  191. package/dist/types/components.d.ts +108 -8
  192. package/dist/types/utils/control-text.d.ts +2 -0
  193. package/dist/types/utils/index.d.ts +1 -1
  194. package/package.json +3 -3
  195. package/dist/components/p-BFXN4ioL.js +0 -2
  196. package/dist/components/p-BYre-ZDg.js +0 -2
  197. package/dist/components/p-Bj7KrXzw.js +0 -2
  198. package/dist/components/p-BxJurXEn.js +0 -2
  199. package/dist/components/p-C5gZHZHU.js +0 -2
  200. package/dist/components/p-CFNG0K3m.js +0 -2
  201. package/dist/components/p-CFNG0K3m.js.map +0 -1
  202. package/dist/components/p-CIlO_j3q.js +0 -2
  203. package/dist/components/p-DAbaMtEX.js +0 -2
  204. package/dist/components/p-DAbaMtEX.js.map +0 -1
  205. package/dist/components/p-DK4PGrHm.js +0 -2
  206. package/dist/components/p-DumWT7NN.js +0 -2
  207. package/dist/components/p-DumWT7NN.js.map +0 -1
  208. package/dist/components/p-c1pY-Mqq.js +0 -2
  209. package/dist/components/p-c1pY-Mqq.js.map +0 -1
  210. package/dist/components/p-x9jO4lZ7.js +0 -2
  211. /package/dist/components/{p-DBchq8Zw.js.map → p-B6linhwY.js.map} +0 -0
  212. /package/dist/components/{p-DcmqsAMP.js.map → p-BJVzrR_P.js.map} +0 -0
  213. /package/dist/components/{p-BX57q1Lo.js.map → p-BSdAFIcG.js.map} +0 -0
  214. /package/dist/components/{p-DGkWMhHO.js.map → p-BULWzaBs.js.map} +0 -0
  215. /package/dist/components/{p-B_eqa09C.js.map → p-C-aCs6ID.js.map} +0 -0
  216. /package/dist/components/{p-BRajO-Za.js.map → p-C5ecLNZA.js.map} +0 -0
  217. /package/dist/components/{p-DKftz0O8.js.map → p-COmqMfQo.js.map} +0 -0
  218. /package/dist/components/{p-DxygUurH.js.map → p-CeyoHpPn.js.map} +0 -0
  219. /package/dist/components/{p-CdFnvBem.js.map → p-CiUciD5Z.js.map} +0 -0
  220. /package/dist/components/{p-DBJJiSP0.js.map → p-DMJEOAwP.js.map} +0 -0
  221. /package/dist/components/{p-DkhE3BsH.js.map → p-Dz3Ufmr4.js.map} +0 -0
  222. /package/dist/components/{p-Doz2uwRv.js.map → p-fkQhr5s9.js.map} +0 -0
@@ -22,7 +22,7 @@
22
22
  "avoidWhen": [
23
23
  "Content is primarily editorial prose; use semantic product-owned markup with the prose recipe instead.",
24
24
  "A small label/value set reads more clearly as a list, description list, or card.",
25
- "The interface requires tree-grid behavior, nested grouping, inline cell editing, row reordering, column pinning, or virtualization; those are outside this first contract.",
25
+ "The interface requires tree-grid behavior, nested grouping, inline cell editing, row reordering, multiple pinned columns per edge, or virtualization; those are outside this first contract.",
26
26
  "The application expects the component to fetch, paginate, sort, group, or mutate records internally."
27
27
  ],
28
28
  "commonlyComposedWith": [
@@ -34,24 +34,30 @@
34
34
  "component:ds-icon"
35
35
  ],
36
36
  "accessibility": [
37
- "The rendered structure uses a native table, caption, column headers, row groups, and data cells; keep the caption non-empty even when it is visually hidden.",
37
+ "The rendered structure uses a native table, caption, column headers, row groups, and data cells; keep the caption non-empty. The native caption remains visually hidden as the table's accessible name. When caption visibility is enabled, a presentational title bar renders as component chrome outside the horizontal scrollport so it stays aligned with the table frame without duplicating the accessible name.",
38
38
  "Sorting controls remain buttons inside column headers. Member-row sort uses aria-sort, while independent group order is described by the grouped header control.",
39
- "Selection controls expose checkbox semantics and explicit row names; select-all affects selectable loaded rows only and preserves selected identities outside the loaded window.",
39
+ "Selection controls expose checkbox semantics and explicit row names; select-all affects selectable loaded rows only and preserves selected identities outside the loaded window. When grouping is active with multi-selection, each group section header also exposes a checkbox that selects or clears that group's selectable loaded rows.",
40
+ "Interactive rows are focusable as native rows, activate with Enter or Space, and do not steal activation from their nested selection or action controls.",
40
41
  "Scrollable table regions become keyboard focusable and named only when overflow exists.",
41
- "Incremental loading, newly appended row counts, failures, and end-of-results transitions are announced through a polite status region without replacing visible state."
42
+ "Incremental loading, newly appended row counts, failures, and end-of-results transitions are announced through a polite status region without replacing visible state.",
43
+ "An optional result summary footer appears below the table frame when both displayedCount and totalCount are finite, formatting Displaying {displayed} of {total} in text-body-medium secondary."
42
44
  ],
43
45
  "states": [
44
- "Columns, rows, groups, grouping, sorting, selected row IDs, and lazy-loading state are controlled inputs. Events communicate intent and never mutate those inputs.",
45
- "Grouping supports one level. The application supplies ordered groups and ordered member rows; group order and member-row sort are independent controls.",
46
+ "Columns, rows, groups, grouping, sorting, selected row IDs, collapsed group IDs, displayedCount, totalCount, and lazy-loading state are controlled inputs. Events communicate intent and never mutate those inputs.",
47
+ "Grouping supports one level. The application supplies ordered groups and ordered member rows; group order and member-row sort are independent controls. Group section headers show an emphasized text-body-medium label followed by a rounded sm Tag containing only the numeric count. The Tag uses the group's faint intent surface (neutral when intent is absent) and a small-elevation, button-hugging wrapper; a localized countLabel remains available to assistive technology. Groups may declare an optional intent (brand, neutral, negative, warning, caution, positive) that paints an LTR gradient from the faint intent surface at the label edge into faint neutral at the trailing edge, plus a bold intent title color. Neutral groups remain solid faint neutral. Section separators are painted inside that viewport-width surface so translucent border tokens composite with each intent background; the final collapsed section does not duplicate the terminal table edge. A trailing visually neutral icon-only ButtonUnfilled uses the 24px md double-inset action recipe and toggles controlled collapsedGroupIds, showing ChevronUp while expanded and ChevronDown while collapsed. While any group remains expanded, a ChevronDownUp action uses the xs sort-control recipe widened to a table-owned 24px lane and collapses every group. It appears at the inline-end edge of the trailing column declared with kind action when one exists; otherwise a button-hugging 24px by 16px medium-elevation surface floats at the visible scrollport's header edge so the action remains reachable while member columns scroll beneath it. The section header stays pinned to the visible horizontal scrollport and spans the smaller of the visible viewport or table width.",
46
48
  "An active member sort toggles only between ascending and descending; activating a different sortable label moves sorting to that key. Applications may still clear the controlled sort state explicitly. The grouped column toggles ascending and descending group order without clearing member sort.",
47
49
  "Compound headers can expose multiple related labels with independent sort keys. Each label owns only its content-width interaction target while the active direction remains in the column's alignment-aware sort lane.",
48
- "Initial loading uses row-shaped skeletons. Empty and initial error states remain inside the table. Incremental loading retains existing rows and supports automatic sentinel, manual load-more, retry, and terminal states.",
50
+ "Initial loading uses row-shaped skeletons that preserve the standard single-line cell padding, row dividers, column dividers, selection lane, and sticky-lane boundaries. Empty and initial error states remain inside the table. Incremental loading retains existing rows and supports automatic sentinel, manual load-more, retry, and terminal states.",
49
51
  "Lazy loading replaces pagination. The application owns requests, cancellation, cursors, deduplication, appended records, and query identity; loadIdentity resets the component request guard.",
50
- "Cells accept scalar content, standard primary-and-secondary or dual-primary text primitives, declarative Tag, icon-only, 16:9 Image, or ButtonUnfilled Action content, or explicit Empty and Blank absence values. Single-track text, selection, tag-only, icon-only, Action, Empty, and Blank cells resolve a 40px base contract while multi-track cells grow downward. Action cells primarily use an icon-only Ellipses treatment, render a 24px small ButtonUnfilled inside 8px padding, default to unbordered, and emit row, column, and action identities through dsCellAction. Image cells render a 52px-high bordered preview inside 6px outer padding, establishing a 64px row, and fall back to an accessible placeholder when no source is supplied. Empty means the data applies but has no value and renders an em dash with localized assistive text; Blank means the data is not applicable and renders no content. Nullish values remain backward-compatible Empty values. Stable type and variant hooks keep every cell family independently styleable.",
52
+ "Rows opt into complete-row activation with interactive and emit dsRowActivate without owning navigation. Selection paints through the shared selected ::before layer; interactive hover and press paint through the shared ::after layer.",
53
+ "Row selection owns the single sticky start lane. One explicitly sized application column can own the sticky end lane, or the start lane when selection is absent. Each sticky boundary owns one fixed divider and a row-clipped directional shadow that paints into the adjacent scrolling columns only while more inline content remains beyond that edge.",
54
+ "Cells accept scalar content, standard primary-and-secondary or dual-primary text primitives, declarative Tag, icon-only, 16:9 Image, or ButtonUnfilled Action content, or explicit Empty and Blank absence values. Single-track text, selection, tag-only, icon-only, Action, Empty, and Blank cells resolve a 40px base contract while multi-track cells grow downward. Tag cells use the double-inset Tag recipe; the md tag-only recipe is 24px inside 8px padding, while compound tag tracks preserve their peer text alignment. Action cells primarily use an icon-only Ellipses treatment, render a 24px small ButtonUnfilled inside 8px padding, default to unbordered, and emit row, column, and action identities through dsCellAction. Image cells render a 48px-high 16:9 bordered preview inside 8px outer padding, establishing a 64px row and a 102px column recipe, and fall back to an accessible placeholder when no source is supplied. Empty means the data applies but has no value and renders an em dash with localized assistive text; Blank means the data is not applicable and renders no content. Nullish values remain backward-compatible Empty values. Stable type and variant hooks keep every cell family independently styleable.",
51
55
  "The public @ds-mo/ui/table.css export is the authoritative zero-specificity visual recipe. Override its --ds-table-* properties or target its stable element classes for product-specific styling."
52
56
  ],
53
57
  "responsiveBehavior": [
54
58
  "The table keeps native semantics at narrow widths and scrolls horizontally instead of transforming into cards.",
59
+ "stickyHeader follows the active vertical scroll owner: a maxHeight table keeps its native header inside the bounded table scrollport, while a document-flow table retains a semantic native header and renders a sticky control header whose columns follow the body's native inline scroll timeline. Consumers set --ds-table-sticky-header-offset when page chrome occupies the scrollport edge.",
60
+ "Without maxHeight the table grows the document and automatic lazy loading observes the browser viewport. Horizontal scrolling chains vertical wheel and trackpad movement to the page instead of trapping it.",
55
61
  "Explicit column sizes establish the horizontal overflow width; unsized columns share remaining space.",
56
62
  "Prefer the named xs through xl column sizes, which resolve through TokoMo's table-column width tokens; numeric pixel sizes remain an escape hatch for exceptional application-owned widths.",
57
63
  "Column headers use one 32px geometry. Body rows use a 40px single-track minimum; cell-specific outer padding and track geometry align text, selection, Tag, icon, Action, Empty, Blank, and Image primitives while additional tracks grow downward.",
@@ -59,12 +65,12 @@
59
65
  ],
60
66
  "frameworkCaveats": {
61
67
  "customElements": [
62
- "Assign columns, rows, groups, grouping, sort, and selectedRowIds through JavaScript properties rather than HTML attribute strings.",
63
- "Replace controlled property values in response to dsSortChange, dsGroupingChange, dsSelectionChange, and dsLoadMore."
68
+ "Assign columns, rows, groups, grouping, sort, selectedRowIds, collapsedGroupIds, displayedCount, and totalCount through JavaScript properties rather than HTML attribute strings.",
69
+ "Replace controlled property values in response to dsSortChange, dsGroupingChange, dsGroupCollapseChange, dsSelectionChange, and dsLoadMore; route dsRowActivate in the application owner."
64
70
  ],
65
71
  "react": [
66
72
  "Pass new array and state-object references when the controlled data or query state changes.",
67
- "Apply emitted sort, grouping, selection, action, or loading detail to application state and perform data transformations in the owning layer."
73
+ "Apply emitted sort, grouping, collapse, selection, action, or loading detail to application state and perform data transformations in the owning layer."
68
74
  ],
69
75
  "angular": [
70
76
  "Bind complex table inputs as properties and update the controlled values from the corresponding output events.",
@@ -90,7 +96,7 @@
90
96
  "attribute": "caption",
91
97
  "required": true,
92
98
  "mutable": false,
93
- "description": "Required accessible table name, rendered as a native caption."
99
+ "description": "Required accessible table name, retained as a native caption."
94
100
  },
95
101
  "captionVisibility": {
96
102
  "type": "TableCaptionVisibility",
@@ -98,7 +104,16 @@
98
104
  "attribute": "caption-visibility",
99
105
  "default": "'hidden'",
100
106
  "required": false,
101
- "mutable": false
107
+ "mutable": false,
108
+ "description": "Shows a matching presentational title bar above the native table frame."
109
+ },
110
+ "collapsedGroupIds": {
111
+ "type": "string[]",
112
+ "resolvedType": "string[]",
113
+ "default": "[]",
114
+ "required": false,
115
+ "mutable": false,
116
+ "description": "Controlled collapsed group identities. Groups not listed remain expanded."
102
117
  },
103
118
  "columns": {
104
119
  "type": "TableColumn[]",
@@ -108,6 +123,14 @@
108
123
  "mutable": false,
109
124
  "description": "Stable column definitions. Assign through JavaScript."
110
125
  },
126
+ "displayedCount": {
127
+ "type": "number | undefined",
128
+ "resolvedType": "number | undefined",
129
+ "attribute": "displayed-count",
130
+ "required": false,
131
+ "mutable": false,
132
+ "description": "Optional result summary footer. When both `displayedCount` and `totalCount`\nare finite numbers, the table shows “Displaying {displayed} of {total}”."
133
+ },
111
134
  "emptyBody": {
112
135
  "type": "string",
113
136
  "resolvedType": "string",
@@ -272,6 +295,15 @@
272
295
  "mutable": false,
273
296
  "description": "Maximum scroll-region height. Numbers resolve to CSS pixels."
274
297
  },
298
+ "resultSummaryLabel": {
299
+ "type": "string",
300
+ "resolvedType": "string",
301
+ "attribute": "result-summary-label",
302
+ "default": "'Displaying {displayed} of {total}'",
303
+ "required": false,
304
+ "mutable": false,
305
+ "description": "Supports {displayed} and {total} placeholders."
306
+ },
275
307
  "retryLabel": {
276
308
  "type": "string",
277
309
  "resolvedType": "string",
@@ -344,6 +376,13 @@
344
376
  "default": "false",
345
377
  "required": false,
346
378
  "mutable": false
379
+ },
380
+ "totalCount": {
381
+ "type": "number | undefined",
382
+ "resolvedType": "number | undefined",
383
+ "attribute": "total-count",
384
+ "required": false,
385
+ "mutable": false
347
386
  }
348
387
  },
349
388
  "events": [
@@ -354,6 +393,13 @@
354
393
  "cancelable": true,
355
394
  "composed": true
356
395
  },
396
+ {
397
+ "name": "dsGroupCollapseChange",
398
+ "detail": "TableGroupCollapseChangeDetail",
399
+ "bubbles": true,
400
+ "cancelable": true,
401
+ "composed": true
402
+ },
357
403
  {
358
404
  "name": "dsGroupingChange",
359
405
  "detail": "TableGroupingChangeDetail",
@@ -368,6 +414,13 @@
368
414
  "cancelable": true,
369
415
  "composed": true
370
416
  },
417
+ {
418
+ "name": "dsRowActivate",
419
+ "detail": "TableRowActivateDetail",
420
+ "bubbles": true,
421
+ "cancelable": true,
422
+ "composed": true
423
+ },
371
424
  {
372
425
  "name": "dsSelectionChange",
373
426
  "detail": "TableSelectionChangeDetail",
@@ -393,7 +446,7 @@
393
446
  "attribute": "caption",
394
447
  "required": true,
395
448
  "mutable": false,
396
- "description": "Required accessible table name, rendered as a native caption."
449
+ "description": "Required accessible table name, retained as a native caption."
397
450
  },
398
451
  "captionVisibility": {
399
452
  "type": "TableCaptionVisibility",
@@ -401,7 +454,16 @@
401
454
  "attribute": "caption-visibility",
402
455
  "default": "'hidden'",
403
456
  "required": false,
404
- "mutable": false
457
+ "mutable": false,
458
+ "description": "Shows a matching presentational title bar above the native table frame."
459
+ },
460
+ "collapsedGroupIds": {
461
+ "type": "string[]",
462
+ "resolvedType": "string[]",
463
+ "default": "[]",
464
+ "required": false,
465
+ "mutable": false,
466
+ "description": "Controlled collapsed group identities. Groups not listed remain expanded."
405
467
  },
406
468
  "columns": {
407
469
  "type": "TableColumn[]",
@@ -411,6 +473,14 @@
411
473
  "mutable": false,
412
474
  "description": "Stable column definitions. Assign through JavaScript."
413
475
  },
476
+ "displayedCount": {
477
+ "type": "number | undefined",
478
+ "resolvedType": "number | undefined",
479
+ "attribute": "displayed-count",
480
+ "required": false,
481
+ "mutable": false,
482
+ "description": "Optional result summary footer. When both `displayedCount` and `totalCount`\nare finite numbers, the table shows “Displaying {displayed} of {total}”."
483
+ },
414
484
  "emptyBody": {
415
485
  "type": "string",
416
486
  "resolvedType": "string",
@@ -575,6 +645,15 @@
575
645
  "mutable": false,
576
646
  "description": "Maximum scroll-region height. Numbers resolve to CSS pixels."
577
647
  },
648
+ "resultSummaryLabel": {
649
+ "type": "string",
650
+ "resolvedType": "string",
651
+ "attribute": "result-summary-label",
652
+ "default": "'Displaying {displayed} of {total}'",
653
+ "required": false,
654
+ "mutable": false,
655
+ "description": "Supports {displayed} and {total} placeholders."
656
+ },
578
657
  "retryLabel": {
579
658
  "type": "string",
580
659
  "resolvedType": "string",
@@ -647,6 +726,13 @@
647
726
  "default": "false",
648
727
  "required": false,
649
728
  "mutable": false
729
+ },
730
+ "totalCount": {
731
+ "type": "number | undefined",
732
+ "resolvedType": "number | undefined",
733
+ "attribute": "total-count",
734
+ "required": false,
735
+ "mutable": false
650
736
  }
651
737
  },
652
738
  "events": [
@@ -657,6 +743,13 @@
657
743
  "cancelable": true,
658
744
  "composed": true
659
745
  },
746
+ {
747
+ "name": "dsGroupCollapseChange",
748
+ "detail": "TableGroupCollapseChangeDetail",
749
+ "bubbles": true,
750
+ "cancelable": true,
751
+ "composed": true
752
+ },
660
753
  {
661
754
  "name": "dsGroupingChange",
662
755
  "detail": "TableGroupingChangeDetail",
@@ -671,6 +764,13 @@
671
764
  "cancelable": true,
672
765
  "composed": true
673
766
  },
767
+ {
768
+ "name": "dsRowActivate",
769
+ "detail": "TableRowActivateDetail",
770
+ "bubbles": true,
771
+ "cancelable": true,
772
+ "composed": true
773
+ },
674
774
  {
675
775
  "name": "dsSelectionChange",
676
776
  "detail": "TableSelectionChangeDetail",
@@ -708,11 +808,11 @@
708
808
  "import": "import { DsTable } from '@ds-mo/ui/angular/ds-table';",
709
809
  "example": "<ds-table></ds-table>"
710
810
  },
711
- "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: columns, grouping, groups, rows, selectedRowIds, sort.",
811
+ "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: collapsedGroupIds, columns, grouping, groups, rows, selectedRowIds, sort.",
712
812
  "peerDependencies": {
713
813
  "required": [
714
814
  "@ds-mo/tokens ^6.0.0",
715
- "@ds-mo/icons ^6.5.0"
815
+ "@ds-mo/icons ^6.6.0"
716
816
  ],
717
817
  "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
718
818
  }
@@ -736,22 +836,42 @@
736
836
  "files": [
737
837
  {
738
838
  "path": "src/wc/components/Table/Table.css",
739
- "content": "@import '../../styles/table.css';\n\n:host {\n display: block;\n box-sizing: border-box;\n min-width: 0;\n max-width: 100%;\n}\n",
839
+ "content": "@import '../../styles/table.css';\n@import '../../styles/control-elevation.css';\n\n:host {\n display: block;\n box-sizing: border-box;\n min-width: 0;\n max-width: 100%;\n}\n",
740
840
  "type": "registry:ui"
741
841
  },
742
842
  {
743
843
  "path": "src/wc/components/Table/Table.tsx",
744
- "content": "import {\n Component,\n Event,\n EventEmitter,\n h,\n Host,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport {\n deriveTableSelectionState,\n isTableCellAction,\n isTableCellBlank,\n isTableCellEmpty,\n isTableCellIcon,\n isTableCellImage,\n isTableCellPrimaryText,\n isTableCellText,\n isTableCellTag,\n nextTableGroupOrder,\n nextTableSortState,\n resolvedTableGroupCount,\n tableColumnSize,\n tableExplicitMinWidth,\n tableModelIssues,\n tableRows,\n tableRowSelectionLabel,\n toggleAllLoadedTableRows,\n toggleTableRowSelection,\n} from './table-model';\nimport type {\n TableCaptionVisibility,\n TableCellActionDetail,\n TableCellValue,\n TableColumn,\n TableGroup,\n TableGroupingChangeDetail,\n TableGroupingState,\n TableLoadMoreDetail,\n TableLoadMoreMode,\n TableLoadMoreReason,\n TableRow,\n TableSelectionChangeDetail,\n TableSelectionMode,\n TableSortChangeDetail,\n TableSortState,\n} from './table-types';\n\n@Component({\n tag: 'ds-table',\n styleUrl: 'Table.css',\n scoped: true,\n})\nexport class Table {\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 and group-order direction. */\n @Prop() grouping: TableGroupingState | null = null;\n /** Controlled member-row sort state. */\n @Prop() sort: TableSortState | null = null;\n\n /** Required accessible table name, rendered as a native caption. */\n @Prop() caption!: string;\n @Prop() captionVisibility: TableCaptionVisibility = 'hidden';\n @Prop() stickyHeader: boolean = false;\n /** Maximum scroll-region height. Numbers resolve to CSS pixels. */\n @Prop() maxHeight: string | number | undefined;\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 @Prop() skeletonRows: number = 5;\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 /** Enable application-owned incremental loading without pagination. */\n @Prop() lazyLoading: boolean = false;\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 results';\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\n @Event() dsSortChange!: EventEmitter<TableSortChangeDetail>;\n @Event() dsGroupingChange!: EventEmitter<TableGroupingChangeDetail>;\n @Event() dsSelectionChange!: EventEmitter<TableSelectionChangeDetail>;\n @Event() dsLoadMore!: EventEmitter<TableLoadMoreDetail>;\n @Event() dsCellAction!: EventEmitter<TableCellActionDetail>;\n\n @State() private overflowStart = false;\n @State() private overflowEnd = false;\n @State() private scrollable = false;\n @State() private intersectionSupported = true;\n @State() private announcement = '';\n\n private viewportEl: HTMLElement | null = null;\n private tableEl: HTMLTableElement | null = null;\n private sentinelEl: HTMLElement | null = null;\n private resizeObserver: ResizeObserver | null = null;\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 private previousModelWarning = '';\n private hasLoaded = false;\n private reconnectFrame: number | null = null;\n\n componentWillLoad(): void {\n this.previousLoadedRowCount = this.loadedRows.length;\n this.intersectionSupported = typeof IntersectionObserver !== 'undefined';\n this.warnModelIssues();\n }\n\n componentDidLoad(): void {\n this.hasLoaded = true;\n this.connectViewportObserver();\n this.syncOverflow();\n this.connectIntersectionObserver();\n }\n\n componentDidRender(): void {\n this.connectIntersectionObserver();\n }\n\n connectedCallback(): void {\n if (!this.hasLoaded) return;\n if (this.reconnectFrame !== null) cancelAnimationFrame(this.reconnectFrame);\n this.reconnectFrame = requestAnimationFrame(() => {\n this.reconnectFrame = null;\n this.connectViewportObserver();\n this.syncOverflow();\n this.connectIntersectionObserver();\n });\n }\n\n disconnectedCallback(): void {\n if (this.reconnectFrame !== null) cancelAnimationFrame(this.reconnectFrame);\n this.reconnectFrame = null;\n this.viewportEl?.removeEventListener('scroll', this.syncOverflow);\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n this.disconnectIntersectionObserver();\n }\n\n @Watch('columns')\n @Watch('grouping')\n handleStructureChange(): void {\n this.warnModelIssues();\n this.resetLoadRequest();\n }\n\n @Watch('rows')\n @Watch('groups')\n handleDataChange(): void {\n const nextCount = this.loadedRows.length;\n if (nextCount > this.previousLoadedRowCount) {\n const added = nextCount - this.previousLoadedRowCount;\n this.announcement = this.rowsLoadedLabel\n .replace('{count}', String(added))\n .replace('{total}', String(nextCount));\n }\n if (nextCount > this.requestedRowCount) this.requestPending = false;\n this.previousLoadedRowCount = nextCount;\n this.warnModelIssues();\n this.disconnectIntersectionObserver();\n }\n\n @Watch('loadIdentity')\n handleLoadIdentityChange(): void {\n this.previousLoadedRowCount = this.loadedRows.length;\n this.resetLoadRequest();\n }\n\n @Watch('lazyLoading')\n @Watch('loadMoreMode')\n @Watch('loadMoreThreshold')\n handleLazyConfigurationChange(): void {\n this.disconnectIntersectionObserver();\n }\n\n @Watch('loadingMore')\n handleLoadingMoreChange(loading: boolean): void {\n if (loading) {\n this.requestPending = true;\n this.announcement = this.loadingMoreLabel;\n } else if (this.loadMoreMode === 'manual') {\n // A completed manual request may legitimately append no rows. Permit a\n // deliberate follow-up activation; automatic mode stays guarded until\n // the data shape or query identity changes.\n this.requestPending = false;\n }\n this.disconnectIntersectionObserver();\n }\n\n @Watch('loadMoreError')\n handleLoadMoreErrorChange(error: string | undefined): void {\n if (error?.trim()) {\n this.requestPending = false;\n this.announcement = error;\n }\n this.disconnectIntersectionObserver();\n }\n\n @Watch('hasMore')\n handleHasMoreChange(hasMore: boolean, hadMore: boolean): void {\n if (!hasMore) {\n this.requestPending = false;\n if (hadMore) this.announcement = this.endOfResultsLabel;\n }\n this.disconnectIntersectionObserver();\n }\n\n private get grouped(): boolean {\n return this.grouping !== null;\n }\n\n private get selectable(): boolean {\n return this.selectionMode === 'multiple';\n }\n\n private get loadedRows(): TableRow[] {\n return tableRows(this.rows, this.groups, this.grouped);\n }\n\n private get hasData(): boolean {\n return this.loadedRows.length > 0;\n }\n\n private get totalColumns(): number {\n return this.columns.length + (this.selectable ? 1 : 0);\n }\n\n private get selectedSet(): Set<string> {\n return new Set(this.selectedRowIds);\n }\n\n private get selectionState() {\n return deriveTableSelectionState(this.loadedRows, this.selectedRowIds);\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 tableStyle(): Record<string, string> | undefined {\n const width = tableExplicitMinWidth(this.columns);\n return width == null ? undefined : { '--ds-table-explicit-min-inline-size': width };\n }\n\n private warnModelIssues(): void {\n const issues = tableModelIssues(this.columns, this.rows, this.groups, this.grouped);\n if (!this.caption?.trim()) issues.unshift('A non-empty caption is required.');\n if (this.grouping && !this.columns.some(column => column.id === this.grouping!.columnId)) {\n issues.push(`Grouping references unknown column id: ${this.grouping.columnId}`);\n }\n if (\n this.sort &&\n !this.columns.some(column =>\n column.id === this.sort!.columnId ||\n column.headerSegments?.some(segment => segment.sortKey === this.sort!.columnId),\n )\n ) {\n issues.push(`Sorting references unknown column id: ${this.sort.columnId}`);\n }\n const message = issues.join(' ');\n if (!message || message === this.previousModelWarning) return;\n this.previousModelWarning = message;\n console.warn(`[ds-table] ${message}`);\n }\n\n private connectViewportObserver(): void {\n if (!this.viewportEl) return;\n this.viewportEl.removeEventListener('scroll', this.syncOverflow);\n this.viewportEl.addEventListener('scroll', this.syncOverflow, { passive: true });\n\n this.resizeObserver?.disconnect();\n if (typeof ResizeObserver === 'undefined') return;\n this.resizeObserver = new ResizeObserver(this.syncOverflow);\n this.resizeObserver.observe(this.viewportEl);\n if (this.tableEl) this.resizeObserver.observe(this.tableEl);\n }\n\n private syncOverflow = (): void => {\n const viewport = this.viewportEl;\n if (!viewport) return;\n const overflows = viewport.scrollWidth - viewport.clientWidth > 1;\n const scrollable = overflows || viewport.scrollHeight - viewport.clientHeight > 1;\n const nextStart = overflows && viewport.scrollLeft > 1;\n const nextEnd = overflows && viewport.scrollLeft + viewport.clientWidth < viewport.scrollWidth - 1;\n if (nextStart !== this.overflowStart) this.overflowStart = nextStart;\n if (nextEnd !== this.overflowEnd) this.overflowEnd = nextEnd;\n if (scrollable !== this.scrollable) this.scrollable = scrollable;\n };\n\n private resetLoadRequest(): void {\n this.requestPending = false;\n this.requestedRowCount = this.loadedRows.length;\n this.disconnectIntersectionObserver();\n }\n\n private disconnectIntersectionObserver(): void {\n this.intersectionObserver?.disconnect();\n this.intersectionObserver = null;\n this.observedSentinel = null;\n }\n\n private connectIntersectionObserver(): void {\n if (\n !this.lazyLoading ||\n this.loadMoreMode !== 'auto' ||\n !this.hasMore ||\n !!this.loadMoreError?.trim() ||\n !this.intersectionSupported ||\n !this.viewportEl ||\n !this.sentinelEl\n ) {\n this.disconnectIntersectionObserver();\n return;\n }\n\n if (this.intersectionObserver && this.observedSentinel === this.sentinelEl) return;\n this.disconnectIntersectionObserver();\n\n const threshold = Number.isFinite(this.loadMoreThreshold)\n ? Math.max(0, this.loadMoreThreshold)\n : 0;\n this.intersectionObserver = new IntersectionObserver(\n entries => {\n if (entries.some(entry => entry.isIntersecting)) this.requestLoadMore('auto');\n },\n {\n root: this.viewportEl,\n rootMargin: `0px 0px ${threshold}px 0px`,\n },\n );\n this.observedSentinel = this.sentinelEl;\n this.intersectionObserver.observe(this.sentinelEl);\n }\n\n private requestLoadMore(reason: TableLoadMoreReason): void {\n if (\n !this.lazyLoading ||\n !this.hasMore ||\n this.loadingMore ||\n (reason !== 'retry' && !!this.loadMoreError?.trim()) ||\n this.requestPending\n ) return;\n\n this.requestPending = true;\n this.requestedRowCount = this.loadedRows.length;\n this.announcement = this.loadingMoreLabel;\n this.dsLoadMore.emit({\n reason,\n loadIdentity: this.loadIdentity,\n loadedRowCount: this.requestedRowCount,\n });\n }\n\n private emitSort(column: TableColumn, sortKey = column.id): void {\n if (this.grouping?.columnId === column.id && sortKey === column.id) {\n this.dsGroupingChange.emit({ grouping: nextTableGroupOrder(this.grouping) });\n return;\n }\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.grouping?.columnId === column.id && sortKey === column.id) {\n const next = this.grouping.direction === 'asc' ? 'descending' : 'ascending';\n return `Sort ${column.header} groups ${next}. Currently grouped ${this.grouping.direction === 'asc' ? 'ascending' : 'descending'}.`;\n }\n\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 this.dsSelectionChange.emit({\n selectedRowIds: toggleAllLoadedTableRows(this.selectedRowIds, this.loadedRows),\n scope: 'all-loaded',\n selected: !this.selectionState.allSelected,\n });\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\"\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 renderColumnHeader(column: TableColumn) {\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 = !groupedColumn\n ? headerSegments.find(segment => segment.sortKey === this.sort?.columnId)\n : undefined;\n const activeMemberSort = !!activeMemberSegment;\n const activeSort = groupedColumn || activeMemberSort;\n const align = column.align ?? 'start';\n const direction = groupedColumn\n ? this.grouping!.direction\n : activeMemberSort\n ? this.sort!.direction\n : undefined;\n const memberAriaSort = activeMemberSort\n ? (this.sort!.direction === 'asc' ? 'ascending' : 'descending')\n : undefined;\n\n const labelControl = (\n <span class=\"ds-table__header-labels\">\n {headerSegments.map((segment, index) => {\n const segmentActive = groupedColumn\n ? headerSegments.length === 1\n : activeMemberSegment?.sortKey === segment.sortKey;\n const segmentInteractive = !!column.sortable || (groupedColumn && segment.sortKey === column.id);\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 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\"\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 sortControl = (\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 );\n\n return (\n <th\n key={column.id}\n class={`ds-table__header-cell ds-table__cell--align-${align}`}\n scope=\"col\"\n aria-sort={memberAriaSort}\n data-column-id={column.id}\n data-grouped={groupedColumn ? 'true' : undefined}\n data-sort-active={activeSort ? 'true' : undefined}\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 </span>\n </th>\n );\n }\n\n private renderCellValue(value: TableCellValue, column: TableColumn, row: TableRow) {\n if (isTableCellBlank(value)) return null;\n\n if (value == null || isTableCellEmpty(value)) {\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=\"secondary\"\n >\n <span aria-hidden=\"true\">—</span>\n <span class=\"ds-visually-hidden\">{this.emptyCellLabel}</span>\n </ds-text>\n );\n }\n\n if (isTableCellIcon(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 (isTableCellImage(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\n class=\"ds-table__cell-image-placeholder\"\n role=\"img\"\n aria-label={value.alt}\n />\n )}\n </span>\n );\n }\n\n if (isTableCellAction(value)) {\n return (\n <ds-button-unfilled\n variant={value.variant ?? 'label'}\n size=\"sm\"\n label={value.label ?? ''}\n icon={value.icon ?? ''}\n aria-label={value.ariaLabel ?? null}\n hasBorder={value.hasBorder ?? false}\n isInactive={value.isInactive ?? false}\n isLoading={value.isLoading ?? false}\n onDsClick={event => {\n event.stopPropagation();\n this.dsCellAction.emit({\n actionId: value.actionId,\n rowId: row.id,\n columnId: column.id,\n });\n }}\n />\n );\n }\n\n if (isTableCellTag(value)) {\n const variant = value.variant ?? 'tag-only';\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 />\n );\n\n if (variant === 'tag-only') return tag;\n\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 >\n {value.text}\n </ds-text>\n {variant === 'text-with-tag' && (\n <span class=\"ds-table__cell-tag-control-track\">{tag}</span>\n )}\n </span>\n );\n }\n\n const primaryText = isTableCellPrimaryText(value);\n const text = isTableCellText(value) || primaryText\n ? value\n : { primary: value, fontFeature: typeof value === 'number' ? 'tabular-nums' as const : 'normal' as const };\n const wraps = text.wrap ?? column.wrap ?? false;\n\n return (\n <span class={{ 'ds-table__cell-copy': true, 'ds-table__cell-copy--wrap': wraps }}>\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=\"primary\"\n lineTruncation={wraps ? 'none' : 1}\n wrap={wraps ? 'wrap' : 'nowrap'}\n fontFeature={text.fontFeature ?? 'normal'}\n >\n {text.primary}\n </ds-text>\n {text.secondary !== undefined && text.secondary !== '' && (\n <ds-text\n class=\"ds-table__cell-secondary ds-table__cell-track ds-table__cell-track--text\"\n as=\"span\"\n variant={primaryText ? 'text-body-medium' : 'text-body-small'}\n color={primaryText ? 'primary' : 'secondary'}\n lineTruncation={wraps ? 'none' : 1}\n wrap={wraps ? 'wrap' : 'nowrap'}\n >\n {text.secondary}\n </ds-text>\n )}\n </span>\n );\n }\n\n private renderRow(row: TableRow) {\n const selected = this.selectedSet.has(row.id);\n const rowSelectable = row.selectable !== false && !row.disabled;\n return (\n <tr\n key={row.id}\n class={{\n 'ds-table__row': true,\n 'ds-table__row--selected': selected,\n 'ds-table__row--disabled': !!row.disabled,\n }}\n data-row-id={row.id}\n data-selected={selected ? 'true' : undefined}\n aria-disabled={row.disabled ? 'true' : undefined}\n >\n {this.selectable && (\n <td class=\"ds-table__cell ds-table__selection-cell\">\n {this.renderSelectionControl(\n `${selected ? 'Deselect' : 'Select'} ${tableRowSelectionLabel(row, this.columns)}`,\n selected,\n false,\n !rowSelectable,\n () => this.emitRowSelection(row),\n )}\n </td>\n )}\n {this.columns.map(column => {\n const align = column.align ?? 'start';\n const value = row.cells[column.id];\n const tagCell = isTableCellTag(value);\n const iconCell = isTableCellIcon(value);\n const imageCell = isTableCellImage(value);\n const actionCell = isTableCellAction(value);\n const primaryTextCell = isTableCellPrimaryText(value);\n const emptyCell = value == null || isTableCellEmpty(value);\n const blankCell = isTableCellBlank(value);\n const textCell = primaryTextCell || (!tagCell && !iconCell && !imageCell && !actionCell && !emptyCell && !blankCell);\n const singleTextCell = textCell && !primaryTextCell && (!isTableCellText(value) || !value.secondary);\n const tagVariant = tagCell ? value.variant ?? 'tag-only' : undefined;\n const textVariant = textCell\n ? primaryTextCell\n ? 'primary-pair'\n : singleTextCell\n ? 'single'\n : 'multi'\n : undefined;\n const cellType = tagCell\n ? 'tag'\n : iconCell\n ? 'icon'\n : imageCell\n ? 'image'\n : actionCell\n ? 'action'\n : primaryTextCell\n ? 'primary-text'\n : emptyCell\n ? 'empty'\n : blankCell\n ? 'blank'\n : 'text';\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--image': imageCell,\n 'ds-table__cell--action': actionCell,\n 'ds-table__cell--primary-text': primaryTextCell,\n 'ds-table__cell--text-single': singleTextCell,\n 'ds-table__cell--text-multi': textCell && !singleTextCell,\n 'ds-table__cell--empty': emptyCell,\n 'ds-table__cell--blank': blankCell,\n }}\n data-column-id={column.id}\n data-cell-type={cellType}\n data-cell-variant={tagVariant ?? textVariant}\n >\n <span class=\"ds-table__cell-content\">\n {this.renderCellValue(value, column, row)}\n </span>\n </td>\n );\n })}\n </tr>\n );\n }\n\n private renderDataBodies() {\n if (!this.grouped) {\n return <tbody class=\"ds-table__body\">{this.rows.map(row => this.renderRow(row))}</tbody>;\n }\n\n return this.groups.map(group => {\n const count = resolvedTableGroupCount(group);\n const countLabel = group.countLabel ?? `${count} ${count === 1 ? 'item' : 'items'}`;\n return (\n <tbody class=\"ds-table__body ds-table__group\" data-group-id={group.id} key={group.id}>\n <tr class=\"ds-table__group-row\">\n <th class=\"ds-table__group-cell\" scope=\"rowgroup\" colSpan={this.totalColumns}>\n <span class=\"ds-table__group-content\">\n <ds-icon name=\"GroupBy\" size=\"sm\" color=\"secondary\" aria-hidden=\"true\" />\n <ds-text as=\"span\" variant=\"text-body-small\" emphasis={true} color=\"primary\">\n {group.label}\n </ds-text>\n <ds-text as=\"span\" variant=\"text-body-small\" color=\"secondary\">\n {countLabel}\n </ds-text>\n </span>\n </th>\n </tr>\n {group.rows.map(row => this.renderRow(row))}\n </tbody>\n );\n });\n }\n\n private renderSkeletonBody() {\n const count = Math.min(20, Math.max(1, Math.round(this.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 {this.selectable && (\n <td class=\"ds-table__cell ds-table__selection-cell\">\n <ds-skeleton variant=\"icon\" iconSize=\"md\" />\n </td>\n )}\n {this.columns.map(column => (\n <td class=\"ds-table__cell\" key={`skeleton-${index}:${column.id}`}>\n <span class=\"ds-table__cell-content\">\n <ds-skeleton variant=\"text\" textVariant=\"text-body-medium\" width=\"100%\" />\n </span>\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n );\n }\n\n private renderStateBody(kind: 'empty' | 'error') {\n const error = kind === 'error';\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={this.totalColumns}>\n <ds-empty-state\n icon={error ? 'ErrorTriangle' : 'Inbox'}\n heading={error ? this.errorHeading : this.emptyHeading}\n body={error ? this.errorBody : this.emptyBody}\n />\n </td>\n </tr>\n </tbody>\n );\n }\n\n private renderLazyBody() {\n if (!this.lazyLoading || !this.hasData) return null;\n const error = this.loadMoreError?.trim();\n const manualFallback = this.loadMoreMode === 'manual' || !this.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={this.totalColumns}>\n {error ? (\n <span class=\"ds-table__load-content ds-table__load-content--error\">\n <ds-icon name=\"ErrorTriangle\" size=\"sm\" color=\"negative\" aria-hidden=\"true\" />\n <ds-text as=\"span\" variant=\"text-body-small\" color=\"secondary\">{error}</ds-text>\n <ds-button-unfilled\n label={this.retryLabel}\n size=\"sm\"\n onDsClick={() => this.requestLoadMore('retry')}\n />\n </span>\n ) : this.loadingMore ? (\n <span class=\"ds-table__load-content\">\n <ds-loader size=\"sm\" color=\"secondary\" />\n <ds-text as=\"span\" variant=\"text-body-small\" color=\"secondary\">\n {this.loadingMoreLabel}\n </ds-text>\n </span>\n ) : this.hasMore && manualFallback ? (\n <ds-button-unfilled\n label={this.loadMoreLabel}\n size=\"sm\"\n onDsClick={() => this.requestLoadMore('manual')}\n />\n ) : this.hasMore ? (\n <span class=\"ds-table__auto-sentinel\" aria-hidden=\"true\" />\n ) : (\n <ds-text as=\"span\" variant=\"text-body-small\" color=\"secondary\">\n {this.endOfResultsLabel}\n </ds-text>\n )}\n </td>\n </tr>\n </tbody>\n );\n }\n\n render() {\n const selection = this.selectionState;\n const regionLabel = this.scrollLabel?.trim() || `${this.caption} scroll area`;\n const initialLoading = this.loading && !this.hasData;\n const initialError = !this.hasData && this.error;\n const viewportStyle = this.resolvedMaxHeight\n ? { '--ds-table-max-block-size': this.resolvedMaxHeight }\n : undefined;\n\n return (\n <Host class=\"table-host\">\n <div class={{\n 'ds-table': true,\n 'ds-table--sticky-header': this.stickyHeader,\n }}>\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 >\n <div\n class=\"ds-table__viewport\"\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': this.selectable,\n }}\n style={this.tableStyle}\n aria-busy={initialLoading || this.loadingMore ? 'true' : undefined}\n ref={element => {\n this.tableEl = element ?? null;\n }}\n >\n <caption\n class={{\n 'ds-table__caption': true,\n 'ds-visually-hidden': this.captionVisibility === 'hidden',\n }}\n >\n <ds-text as=\"span\" variant=\"text-title-small\" emphasis={true} color=\"primary\">\n {this.caption}\n </ds-text>\n </caption>\n <colgroup>\n {this.selectable && <col class=\"ds-table__selection-column\" />}\n {this.columns.map(column => {\n const width = tableColumnSize(column);\n return <col key={column.id} style={width ? { width } : undefined} />;\n })}\n </colgroup>\n <thead class=\"ds-table__head\">\n <tr class=\"ds-table__header-row\">\n {this.selectable && (\n <th class=\"ds-table__header-cell ds-table__selection-cell\" scope=\"col\">\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 </th>\n )}\n {this.columns.map(column => this.renderColumnHeader(column))}\n </tr>\n </thead>\n {initialLoading\n ? this.renderSkeletonBody()\n : initialError\n ? this.renderStateBody('error')\n : this.hasData\n ? this.renderDataBodies()\n : this.renderStateBody('empty')}\n {!initialLoading && !initialError && this.renderLazyBody()}\n </table>\n </div>\n </div>\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",
844
+ "content": "import {\n Component,\n Event,\n EventEmitter,\n h,\n Host,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport {\n deriveTableSelectionState,\n formatTableResultSummary,\n nextTableGroupOrder,\n nextTableSortState,\n tableColumnSize,\n tableModelIssues,\n tableRows,\n tableRowSelectionLabel,\n toggleAllLoadedTableRows,\n toggleTableGroupCollapsed,\n toggleTableGroupSelection,\n nextTableGroupsCollapsed,\n toggleTableRowSelection,\n} from './table-model';\nimport {\n resolveTableCellPresentation,\n type TableCellPresentation,\n} from './table-cell-model';\nimport {\n createTableRenderModel,\n type TableRenderModel,\n} from './table-render-model';\nimport { TableLayoutController } from './table-layout-controller';\nimport { TableLoadController } from './table-load-controller';\nimport type {\n TableCaptionVisibility,\n TableCellActionDetail,\n TableColumn,\n TableGroup,\n TableGroupCollapseChangeDetail,\n TableGroupingChangeDetail,\n TableGroupingState,\n TableLoadMoreDetail,\n TableLoadMoreMode,\n TableRow,\n TableRowActivateDetail,\n TableSelectionChangeDetail,\n TableSelectionMode,\n TableSortChangeDetail,\n TableSortState,\n} from './table-types';\n\n@Component({\n tag: 'ds-table',\n styleUrls: [\n '../../utils/interaction-fill.css',\n 'Table.css',\n ],\n scoped: true,\n})\nexport class Table {\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 and group-order direction. */\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 * Optional result summary footer. When both `displayedCount` and `totalCount`\n * are finite numbers, the table shows “Displaying {displayed} of {total}”.\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 @Prop() stickyHeader: boolean = false;\n /** Maximum scroll-region height. Numbers resolve to CSS pixels. */\n @Prop() maxHeight: string | number | undefined;\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 @Prop() skeletonRows: number = 5;\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 /** Enable application-owned incremental loading without pagination. */\n @Prop() lazyLoading: boolean = false;\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 results';\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\n @Event() dsSortChange!: EventEmitter<TableSortChangeDetail>;\n @Event() dsGroupingChange!: EventEmitter<TableGroupingChangeDetail>;\n @Event() dsGroupCollapseChange!: EventEmitter<TableGroupCollapseChangeDetail>;\n @Event() dsSelectionChange!: EventEmitter<TableSelectionChangeDetail>;\n @Event() dsLoadMore!: EventEmitter<TableLoadMoreDetail>;\n @Event() dsCellAction!: EventEmitter<TableCellActionDetail>;\n @Event() dsRowActivate!: EventEmitter<TableRowActivateDetail>;\n\n @State() private overflowStart = false;\n @State() private overflowEnd = false;\n @State() private scrollable = false;\n @State() private announcement = '';\n\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 previousModelWarning = '';\n private hasLoaded = false;\n private renderedModel: TableRenderModel | null = null;\n private readonly layoutController = new TableLayoutController({\n elements: () => ({\n viewport: this.viewportEl,\n table: 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 }),\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.lazyLoading,\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.resolvedMaxHeight !== undefined,\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\n componentWillLoad(): void {\n this.loadController.initialize();\n this.warnModelIssues();\n }\n\n componentDidLoad(): void {\n this.hasLoaded = true;\n this.layoutController.connect();\n this.loadController.connect();\n }\n\n componentDidRender(): void {\n this.layoutController.refresh();\n this.loadController.refresh();\n }\n\n connectedCallback(): void {\n if (!this.hasLoaded) return;\n this.layoutController.connect();\n this.loadController.connect();\n }\n\n disconnectedCallback(): void {\n this.layoutController.disconnect();\n this.loadController.disconnect();\n }\n\n @Watch('columns')\n @Watch('grouping')\n handleStructureChange(): void {\n this.warnModelIssues();\n this.loadController.structureChanged();\n }\n\n @Watch('rows')\n @Watch('groups')\n handleDataChange(): void {\n this.loadController.dataChanged();\n this.warnModelIssues();\n }\n\n @Watch('loadIdentity')\n handleLoadIdentityChange(): void {\n this.loadController.identityChanged();\n }\n\n @Watch('lazyLoading')\n @Watch('loadMoreMode')\n @Watch('loadMoreThreshold')\n handleLazyConfigurationChange(): void {\n this.loadController.configurationChanged();\n }\n\n @Watch('loadingMore')\n handleLoadingMoreChange(loading: boolean): void {\n this.loadController.loadingChanged(loading);\n }\n\n @Watch('loadMoreError')\n handleLoadMoreErrorChange(error: string | undefined): void {\n this.loadController.errorChanged(error);\n }\n\n @Watch('hasMore')\n handleHasMoreChange(hasMore: boolean, hadMore: boolean): void {\n this.loadController.hasMoreChanged(hasMore, hadMore);\n }\n\n private get grouped(): boolean {\n return this.grouping !== null;\n }\n\n private get selectable(): boolean {\n return this.selectionMode === 'multiple';\n }\n\n private get documentStickyHeader(): boolean {\n return this.stickyHeader && this.resolvedMaxHeight === undefined;\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 return createTableRenderModel({\n columns: this.columns,\n rows: this.rows,\n groups: this.groups,\n grouped: this.grouped,\n selectionMode: this.selectionMode,\n selectedRowIds: this.selectedRowIds,\n collapsedGroupIds: this.collapsedGroupIds,\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 warnModelIssues(): void {\n const issues = tableModelIssues(this.columns, this.rows, this.groups, this.grouped);\n if (!this.caption?.trim()) issues.unshift('A non-empty caption is required.');\n if (this.grouping && !this.columns.some(column => column.id === this.grouping!.columnId)) {\n issues.push(`Grouping references unknown column id: ${this.grouping.columnId}`);\n }\n if (\n this.sort &&\n !this.columns.some(column =>\n column.id === this.sort!.columnId ||\n column.headerSegments?.some(segment => segment.sortKey === this.sort!.columnId),\n )\n ) {\n issues.push(`Sorting references unknown column id: ${this.sort.columnId}`);\n }\n const stickyStart = this.columns.filter(column => column.sticky === 'start');\n const stickyEnd = this.columns.filter(column => column.sticky === 'end');\n if (stickyStart.length > 1 || (this.selectable && stickyStart.length > 0)) {\n issues.push('Only one sticky start column is supported, and row selection already owns that lane.');\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)) issues.push(`Sticky column ${column.id} requires an explicit size.`);\n }\n const message = issues.join(' ');\n if (!message || message === this.previousModelWarning) return;\n this.previousModelWarning = message;\n console.warn(`[ds-table] ${message}`);\n }\n\n private emitSort(column: TableColumn, sortKey = column.id): void {\n if (this.grouping?.columnId === column.id && sortKey === column.id) {\n this.dsGroupingChange.emit({ grouping: nextTableGroupOrder(this.grouping) });\n return;\n }\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.grouping?.columnId === column.id && sortKey === column.id) {\n const next = this.grouping.direction === 'asc' ? 'descending' : 'ascending';\n return `Sort ${column.header} groups ${next}. Currently grouped ${this.grouping.direction === 'asc' ? 'ascending' : 'descending'}.`;\n }\n\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\"], ds-button-unfilled',\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 handleRowKeydown(row: TableRow, event: KeyboardEvent): void {\n if (event.target !== event.currentTarget || (event.key !== 'Enter' && event.key !== ' ')) return;\n event.preventDefault();\n this.emitRowActivation(row, event);\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-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\n class={`ds-table__sticky-edge ds-table__sticky-edge--${sticky}`}\n aria-hidden=\"true\"\n />\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 = !groupedColumn\n ? headerSegments.find(segment => segment.sortKey === this.sort?.columnId)\n : undefined;\n const activeMemberSort = !!activeMemberSegment;\n const activeSort = groupedColumn || activeMemberSort;\n const align = column.align ?? 'start';\n const direction = groupedColumn\n ? this.grouping!.direction\n : activeMemberSort\n ? this.sort!.direction\n : undefined;\n const memberAriaSort = activeMemberSort\n ? (this.sort!.direction === 'asc' ? 'ascending' : 'descending')\n : undefined;\n\n const labelControl = (\n <span class=\"ds-table__header-labels\">\n {headerSegments.map((segment, index) => {\n const segmentActive = groupedColumn\n ? headerSegments.length === 1\n : activeMemberSegment?.sortKey === segment.sortKey;\n const segmentInteractive = interactive && (\n !!column.sortable || (groupedColumn && segment.sortKey === column.id)\n );\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 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\"\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 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 = collapseHost?.mode === 'action' && collapseHost.columnId === column.id;\n const blankActionCollapseHost = actionCollapseHost &&\n !column.header.trim() &&\n !column.headerSegments?.length;\n const collapseControl =\n interactive && actionCollapseHost ? (\n <span class=\"ds-table__collapse-slot\">\n {this.renderCollapseAllButton()}\n </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.columns.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 ) {\n const selection = model.selection;\n return (\n <thead class={{\n 'ds-table__head': true,\n 'ds-table__head--semantic-copy': !interactive,\n }} ref={element => {\n if (interactive) this.interactiveHeadEl = element ?? null;\n }}>\n <tr class=\"ds-table__header-row\">\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 ? 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 <span class=\"ds-visually-hidden\">Select rows</span>\n )}\n {this.renderStickyEdge('start')}\n </th>\n )}\n {this.columns.map(column => (\n this.renderColumnHeader(column, model, interactive, presentational)\n ))}\n </tr>\n </thead>\n );\n }\n\n private renderCellValue(\n cell: TableCellPresentation,\n column: TableColumn,\n row: TableRow,\n ) {\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=\"secondary\"\n >\n <span aria-hidden=\"true\">—</span>\n <span class=\"ds-visually-hidden\">{this.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 === '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\n class=\"ds-table__cell-image-placeholder\"\n role=\"img\"\n aria-label={value.alt}\n />\n )}\n </span>\n );\n }\n\n if (cell.kind === 'action') {\n const value = cell.value;\n return (\n <ds-button-unfilled\n variant={value.variant ?? 'label'}\n size=\"md\"\n isInset={true}\n insetDepth=\"double\"\n label={value.label ?? ''}\n icon={value.icon ?? ''}\n aria-label={value.ariaLabel ?? null}\n hasBorder={value.hasBorder ?? false}\n isInactive={value.isInactive ?? false}\n isLoading={value.isLoading ?? false}\n onDsClick={event => {\n event.stopPropagation();\n this.dsCellAction.emit({\n actionId: value.actionId,\n rowId: row.id,\n columnId: column.id,\n });\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=\"double\"\n />\n );\n\n if (variant === 'tag-only') return tag;\n\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 >\n {value.text}\n </ds-text>\n {variant === 'text-with-tag' && (\n <span class=\"ds-table__cell-tag-control-track\">{tag}</span>\n )}\n </span>\n );\n }\n\n const text = cell.value;\n const wraps = cell.wraps;\n\n return (\n <span class={{ 'ds-table__cell-copy': true, 'ds-table__cell-copy--wrap': wraps }}>\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=\"primary\"\n lineTruncation={wraps ? 'none' : 1}\n wrap={wraps ? 'wrap' : 'nowrap'}\n fontFeature={text.fontFeature ?? 'normal'}\n >\n {text.primary}\n </ds-text>\n {text.secondary !== undefined && text.secondary !== '' && (\n <ds-text\n class=\"ds-table__cell-secondary ds-table__cell-track ds-table__cell-track--text\"\n as=\"span\"\n variant={cell.primaryText ? 'text-body-medium' : 'text-body-small'}\n color={cell.primaryText ? 'primary' : 'secondary'}\n lineTruncation={wraps ? 'none' : 1}\n wrap={wraps ? 'wrap' : 'nowrap'}\n >\n {text.secondary}\n </ds-text>\n )}\n </span>\n );\n }\n\n private renderRow(row: TableRow, model: TableRenderModel) {\n const selected = model.selectedRowIds.has(row.id);\n const rowSelectable = row.selectable !== false && !row.disabled;\n return (\n <tr\n key={row.id}\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 }}\n data-row-id={row.id}\n data-selected={selected ? 'true' : undefined}\n aria-disabled={row.disabled ? 'true' : undefined}\n tabIndex={row.interactive && !row.disabled ? 0 : undefined}\n onClick={event => this.emitRowActivation(row, event)}\n onKeyDown={event => this.handleRowKeydown(row, event)}\n >\n {model.selectable && (\n <td 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 {this.renderSelectionControl(\n `${selected ? 'Deselect' : 'Select'} ${tableRowSelectionLabel(row, this.columns)}`,\n selected,\n false,\n !rowSelectable,\n () => this.emitRowSelection(row),\n )}\n {this.renderStickyEdge('start')}\n </td>\n )}\n {this.columns.map(column => {\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 imageCell = cell.kind === 'image';\n const actionCell = cell.kind === 'action';\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 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--image': imageCell,\n 'ds-table__cell--action': actionCell,\n 'ds-table__cell--primary-text': primaryTextCell,\n 'ds-table__cell--text-single': singleTextCell,\n 'ds-table__cell--text-multi': textCell && !singleTextCell,\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}\n >\n <span class=\"ds-table__cell-content ds-interaction-fill__content\">\n {this.renderCellValue(cell, column, row)}\n </span>\n {this.renderStickyEdge(column.sticky)}\n </td>\n );\n })}\n </tr>\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 renderDataBodies(model: TableRenderModel) {\n if (!model.grouped) {\n return (\n <tbody class=\"ds-table__body\">\n {this.rows.map(row => this.renderRow(row, model))}\n </tbody>\n );\n }\n\n return model.groups.map(groupModel => {\n const {\n group,\n count,\n countLabel,\n countIntent,\n collapsed: isCollapsed,\n intent,\n intentClass,\n labelColor,\n selection: groupSelection,\n } = groupModel;\n return (\n <tbody\n class=\"ds-table__body ds-table__group\"\n data-group-id={group.id}\n data-group-intent={intent}\n data-collapsed={isCollapsed ? 'true' : undefined}\n key={group.id}\n >\n <tr class=\"ds-table__group-row\">\n <th\n class={{\n 'ds-table__group-cell': true,\n [intentClass ?? '']: !!intentClass,\n }}\n scope=\"rowgroup\"\n colSpan={model.totalColumns}\n >\n <span class=\"ds-table__group-content\">\n {groupSelection && (\n <span class=\"ds-table__group-selection\">\n {this.renderSelectionControl(\n groupSelection.allSelected\n ? `Deselect ${group.label} group`\n : `Select ${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 <span\n class=\"ds-table__group-count ds-control-elevation ds-control-elevation--sm\"\n aria-hidden=\"true\"\n >\n <ds-tag\n label={String(count)}\n intent={countIntent}\n contrast=\"faint\"\n size=\"sm\"\n rounded={true}\n ></ds-tag>\n </span>\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={\n isCollapsed\n ? `Expand ${group.label} group`\n : `Collapse ${group.label} group`\n }\n hasBorder={false}\n onDsClick={event => {\n event.stopPropagation();\n this.emitGroupCollapse(group);\n }}\n />\n </span>\n </th>\n </tr>\n {!isCollapsed && group.rows.map(row => this.renderRow(row, model))}\n </tbody>\n );\n });\n }\n\n private renderSkeletonBody(model: TableRenderModel) {\n const count = Math.min(20, Math.max(1, Math.round(this.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 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 <span class=\"ds-interaction-fill__content\">\n <ds-skeleton variant=\"icon\" iconSize=\"md\" />\n </span>\n {this.renderStickyEdge('start')}\n </td>\n )}\n {this.columns.map(column => (\n <td\n class={{\n 'ds-table__cell': true,\n 'ds-table__cell--text-single': true,\n 'ds-table__skeleton-cell': true,\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 key={`skeleton-${index}:${column.id}`}\n >\n <span class=\"ds-table__cell-content ds-interaction-fill__content\">\n <ds-skeleton variant=\"text\" textVariant=\"text-body-medium\" width=\"100%\" />\n </span>\n {this.renderStickyEdge(column.sticky)}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n );\n }\n\n private renderStateBody(kind: 'empty' | 'error', totalColumns: number) {\n const error = kind === 'error';\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 <ds-empty-state\n icon={error ? 'ErrorTriangle' : 'Inbox'}\n heading={error ? this.errorHeading : this.emptyHeading}\n body={error ? this.errorBody : this.emptyBody}\n />\n </td>\n </tr>\n </tbody>\n );\n }\n\n private renderLazyBody(model: TableRenderModel) {\n if (!this.lazyLoading || !model.hasData) return null;\n const error = this.loadMoreError?.trim();\n const manualFallback = this.loadMoreMode === 'manual' ||\n !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 {error ? (\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\">{error}</ds-text>\n </span>\n <ds-button-unfilled\n label={this.retryLabel}\n size=\"md\"\n onDsClick={() => this.loadController.request('retry')}\n />\n </span>\n ) : this.loadingMore ? (\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 {this.loadingMoreLabel}\n </ds-text>\n </span>\n ) : this.hasMore && manualFallback ? (\n <span class=\"ds-table__load-content\">\n <ds-button-unfilled\n label={this.loadMoreLabel}\n size=\"md\"\n onDsClick={() => this.loadController.request('manual')}\n />\n </span>\n ) : this.hasMore ? (\n <span class=\"ds-table__auto-sentinel\" aria-hidden=\"true\" />\n ) : (\n <ds-text\n class=\"ds-table__load-content\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n >\n {this.endOfResultsLabel}\n </ds-text>\n )}\n </td>\n </tr>\n </tbody>\n );\n }\n\n private renderDocumentStickyHeader(model: TableRenderModel) {\n if (!this.documentStickyHeader) return null;\n return (\n <div\n class=\"ds-table__document-sticky-header\"\n >\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 return formatTableResultSummary(\n this.displayedCount,\n this.totalCount,\n this.resultSummaryLabel,\n );\n }\n\n private renderResultFooter() {\n const summary = this.resultSummary;\n if (!summary) return null;\n return (\n <ds-text\n class=\"ds-table__footer\"\n as=\"div\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n >\n {summary}\n </ds-text>\n );\n }\n\n private renderCaptionBar() {\n if (this.captionVisibility !== 'visible') return null;\n return (\n <ds-text\n class=\"ds-table__caption-bar\"\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 );\n }\n\n render() {\n const model = this.createRenderModel();\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 viewportStyle = this.resolvedMaxHeight\n ? { '--ds-table-max-block-size': this.resolvedMaxHeight }\n : undefined;\n\n return (\n <Host class=\"table-host\">\n <div class={{\n 'ds-table': true,\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 }}>\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.documentStickyHeader && this.renderFloatingCollapseAll(model)}\n <div\n class=\"ds-table__viewport\"\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 }}\n style={model.tableStyle}\n aria-busy={initialLoading || this.loadingMore ? 'true' : undefined}\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(model, !this.documentStickyHeader)}\n {initialLoading\n ? this.renderSkeletonBody(model)\n : initialError\n ? this.renderStateBody('error', model.totalColumns)\n : model.hasData\n ? this.renderDataBodies(model)\n : this.renderStateBody('empty', model.totalColumns)}\n {!initialLoading && !initialError && this.renderLazyBody(model)}\n </table>\n </div>\n </div>\n {this.renderResultFooter()}\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",
845
+ "type": "registry:ui"
846
+ },
847
+ {
848
+ "path": "src/wc/components/Table/table-cell-model.ts",
849
+ "content": "import {\n isTableCellAction,\n isTableCellBlank,\n isTableCellEmpty,\n isTableCellIcon,\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 TableCellTag,\n TableCellTagVariant,\n TableCellValue,\n TableColumn,\n} from './table-types';\n\nexport interface ResolvedTableCellText {\n primary: string | number;\n secondary?: string | number;\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 | { kind: 'image'; cellType: 'image'; value: TableCellImage }\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' | 'primary-pair';\n wraps: boolean;\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 (isTableCellImage(value)) return { kind: 'image', cellType: 'image', value };\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 text: ResolvedTableCellText = isTableCellText(value) || primaryText\n ? value\n : {\n primary: value,\n fontFeature: typeof value === 'number' ? 'tabular-nums' : 'normal',\n };\n const singleLine = !primaryText && (text.secondary === undefined || text.secondary === '');\n return {\n kind: 'text',\n cellType: primaryText ? 'primary-text' : 'text',\n value: text,\n primaryText,\n singleLine,\n variant: primaryText ? 'primary-pair' : singleLine ? 'single' : 'multi',\n wraps: text.wrap ?? column.wrap ?? false,\n };\n}\n",
850
+ "type": "registry:ui"
851
+ },
852
+ {
853
+ "path": "src/wc/components/Table/table-layout-controller.ts",
854
+ "content": "export interface TableLayoutElements {\n viewport: HTMLElement | null;\n table: 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}\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}\n\nexport interface TableLayoutMetricInput {\n viewportInlineSize: number;\n viewportBlockSize: number;\n scrollInlineSize: number;\n scrollBlockSize: number;\n scrollInlineOffset: number;\n tableInlineSize: 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: Math.min(input.viewportInlineSize, input.tableInlineSize),\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 observedTable: HTMLTableElement | null = null;\n private resizeObserver: ResizeObserver | null = null;\n private frame: number | null = null;\n private overflow: TableOverflowState | 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.schedule);\n this.connectedViewport = null;\n this.observedTable = null;\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n }\n\n /** Reconcile newly rendered refs, observers, and pending geometry. */\n refresh(): void {\n if (!this.connected) return;\n const { viewport, table } = this.options.elements();\n const elementsChanged = viewport !== this.connectedViewport || table !== this.observedTable;\n if (elementsChanged) {\n this.connectedViewport?.removeEventListener('scroll', this.schedule);\n this.resizeObserver?.disconnect();\n this.connectedViewport = viewport;\n this.observedTable = table;\n viewport?.addEventListener('scroll', this.schedule, { passive: true });\n\n if (typeof ResizeObserver !== 'undefined' && viewport) {\n this.resizeObserver ??= new ResizeObserver(this.schedule);\n this.resizeObserver.observe(viewport);\n if (table) this.resizeObserver.observe(table);\n }\n }\n // Rendering can change the table's intrinsic width without replacing either\n // element. Resolve that geometry before componentDidRender returns so WebKit\n // cannot paint or expose one frame using the full spanning-cell width.\n 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 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 tableInlineSize: elements.table?.getBoundingClientRect().width ?? viewport.clientWidth,\n collapseHeadBlockStart: measureCollapse\n ? elements.interactiveHead!.getBoundingClientRect().top\n : undefined,\n collapseFrameBlockStart: measureCollapse\n ? elements.frame!.getBoundingClientRect().top\n : undefined,\n });\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",
855
+ "type": "registry:ui"
856
+ },
857
+ {
858
+ "path": "src/wc/components/Table/table-load-controller.ts",
859
+ "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 (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 (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 (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 (!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",
745
860
  "type": "registry:ui"
746
861
  },
747
862
  {
748
863
  "path": "src/wc/components/Table/table-model.ts",
749
- "content": "import type {\n TableCellBlank,\n TableCellAction,\n TableCellEmpty,\n TableCellIcon,\n TableCellImage,\n TableCellPrimaryText,\n TableCellText,\n TableCellTag,\n TableCellValue,\n TableColumn,\n TableGroup,\n TableGroupingState,\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\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 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 (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 nextTableGroupOrder(grouping: TableGroupingState): TableGroupingState {\n return {\n ...grouping,\n direction: grouping.direction === 'asc' ? 'desc' : 'asc',\n };\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 return width == null ? undefined : `${width}px`;\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\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 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 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 }\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",
864
+ "content": "import type {\n TableCellBlank,\n TableCellAction,\n TableCellEmpty,\n TableCellIcon,\n TableCellImage,\n TableCellPrimaryText,\n TableCellText,\n TableCellTag,\n TableCellValue,\n TableColumn,\n TableGroup,\n TableGroupingState,\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\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 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 (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 nextTableGroupOrder(grouping: TableGroupingState): TableGroupingState {\n return {\n ...grouping,\n direction: grouping.direction === 'asc' ? 'desc' : 'asc',\n };\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 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 formatter = new Intl.NumberFormat(locale);\n return label\n .replace('{displayed}', formatter.format(displayed as number))\n .replace('{total}', formatter.format(total as number));\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",
865
+ "type": "registry:ui"
866
+ },
867
+ {
868
+ "path": "src/wc/components/Table/table-render-model.ts",
869
+ "content": "import {\n deriveTableSelectionState,\n isTableGroupIntent,\n resolvedTableGroupCount,\n tableCollapseAllHost,\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 interface TableGroupRenderModel {\n group: TableGroup;\n count: number;\n countLabel: string;\n countIntent: TableGroupIntent;\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}\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\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 count = resolvedTableGroupCount(group);\n const intent = isTableGroupIntent(group.intent) ? group.intent : undefined;\n return {\n group,\n count,\n countLabel: group.countLabel ?? `${count} ${count === 1 ? 'item' : 'items'}`,\n countIntent: intent ?? 'neutral',\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: tableFlexibleColumnId(input.columns),\n tableStyle: explicitMinWidth == null\n ? undefined\n : { '--ds-table-explicit-min-inline-size': explicitMinWidth },\n };\n}\n",
750
870
  "type": "registry:ui"
751
871
  },
752
872
  {
753
873
  "path": "src/wc/components/Table/table-types.ts",
754
- "content": "import type { TagContrast, TagIntent } from '../Tag/Tag';\nimport type { IconColor } from '../Icon/Icon';\n\nexport type TableSortDirection = 'asc' | 'desc';\nexport type TableSelectionMode = 'none' | 'multiple';\nexport type TableCellAlign = 'start' | 'center' | 'end';\nexport type TableCaptionVisibility = 'visible' | 'hidden';\nexport type TableLoadMoreMode = 'auto' | 'manual';\nexport type TableLoadMoreReason = 'auto' | 'manual' | 'retry';\nexport type TableColumnWidth = 'xs' | 'sm' | 'md' | 'lg' | 'xl';\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 plus the supplied group-order direction. */\nexport interface TableGroupingState {\n columnId: string;\n direction: TableSortDirection;\n}\n\n/** Standard two-line cell content owned by the table renderer. */\nexport interface TableCellText {\n primary: string | number;\n secondary?: string;\n /** Allow this cell to wrap even when its column truncates by default. */\n wrap?: boolean;\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 /** Allow this cell to wrap even when its column truncates by default. */\n wrap?: boolean;\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/** 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\ninterface TableCellActionBase {\n kind: 'action';\n /** Stable application-owned action identity emitted with dsCellAction. */\n actionId: string;\n /** Add the ButtonUnfilled resting border. Action cells are unbordered by default. */\n hasBorder?: boolean;\n isInactive?: boolean;\n isLoading?: boolean;\n}\n\n/** Declarative ButtonUnfilled content rendered by the table's standard action cell. */\nexport type TableCellAction = TableCellActionBase & (\n | {\n variant?: 'label';\n label: string;\n icon?: never;\n ariaLabel?: string;\n }\n | {\n variant: 'icon-label';\n label: string;\n icon: string;\n ariaLabel?: string;\n }\n | {\n variant: 'icon';\n label?: never;\n icon: string;\n ariaLabel: string;\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 | TableCellImage\n | TableCellAction\n | TableCellEmpty\n | TableCellBlank;\n\nexport interface TableColumn {\n /** Stable column identity. */\n id: string;\n header: 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 /** 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}\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}\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, display-ready count such as “18 vehicles”. */\n countLabel?: string;\n rows: TableRow[];\n}\n\nexport interface TableSortChangeDetail {\n sort: TableSortState | null;\n}\n\nexport interface TableGroupingChangeDetail {\n grouping: TableGroupingState;\n}\n\nexport interface TableSelectionChangeDetail {\n selectedRowIds: string[];\n scope: 'row' | 'all-loaded';\n changedRowId?: string;\n selected?: boolean;\n}\n\nexport interface TableLoadMoreDetail {\n reason: TableLoadMoreReason;\n loadIdentity: string | number;\n loadedRowCount: number;\n}\n\nexport interface TableCellActionDetail {\n actionId: string;\n rowId: string;\n columnId: string;\n}\n",
874
+ "content": "import type { TagContrast, TagIntent } from '../Tag/Tag';\nimport type { IconColor } from '../Icon/Icon';\n\nexport type TableSortDirection = 'asc' | 'desc';\nexport type TableSelectionMode = 'none' | 'multiple';\nexport type TableCellAlign = 'start' | 'center' | 'end';\nexport type TableColumnSticky = 'start' | 'end';\nexport type TableCaptionVisibility = 'visible' | 'hidden';\nexport type TableLoadMoreMode = 'auto' | 'manual';\nexport type TableLoadMoreReason = 'auto' | 'manual' | 'retry';\nexport type TableColumnWidth = 'xs' | 'sm' | 'md' | 'lg' | 'xl';\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 plus the supplied group-order direction. */\nexport interface TableGroupingState {\n columnId: string;\n direction: TableSortDirection;\n}\n\n/** Standard two-line cell content owned by the table renderer. */\nexport interface TableCellText {\n primary: string | number;\n secondary?: string;\n /** Allow this cell to wrap even when its column truncates by default. */\n wrap?: boolean;\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 /** Allow this cell to wrap even when its column truncates by default. */\n wrap?: boolean;\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/** 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\ninterface TableCellActionBase {\n kind: 'action';\n /** Stable application-owned action identity emitted with dsCellAction. */\n actionId: string;\n /** Add the ButtonUnfilled resting border. Action cells are unbordered by default. */\n hasBorder?: boolean;\n isInactive?: boolean;\n isLoading?: boolean;\n}\n\n/** Declarative ButtonUnfilled content rendered by the table's standard action cell. */\nexport type TableCellAction = TableCellActionBase & (\n | {\n variant?: 'label';\n label: string;\n icon?: never;\n ariaLabel?: string;\n }\n | {\n variant: 'icon-label';\n label: string;\n icon: string;\n ariaLabel?: string;\n }\n | {\n variant: 'icon';\n label?: never;\n icon: string;\n ariaLabel: string;\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 | TableCellImage\n | TableCellAction\n | TableCellEmpty\n | TableCellBlank;\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 /** 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 /** 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 /** 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}\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 accessible count phrase such as “18 vehicles”; the visible tag remains numeric. */\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 rows: TableRow[];\n}\n\nexport interface TableSortChangeDetail {\n sort: TableSortState | null;\n}\n\nexport interface TableGroupingChangeDetail {\n grouping: TableGroupingState;\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 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",
755
875
  "type": "registry:ui"
756
876
  }
757
877
  ]