@ds-mo/ui 13.11.1 → 13.13.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 (167) hide show
  1. package/dist/.package-ready.json +1 -1
  2. package/dist/agent-patterns.json +1 -1
  3. package/dist/agent.json +307 -10
  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-mobile-header.d.ts +1 -1
  9. package/dist/angular/ds-mobile-header.js +3 -3
  10. package/dist/angular/ds-tab-group.d.ts +1 -1
  11. package/dist/angular/ds-tab-group.js +3 -3
  12. package/dist/angular/ds-table.d.ts +31 -0
  13. package/dist/angular/ds-table.js +57 -0
  14. package/dist/angular/ds-tag.d.ts +1 -1
  15. package/dist/angular/ds-tag.js +3 -3
  16. package/dist/angular/index.d.ts +1 -1
  17. package/dist/angular/index.js +1 -0
  18. package/dist/angular/proxies.d.ts +1 -0
  19. package/dist/angular/proxies.js +1 -0
  20. package/dist/components/ds-agent-response.js +1 -1
  21. package/dist/components/ds-banner.js +1 -1
  22. package/dist/components/ds-bar-nav.js +1 -1
  23. package/dist/components/ds-bar-title.js +1 -1
  24. package/dist/components/ds-bar-workflow.js +1 -1
  25. package/dist/components/ds-bar-workflow.js.map +1 -1
  26. package/dist/components/ds-button-filled.js +1 -1
  27. package/dist/components/ds-button-unfilled.js +1 -1
  28. package/dist/components/ds-card-chart.js +1 -1
  29. package/dist/components/ds-card-overview.js +1 -1
  30. package/dist/components/ds-card-overview.js.map +1 -1
  31. package/dist/components/ds-card-setting.js +1 -1
  32. package/dist/components/ds-chart-legend.js +1 -1
  33. package/dist/components/ds-chart-legend.js.map +1 -1
  34. package/dist/components/ds-checkbox.js +1 -1
  35. package/dist/components/ds-chip.js +1 -1
  36. package/dist/components/ds-chip.js.map +1 -1
  37. package/dist/components/ds-code-block.js +1 -1
  38. package/dist/components/ds-conversation-list-item.js +1 -1
  39. package/dist/components/ds-conversation-list-item.js.map +1 -1
  40. package/dist/components/ds-input.js +1 -1
  41. package/dist/components/ds-input.js.map +1 -1
  42. package/dist/components/ds-markdown.js +1 -1
  43. package/dist/components/ds-menu.js +1 -1
  44. package/dist/components/ds-message-actions.js +1 -1
  45. package/dist/components/ds-message-composer.js +1 -1
  46. package/dist/components/ds-message-scroller.js +1 -1
  47. package/dist/components/ds-mobile-bar-nav.js +1 -1
  48. package/dist/components/ds-mobile-header.js +1 -1
  49. package/dist/components/ds-mobile-section-switcher.js +1 -1
  50. package/dist/components/ds-mobile-sheet-nav.js +1 -1
  51. package/dist/components/ds-modal.js +1 -1
  52. package/dist/components/ds-panel-nav.js +1 -1
  53. package/dist/components/ds-panel-sub-nav.js +1 -1
  54. package/dist/components/ds-panel-sub-nav.js.map +1 -1
  55. package/dist/components/ds-panel-tool-header.js +1 -1
  56. package/dist/components/ds-panel-tool-search.js +1 -1
  57. package/dist/components/ds-panel-tools.js +1 -1
  58. package/dist/components/ds-radio.js +1 -1
  59. package/dist/components/ds-radio.js.map +1 -1
  60. package/dist/components/ds-scroll-overlay.js +1 -1
  61. package/dist/components/ds-scroll-overlay.js.map +1 -1
  62. package/dist/components/ds-select.js +1 -1
  63. package/dist/components/ds-select.js.map +1 -1
  64. package/dist/components/ds-shell-app.js +1 -1
  65. package/dist/components/ds-shell-app.js.map +1 -1
  66. package/dist/components/ds-shell-tools.js +1 -1
  67. package/dist/components/ds-skeleton.js +1 -1
  68. package/dist/components/ds-slider.js +1 -1
  69. package/dist/components/ds-switch.js +1 -1
  70. package/dist/components/ds-tab-group.js +1 -1
  71. package/dist/components/ds-table.d.ts +11 -0
  72. package/dist/components/ds-table.js +2 -0
  73. package/dist/components/ds-table.js.map +1 -0
  74. package/dist/components/ds-tag.js +1 -1
  75. package/dist/components/ds-tag.js.map +1 -1
  76. package/dist/components/ds-toast.js +1 -1
  77. package/dist/components/ds-typing-indicator.js +1 -1
  78. package/dist/components/index.d.ts +30 -0
  79. package/dist/components/p-B1JB0Zn-.js +2 -0
  80. package/dist/components/{p-DgglLVCj.js.map → p-B1JB0Zn-.js.map} +1 -1
  81. package/dist/components/p-B2t8alvH.js +2 -0
  82. package/dist/components/{p-DK4PGrHm.js.map → p-B2t8alvH.js.map} +1 -1
  83. package/dist/components/p-BAGTb6cZ.js +2 -0
  84. package/dist/components/p-BAGTb6cZ.js.map +1 -0
  85. package/dist/components/p-BEEYhbIb.js +2 -0
  86. package/dist/components/p-BEEYhbIb.js.map +1 -0
  87. package/dist/components/{p-BRajO-Za.js → p-BFMV9TJO.js} +2 -2
  88. package/dist/components/p-BNHPCCw8.js +2 -0
  89. package/dist/components/{p-DCN04sgt.js.map → p-BNHPCCw8.js.map} +1 -1
  90. package/dist/components/{p-DJGdoPMG.js → p-BUZLhvs1.js} +2 -2
  91. package/dist/components/p-BgaF83uh.js +2 -0
  92. package/dist/components/{p-C5gZHZHU.js.map → p-BgaF83uh.js.map} +1 -1
  93. package/dist/components/p-Br0fwWUG.js +2 -0
  94. package/dist/components/{p-BHb3Ws8e.js.map → p-Br0fwWUG.js.map} +1 -1
  95. package/dist/components/{p-DcmqsAMP.js → p-BydCwx7x.js} +2 -2
  96. package/dist/components/p-C3Z01Glr.js +2 -0
  97. package/dist/components/p-C3Z01Glr.js.map +1 -0
  98. package/dist/components/{p-Dg9qiMDa.js → p-C9StMCWg.js} +2 -2
  99. package/dist/components/{p-BX57q1Lo.js → p-CSlTJWFG.js} +2 -2
  100. package/dist/components/{p-BQ7Nzx9A.js → p-Ch5yT3UA.js} +2 -2
  101. package/dist/components/p-Cw1bbiCG.js +2 -0
  102. package/dist/components/p-Cw1bbiCG.js.map +1 -0
  103. package/dist/components/p-CzV4gIxd.js +2 -0
  104. package/dist/components/p-CzV4gIxd.js.map +1 -0
  105. package/dist/components/p-D-F8eyXy.js +2 -0
  106. package/dist/components/p-D-F8eyXy.js.map +1 -0
  107. package/dist/components/p-DGvzGGcu.js +2 -0
  108. package/dist/components/{p-CIlO_j3q.js.map → p-DGvzGGcu.js.map} +1 -1
  109. package/dist/components/{p-DkhE3BsH.js → p-bLCbOPYa.js} +2 -2
  110. package/dist/components/p-cPh32CFB.js +2 -0
  111. package/dist/components/{p-BnGJrBPG.js.map → p-cPh32CFB.js.map} +1 -1
  112. package/dist/docs/components.json +1590 -59
  113. package/dist/lib/components/ButtonFilled/ButtonFilled.d.ts +2 -0
  114. package/dist/lib/components/MobileHeader/mobile-header-types.d.ts +3 -2
  115. package/dist/mcp-data/registry/button-filled.json +20 -1
  116. package/dist/mcp-data/registry/button-unfilled.json +20 -1
  117. package/dist/mcp-data/registry/mobile-header.json +48 -10
  118. package/dist/mcp-data/registry/mobile-sheet-nav.json +2 -2
  119. package/dist/mcp-data/registry/registry.json +309 -9
  120. package/dist/mcp-data/registry/shell-app.json +4 -2
  121. package/dist/mcp-data/registry/shell-tools.json +4 -4
  122. package/dist/mcp-data/registry/tab-group.json +22 -2
  123. package/dist/mcp-data/registry/table.json +777 -0
  124. package/dist/mcp-data/registry/tag.json +22 -3
  125. package/dist/react/components.d.ts +1 -0
  126. package/dist/react/components.js +1 -0
  127. package/dist/react/ds-table.d.ts +17 -0
  128. package/dist/react/ds-table.js +19 -0
  129. package/dist/styles/table.css +874 -0
  130. package/dist/types/components/ButtonFilled/ButtonFilled.d.ts +2 -0
  131. package/dist/types/components/ButtonUnfilled/ButtonUnfilled.d.ts +2 -0
  132. package/dist/types/components/MobileHeader/MobileHeader.d.ts +8 -3
  133. package/dist/types/components/MobileHeader/index.d.ts +1 -1
  134. package/dist/types/components/MobileHeader/mobile-header-types.d.ts +3 -2
  135. package/dist/types/components/ShellApp/ShellApp.d.ts +8 -0
  136. package/dist/types/components/TabGroup/TabGroup.d.ts +3 -0
  137. package/dist/types/components/Table/Table.d.ts +126 -0
  138. package/dist/types/components/Table/index.d.ts +2 -0
  139. package/dist/types/components/Table/table-model.d.ts +29 -0
  140. package/dist/types/components/Table/table-types.d.ts +189 -0
  141. package/dist/types/components/Tag/Tag.d.ts +2 -0
  142. package/dist/types/components.d.ts +417 -6
  143. package/package.json +3 -2
  144. package/dist/components/p-BHb3Ws8e.js +0 -2
  145. package/dist/components/p-BnGJrBPG.js +0 -2
  146. package/dist/components/p-C5gZHZHU.js +0 -2
  147. package/dist/components/p-CIlO_j3q.js +0 -2
  148. package/dist/components/p-CVmtbj-6.js +0 -2
  149. package/dist/components/p-CVmtbj-6.js.map +0 -1
  150. package/dist/components/p-DAbaMtEX.js +0 -2
  151. package/dist/components/p-DAbaMtEX.js.map +0 -1
  152. package/dist/components/p-DCN04sgt.js +0 -2
  153. package/dist/components/p-DK4PGrHm.js +0 -2
  154. package/dist/components/p-DW9p068S.js +0 -2
  155. package/dist/components/p-DW9p068S.js.map +0 -1
  156. package/dist/components/p-DgglLVCj.js +0 -2
  157. package/dist/components/p-Xftomzsv.js +0 -2
  158. package/dist/components/p-Xftomzsv.js.map +0 -1
  159. package/dist/components/p-c1pY-Mqq.js +0 -2
  160. package/dist/components/p-c1pY-Mqq.js.map +0 -1
  161. /package/dist/components/{p-BRajO-Za.js.map → p-BFMV9TJO.js.map} +0 -0
  162. /package/dist/components/{p-DJGdoPMG.js.map → p-BUZLhvs1.js.map} +0 -0
  163. /package/dist/components/{p-DcmqsAMP.js.map → p-BydCwx7x.js.map} +0 -0
  164. /package/dist/components/{p-Dg9qiMDa.js.map → p-C9StMCWg.js.map} +0 -0
  165. /package/dist/components/{p-BX57q1Lo.js.map → p-CSlTJWFG.js.map} +0 -0
  166. /package/dist/components/{p-BQ7Nzx9A.js.map → p-Ch5yT3UA.js.map} +0 -0
  167. /package/dist/components/{p-DkhE3BsH.js.map → p-bLCbOPYa.js.map} +0 -0
@@ -0,0 +1,777 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema/registry-item.json",
3
+ "name": "table",
4
+ "title": "Table",
5
+ "description": "Semantic, controlled data table with durable visual primitives for cell geometry, column geometry, grouping, sorting, multi-selection, sticky overflow, and incremental loading.",
6
+ "type": "registry:ui",
7
+ "meta": {
8
+ "library": "@ds-mo/ui",
9
+ "distribution": "npm",
10
+ "storybook": "https://zainadeel.github.io/compomo/?path=/story/table",
11
+ "source": "src/wc/components/Table/Table.tsx",
12
+ "intentStatus": "complete",
13
+ "intent": {
14
+ "audience": "general",
15
+ "status": "experimental",
16
+ "summary": "Semantic, controlled data table with durable visual primitives for cell geometry, column geometry, grouping, sorting, multi-selection, sticky overflow, and incremental loading.",
17
+ "useWhen": [
18
+ "A structured dataset needs native row and column relationships, optional one-level grouping, or comparable values across records.",
19
+ "An application needs controlled sorting, loaded-row selection, or lazy loading while retaining ownership of the data query and transformations.",
20
+ "A product table needs a token-backed recipe whose individual header, group, row, cell, selection, state, and load-more layers remain easy to restyle."
21
+ ],
22
+ "avoidWhen": [
23
+ "Content is primarily editorial prose; use semantic product-owned markup with the prose recipe instead.",
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, multiple pinned columns per edge, or virtualization; those are outside this first contract.",
26
+ "The application expects the component to fetch, paginate, sort, group, or mutate records internally."
27
+ ],
28
+ "commonlyComposedWith": [
29
+ "component:ds-empty-state",
30
+ "component:ds-skeleton",
31
+ "component:ds-loader",
32
+ "component:ds-button-unfilled",
33
+ "component:ds-tag",
34
+ "component:ds-icon"
35
+ ],
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.",
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.",
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.",
41
+ "Scrollable table regions become keyboard focusable and named only when overflow exists.",
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
+ ],
44
+ "states": [
45
+ "Columns, rows, groups, grouping, sorting, selected row IDs, and lazy-loading state are controlled inputs. Events communicate intent and never mutate those inputs.",
46
+ "Grouping supports one level. The application supplies ordered groups and ordered member rows; group order and member-row sort are independent controls.",
47
+ "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.",
48
+ "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.",
49
+ "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
+ "Lazy loading replaces pagination. The application owns requests, cancellation, cursors, deduplication, appended records, and query identity; loadIdentity resets the component request guard.",
51
+ "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.",
52
+ "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.",
53
+ "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.",
54
+ "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."
55
+ ],
56
+ "responsiveBehavior": [
57
+ "The table keeps native semantics at narrow widths and scrolls horizontally instead of transforming into cards.",
58
+ "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.",
59
+ "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.",
60
+ "Explicit column sizes establish the horizontal overflow width; unsized columns share remaining space.",
61
+ "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.",
62
+ "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.",
63
+ "Long content truncates by default and can opt into wrapping at the column or cell level."
64
+ ],
65
+ "frameworkCaveats": {
66
+ "customElements": [
67
+ "Assign columns, rows, groups, grouping, sort, and selectedRowIds through JavaScript properties rather than HTML attribute strings.",
68
+ "Replace controlled property values in response to dsSortChange, dsGroupingChange, dsSelectionChange, and dsLoadMore; route dsRowActivate in the application owner."
69
+ ],
70
+ "react": [
71
+ "Pass new array and state-object references when the controlled data or query state changes.",
72
+ "Apply emitted sort, grouping, selection, action, or loading detail to application state and perform data transformations in the owning layer."
73
+ ],
74
+ "angular": [
75
+ "Bind complex table inputs as properties and update the controlled values from the corresponding output events.",
76
+ "Keep server cursor, query lifecycle, and cell-action handling in the owning service or container."
77
+ ]
78
+ },
79
+ "references": [
80
+ {
81
+ "label": "Storybook review matrix",
82
+ "path": "src/wc/components/Table/Table.stories.ts"
83
+ },
84
+ {
85
+ "label": "Public visual recipe",
86
+ "path": "src/wc/styles/table.css"
87
+ }
88
+ ]
89
+ },
90
+ "api": {
91
+ "props": {
92
+ "caption": {
93
+ "type": "string",
94
+ "resolvedType": "string",
95
+ "attribute": "caption",
96
+ "required": true,
97
+ "mutable": false,
98
+ "description": "Required accessible table name, rendered as a native caption."
99
+ },
100
+ "captionVisibility": {
101
+ "type": "TableCaptionVisibility",
102
+ "resolvedType": "\"hidden\" | \"visible\"",
103
+ "attribute": "caption-visibility",
104
+ "default": "'hidden'",
105
+ "required": false,
106
+ "mutable": false
107
+ },
108
+ "columns": {
109
+ "type": "TableColumn[]",
110
+ "resolvedType": "TableColumn[]",
111
+ "default": "[]",
112
+ "required": false,
113
+ "mutable": false,
114
+ "description": "Stable column definitions. Assign through JavaScript."
115
+ },
116
+ "emptyBody": {
117
+ "type": "string",
118
+ "resolvedType": "string",
119
+ "attribute": "empty-body",
120
+ "default": "'No data is available.'",
121
+ "required": false,
122
+ "mutable": false
123
+ },
124
+ "emptyCellLabel": {
125
+ "type": "string",
126
+ "resolvedType": "string",
127
+ "attribute": "empty-cell-label",
128
+ "default": "'Not available'",
129
+ "required": false,
130
+ "mutable": false
131
+ },
132
+ "emptyHeading": {
133
+ "type": "string",
134
+ "resolvedType": "string",
135
+ "attribute": "empty-heading",
136
+ "default": "'No results'",
137
+ "required": false,
138
+ "mutable": false
139
+ },
140
+ "endOfResultsLabel": {
141
+ "type": "string",
142
+ "resolvedType": "string",
143
+ "attribute": "end-of-results-label",
144
+ "default": "'All results loaded'",
145
+ "required": false,
146
+ "mutable": false
147
+ },
148
+ "error": {
149
+ "type": "boolean",
150
+ "resolvedType": "boolean",
151
+ "attribute": "error",
152
+ "default": "false",
153
+ "required": false,
154
+ "mutable": false,
155
+ "description": "Initial error state. Existing rows stay visible; incremental failures use loadMoreError."
156
+ },
157
+ "errorBody": {
158
+ "type": "string",
159
+ "resolvedType": "string",
160
+ "attribute": "error-body",
161
+ "default": "'The data could not be loaded.'",
162
+ "required": false,
163
+ "mutable": false
164
+ },
165
+ "errorHeading": {
166
+ "type": "string",
167
+ "resolvedType": "string",
168
+ "attribute": "error-heading",
169
+ "default": "'Unable to load data'",
170
+ "required": false,
171
+ "mutable": false
172
+ },
173
+ "grouping": {
174
+ "type": "TableGroupingState | null",
175
+ "resolvedType": "TableGroupingState | null",
176
+ "default": "null",
177
+ "required": false,
178
+ "mutable": false,
179
+ "description": "Controlled grouping column and group-order direction."
180
+ },
181
+ "groups": {
182
+ "type": "TableGroup[]",
183
+ "resolvedType": "TableGroup[]",
184
+ "default": "[]",
185
+ "required": false,
186
+ "mutable": false,
187
+ "description": "One level of application-owned grouped data. Assign through JavaScript."
188
+ },
189
+ "hasMore": {
190
+ "type": "boolean",
191
+ "resolvedType": "boolean",
192
+ "attribute": "has-more",
193
+ "default": "false",
194
+ "required": false,
195
+ "mutable": false
196
+ },
197
+ "lazyLoading": {
198
+ "type": "boolean",
199
+ "resolvedType": "boolean",
200
+ "attribute": "lazy-loading",
201
+ "default": "false",
202
+ "required": false,
203
+ "mutable": false,
204
+ "description": "Enable application-owned incremental loading without pagination."
205
+ },
206
+ "loadIdentity": {
207
+ "type": "string | number",
208
+ "resolvedType": "number | string",
209
+ "attribute": "load-identity",
210
+ "default": "'default'",
211
+ "required": false,
212
+ "mutable": false,
213
+ "description": "Reset key for a new query/group/sort dataset."
214
+ },
215
+ "loadMoreError": {
216
+ "type": "string | undefined",
217
+ "resolvedType": "string | undefined",
218
+ "attribute": "load-more-error",
219
+ "required": false,
220
+ "mutable": false
221
+ },
222
+ "loadMoreLabel": {
223
+ "type": "string",
224
+ "resolvedType": "string",
225
+ "attribute": "load-more-label",
226
+ "default": "'Load more'",
227
+ "required": false,
228
+ "mutable": false
229
+ },
230
+ "loadMoreMode": {
231
+ "type": "TableLoadMoreMode",
232
+ "resolvedType": "\"auto\" | \"manual\"",
233
+ "attribute": "load-more-mode",
234
+ "default": "'auto'",
235
+ "required": false,
236
+ "mutable": false
237
+ },
238
+ "loadMoreThreshold": {
239
+ "type": "number",
240
+ "resolvedType": "number",
241
+ "attribute": "load-more-threshold",
242
+ "default": "0",
243
+ "required": false,
244
+ "mutable": false,
245
+ "description": "IntersectionObserver root margin in CSS pixels."
246
+ },
247
+ "loading": {
248
+ "type": "boolean",
249
+ "resolvedType": "boolean",
250
+ "attribute": "loading",
251
+ "default": "false",
252
+ "required": false,
253
+ "mutable": false,
254
+ "description": "Initial loading state. Existing rows stay visible; incremental loading uses loadingMore."
255
+ },
256
+ "loadingMore": {
257
+ "type": "boolean",
258
+ "resolvedType": "boolean",
259
+ "attribute": "loading-more",
260
+ "default": "false",
261
+ "required": false,
262
+ "mutable": false
263
+ },
264
+ "loadingMoreLabel": {
265
+ "type": "string",
266
+ "resolvedType": "string",
267
+ "attribute": "loading-more-label",
268
+ "default": "'Loading more results'",
269
+ "required": false,
270
+ "mutable": false
271
+ },
272
+ "maxHeight": {
273
+ "type": "string | number | undefined",
274
+ "resolvedType": "number | string | undefined",
275
+ "attribute": "max-height",
276
+ "required": false,
277
+ "mutable": false,
278
+ "description": "Maximum scroll-region height. Numbers resolve to CSS pixels."
279
+ },
280
+ "retryLabel": {
281
+ "type": "string",
282
+ "resolvedType": "string",
283
+ "attribute": "retry-label",
284
+ "default": "'Retry'",
285
+ "required": false,
286
+ "mutable": false
287
+ },
288
+ "rows": {
289
+ "type": "TableRow[]",
290
+ "resolvedType": "TableRow[]",
291
+ "default": "[]",
292
+ "required": false,
293
+ "mutable": false,
294
+ "description": "Ungrouped row data. Ignored while grouping is active. Assign through JavaScript."
295
+ },
296
+ "rowsLoadedLabel": {
297
+ "type": "string",
298
+ "resolvedType": "string",
299
+ "attribute": "rows-loaded-label",
300
+ "default": "'{count} more rows loaded. {total} rows loaded.'",
301
+ "required": false,
302
+ "mutable": false,
303
+ "description": "Supports {count} and {total} placeholders."
304
+ },
305
+ "scrollLabel": {
306
+ "type": "string | undefined",
307
+ "resolvedType": "string | undefined",
308
+ "attribute": "scroll-label",
309
+ "required": false,
310
+ "mutable": false,
311
+ "description": "Optional explicit label for the horizontal/vertical scroll region."
312
+ },
313
+ "selectedRowIds": {
314
+ "type": "string[]",
315
+ "resolvedType": "string[]",
316
+ "default": "[]",
317
+ "required": false,
318
+ "mutable": false,
319
+ "description": "Controlled selected row identities. IDs outside the loaded rows are preserved."
320
+ },
321
+ "selectionMode": {
322
+ "type": "TableSelectionMode",
323
+ "resolvedType": "\"multiple\" | \"none\"",
324
+ "attribute": "selection-mode",
325
+ "default": "'none'",
326
+ "required": false,
327
+ "mutable": false
328
+ },
329
+ "skeletonRows": {
330
+ "type": "number",
331
+ "resolvedType": "number",
332
+ "attribute": "skeleton-rows",
333
+ "default": "5",
334
+ "required": false,
335
+ "mutable": false
336
+ },
337
+ "sort": {
338
+ "type": "TableSortState | null",
339
+ "resolvedType": "TableSortState | null",
340
+ "default": "null",
341
+ "required": false,
342
+ "mutable": false,
343
+ "description": "Controlled member-row sort state."
344
+ },
345
+ "stickyHeader": {
346
+ "type": "boolean",
347
+ "resolvedType": "boolean",
348
+ "attribute": "sticky-header",
349
+ "default": "false",
350
+ "required": false,
351
+ "mutable": false
352
+ }
353
+ },
354
+ "events": [
355
+ {
356
+ "name": "dsCellAction",
357
+ "detail": "TableCellActionDetail",
358
+ "bubbles": true,
359
+ "cancelable": true,
360
+ "composed": true
361
+ },
362
+ {
363
+ "name": "dsGroupingChange",
364
+ "detail": "TableGroupingChangeDetail",
365
+ "bubbles": true,
366
+ "cancelable": true,
367
+ "composed": true
368
+ },
369
+ {
370
+ "name": "dsLoadMore",
371
+ "detail": "TableLoadMoreDetail",
372
+ "bubbles": true,
373
+ "cancelable": true,
374
+ "composed": true
375
+ },
376
+ {
377
+ "name": "dsRowActivate",
378
+ "detail": "TableRowActivateDetail",
379
+ "bubbles": true,
380
+ "cancelable": true,
381
+ "composed": true
382
+ },
383
+ {
384
+ "name": "dsSelectionChange",
385
+ "detail": "TableSelectionChangeDetail",
386
+ "bubbles": true,
387
+ "cancelable": true,
388
+ "composed": true
389
+ },
390
+ {
391
+ "name": "dsSortChange",
392
+ "detail": "TableSortChangeDetail",
393
+ "bubbles": true,
394
+ "cancelable": true,
395
+ "composed": true
396
+ }
397
+ ],
398
+ "methods": [],
399
+ "slots": []
400
+ },
401
+ "props": {
402
+ "caption": {
403
+ "type": "string",
404
+ "resolvedType": "string",
405
+ "attribute": "caption",
406
+ "required": true,
407
+ "mutable": false,
408
+ "description": "Required accessible table name, rendered as a native caption."
409
+ },
410
+ "captionVisibility": {
411
+ "type": "TableCaptionVisibility",
412
+ "resolvedType": "\"hidden\" | \"visible\"",
413
+ "attribute": "caption-visibility",
414
+ "default": "'hidden'",
415
+ "required": false,
416
+ "mutable": false
417
+ },
418
+ "columns": {
419
+ "type": "TableColumn[]",
420
+ "resolvedType": "TableColumn[]",
421
+ "default": "[]",
422
+ "required": false,
423
+ "mutable": false,
424
+ "description": "Stable column definitions. Assign through JavaScript."
425
+ },
426
+ "emptyBody": {
427
+ "type": "string",
428
+ "resolvedType": "string",
429
+ "attribute": "empty-body",
430
+ "default": "'No data is available.'",
431
+ "required": false,
432
+ "mutable": false
433
+ },
434
+ "emptyCellLabel": {
435
+ "type": "string",
436
+ "resolvedType": "string",
437
+ "attribute": "empty-cell-label",
438
+ "default": "'Not available'",
439
+ "required": false,
440
+ "mutable": false
441
+ },
442
+ "emptyHeading": {
443
+ "type": "string",
444
+ "resolvedType": "string",
445
+ "attribute": "empty-heading",
446
+ "default": "'No results'",
447
+ "required": false,
448
+ "mutable": false
449
+ },
450
+ "endOfResultsLabel": {
451
+ "type": "string",
452
+ "resolvedType": "string",
453
+ "attribute": "end-of-results-label",
454
+ "default": "'All results loaded'",
455
+ "required": false,
456
+ "mutable": false
457
+ },
458
+ "error": {
459
+ "type": "boolean",
460
+ "resolvedType": "boolean",
461
+ "attribute": "error",
462
+ "default": "false",
463
+ "required": false,
464
+ "mutable": false,
465
+ "description": "Initial error state. Existing rows stay visible; incremental failures use loadMoreError."
466
+ },
467
+ "errorBody": {
468
+ "type": "string",
469
+ "resolvedType": "string",
470
+ "attribute": "error-body",
471
+ "default": "'The data could not be loaded.'",
472
+ "required": false,
473
+ "mutable": false
474
+ },
475
+ "errorHeading": {
476
+ "type": "string",
477
+ "resolvedType": "string",
478
+ "attribute": "error-heading",
479
+ "default": "'Unable to load data'",
480
+ "required": false,
481
+ "mutable": false
482
+ },
483
+ "grouping": {
484
+ "type": "TableGroupingState | null",
485
+ "resolvedType": "TableGroupingState | null",
486
+ "default": "null",
487
+ "required": false,
488
+ "mutable": false,
489
+ "description": "Controlled grouping column and group-order direction."
490
+ },
491
+ "groups": {
492
+ "type": "TableGroup[]",
493
+ "resolvedType": "TableGroup[]",
494
+ "default": "[]",
495
+ "required": false,
496
+ "mutable": false,
497
+ "description": "One level of application-owned grouped data. Assign through JavaScript."
498
+ },
499
+ "hasMore": {
500
+ "type": "boolean",
501
+ "resolvedType": "boolean",
502
+ "attribute": "has-more",
503
+ "default": "false",
504
+ "required": false,
505
+ "mutable": false
506
+ },
507
+ "lazyLoading": {
508
+ "type": "boolean",
509
+ "resolvedType": "boolean",
510
+ "attribute": "lazy-loading",
511
+ "default": "false",
512
+ "required": false,
513
+ "mutable": false,
514
+ "description": "Enable application-owned incremental loading without pagination."
515
+ },
516
+ "loadIdentity": {
517
+ "type": "string | number",
518
+ "resolvedType": "number | string",
519
+ "attribute": "load-identity",
520
+ "default": "'default'",
521
+ "required": false,
522
+ "mutable": false,
523
+ "description": "Reset key for a new query/group/sort dataset."
524
+ },
525
+ "loadMoreError": {
526
+ "type": "string | undefined",
527
+ "resolvedType": "string | undefined",
528
+ "attribute": "load-more-error",
529
+ "required": false,
530
+ "mutable": false
531
+ },
532
+ "loadMoreLabel": {
533
+ "type": "string",
534
+ "resolvedType": "string",
535
+ "attribute": "load-more-label",
536
+ "default": "'Load more'",
537
+ "required": false,
538
+ "mutable": false
539
+ },
540
+ "loadMoreMode": {
541
+ "type": "TableLoadMoreMode",
542
+ "resolvedType": "\"auto\" | \"manual\"",
543
+ "attribute": "load-more-mode",
544
+ "default": "'auto'",
545
+ "required": false,
546
+ "mutable": false
547
+ },
548
+ "loadMoreThreshold": {
549
+ "type": "number",
550
+ "resolvedType": "number",
551
+ "attribute": "load-more-threshold",
552
+ "default": "0",
553
+ "required": false,
554
+ "mutable": false,
555
+ "description": "IntersectionObserver root margin in CSS pixels."
556
+ },
557
+ "loading": {
558
+ "type": "boolean",
559
+ "resolvedType": "boolean",
560
+ "attribute": "loading",
561
+ "default": "false",
562
+ "required": false,
563
+ "mutable": false,
564
+ "description": "Initial loading state. Existing rows stay visible; incremental loading uses loadingMore."
565
+ },
566
+ "loadingMore": {
567
+ "type": "boolean",
568
+ "resolvedType": "boolean",
569
+ "attribute": "loading-more",
570
+ "default": "false",
571
+ "required": false,
572
+ "mutable": false
573
+ },
574
+ "loadingMoreLabel": {
575
+ "type": "string",
576
+ "resolvedType": "string",
577
+ "attribute": "loading-more-label",
578
+ "default": "'Loading more results'",
579
+ "required": false,
580
+ "mutable": false
581
+ },
582
+ "maxHeight": {
583
+ "type": "string | number | undefined",
584
+ "resolvedType": "number | string | undefined",
585
+ "attribute": "max-height",
586
+ "required": false,
587
+ "mutable": false,
588
+ "description": "Maximum scroll-region height. Numbers resolve to CSS pixels."
589
+ },
590
+ "retryLabel": {
591
+ "type": "string",
592
+ "resolvedType": "string",
593
+ "attribute": "retry-label",
594
+ "default": "'Retry'",
595
+ "required": false,
596
+ "mutable": false
597
+ },
598
+ "rows": {
599
+ "type": "TableRow[]",
600
+ "resolvedType": "TableRow[]",
601
+ "default": "[]",
602
+ "required": false,
603
+ "mutable": false,
604
+ "description": "Ungrouped row data. Ignored while grouping is active. Assign through JavaScript."
605
+ },
606
+ "rowsLoadedLabel": {
607
+ "type": "string",
608
+ "resolvedType": "string",
609
+ "attribute": "rows-loaded-label",
610
+ "default": "'{count} more rows loaded. {total} rows loaded.'",
611
+ "required": false,
612
+ "mutable": false,
613
+ "description": "Supports {count} and {total} placeholders."
614
+ },
615
+ "scrollLabel": {
616
+ "type": "string | undefined",
617
+ "resolvedType": "string | undefined",
618
+ "attribute": "scroll-label",
619
+ "required": false,
620
+ "mutable": false,
621
+ "description": "Optional explicit label for the horizontal/vertical scroll region."
622
+ },
623
+ "selectedRowIds": {
624
+ "type": "string[]",
625
+ "resolvedType": "string[]",
626
+ "default": "[]",
627
+ "required": false,
628
+ "mutable": false,
629
+ "description": "Controlled selected row identities. IDs outside the loaded rows are preserved."
630
+ },
631
+ "selectionMode": {
632
+ "type": "TableSelectionMode",
633
+ "resolvedType": "\"multiple\" | \"none\"",
634
+ "attribute": "selection-mode",
635
+ "default": "'none'",
636
+ "required": false,
637
+ "mutable": false
638
+ },
639
+ "skeletonRows": {
640
+ "type": "number",
641
+ "resolvedType": "number",
642
+ "attribute": "skeleton-rows",
643
+ "default": "5",
644
+ "required": false,
645
+ "mutable": false
646
+ },
647
+ "sort": {
648
+ "type": "TableSortState | null",
649
+ "resolvedType": "TableSortState | null",
650
+ "default": "null",
651
+ "required": false,
652
+ "mutable": false,
653
+ "description": "Controlled member-row sort state."
654
+ },
655
+ "stickyHeader": {
656
+ "type": "boolean",
657
+ "resolvedType": "boolean",
658
+ "attribute": "sticky-header",
659
+ "default": "false",
660
+ "required": false,
661
+ "mutable": false
662
+ }
663
+ },
664
+ "events": [
665
+ {
666
+ "name": "dsCellAction",
667
+ "detail": "TableCellActionDetail",
668
+ "bubbles": true,
669
+ "cancelable": true,
670
+ "composed": true
671
+ },
672
+ {
673
+ "name": "dsGroupingChange",
674
+ "detail": "TableGroupingChangeDetail",
675
+ "bubbles": true,
676
+ "cancelable": true,
677
+ "composed": true
678
+ },
679
+ {
680
+ "name": "dsLoadMore",
681
+ "detail": "TableLoadMoreDetail",
682
+ "bubbles": true,
683
+ "cancelable": true,
684
+ "composed": true
685
+ },
686
+ {
687
+ "name": "dsRowActivate",
688
+ "detail": "TableRowActivateDetail",
689
+ "bubbles": true,
690
+ "cancelable": true,
691
+ "composed": true
692
+ },
693
+ {
694
+ "name": "dsSelectionChange",
695
+ "detail": "TableSelectionChangeDetail",
696
+ "bubbles": true,
697
+ "cancelable": true,
698
+ "composed": true
699
+ },
700
+ {
701
+ "name": "dsSortChange",
702
+ "detail": "TableSortChangeDetail",
703
+ "bubbles": true,
704
+ "cancelable": true,
705
+ "composed": true
706
+ }
707
+ ],
708
+ "methods": [],
709
+ "slots": [],
710
+ "exports": {
711
+ "customElement": "ds-table",
712
+ "react": "DsTable",
713
+ "angular": "DsTable"
714
+ },
715
+ "consumption": {
716
+ "install": "npm install @ds-mo/ui @ds-mo/tokens @ds-mo/icons",
717
+ "cssSetup": "import '@ds-mo/tokens';\nimport '@ds-mo/tokens/reset';\nimport '@ds-mo/tokens/globals';",
718
+ "customElements": {
719
+ "import": "import '@ds-mo/ui/dist/components/ds-table.js';",
720
+ "example": "<ds-table></ds-table>"
721
+ },
722
+ "react": {
723
+ "import": "import { DsTable } from '@ds-mo/ui/react';",
724
+ "example": "<DsTable />"
725
+ },
726
+ "angular": {
727
+ "import": "import { DsTable } from '@ds-mo/ui/angular/ds-table';",
728
+ "example": "<ds-table></ds-table>"
729
+ },
730
+ "complexPropertyNote": "Assign these non-primitive values as JavaScript properties: columns, grouping, groups, rows, selectedRowIds, sort.",
731
+ "peerDependencies": {
732
+ "required": [
733
+ "@ds-mo/tokens ^6.0.0",
734
+ "@ds-mo/icons ^6.5.0"
735
+ ],
736
+ "frameworks": "Custom Elements; React 18/19 wrappers; Angular 19-22 standalone adapters."
737
+ }
738
+ }
739
+ },
740
+ "dependencies": [
741
+ "@ds-mo/ui",
742
+ "@ds-mo/tokens",
743
+ "@ds-mo/icons"
744
+ ],
745
+ "registryDependencies": [
746
+ "button-unfilled",
747
+ "checkbox",
748
+ "empty-state",
749
+ "icon",
750
+ "loader",
751
+ "skeleton",
752
+ "tag",
753
+ "text"
754
+ ],
755
+ "files": [
756
+ {
757
+ "path": "src/wc/components/Table/Table.css",
758
+ "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",
759
+ "type": "registry:ui"
760
+ },
761
+ {
762
+ "path": "src/wc/components/Table/Table.tsx",
763
+ "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 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\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 @Event() dsRowActivate!: EventEmitter<TableRowActivateDetail>;\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 stickyHeaderTableEl: HTMLTableElement | 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.syncStickyHeaderPosition();\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 documentStickyHeader(): boolean {\n return this.stickyHeader && this.resolvedMaxHeight === undefined;\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 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 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 this.syncStickyHeaderPosition();\n };\n\n private syncStickyHeaderPosition(): void {\n if (!this.viewportEl || !this.stickyHeaderTableEl) return;\n const offset = this.viewportEl.scrollLeft;\n const maxOffset = Math.max(0, this.viewportEl.scrollWidth - this.viewportEl.clientWidth);\n this.stickyHeaderTableEl.style.setProperty('--ds-table-inline-scroll-offset', `${offset}px`);\n this.stickyHeaderTableEl.style.setProperty('--ds-table-inline-scroll-max-offset', `${maxOffset}px`);\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.resolvedMaxHeight === undefined ? null : 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 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 renderColumnHeader(\n column: TableColumn,\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\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 }}\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 <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 {this.renderStickyEdge(column.sticky)}\n </th>\n );\n }\n\n private renderColgroup() {\n return (\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 );\n }\n\n private renderHeader(interactive = true, presentational = false) {\n const selection = this.selectionState;\n return (\n <thead class={{\n 'ds-table__head': true,\n 'ds-table__head--semantic-copy': !interactive,\n }}>\n <tr class=\"ds-table__header-row\">\n {this.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, interactive, presentational)\n ))}\n </tr>\n </thead>\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=\"md\"\n isInset={true}\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 '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 {this.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 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 '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={cellType}\n data-cell-variant={tagVariant ?? textVariant}\n >\n <span class=\"ds-table__cell-content ds-interaction-fill__content\">\n {this.renderCellValue(value, column, row)}\n </span>\n {this.renderStickyEdge(column.sticky)}\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 ds-table__cell--sticky-start\">\n <ds-skeleton variant=\"icon\" iconSize=\"md\" />\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--sticky-start': column.sticky === 'start',\n 'ds-table__cell--sticky-end': column.sticky === 'end',\n }}\n key={`skeleton-${index}:${column.id}`}\n >\n <span class=\"ds-table__cell-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') {\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 private renderDocumentStickyHeader() {\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': this.selectable,\n }}\n style={this.tableStyle}\n role=\"presentation\"\n ref={element => {\n this.stickyHeaderTableEl = element ?? null;\n }}\n >\n {this.renderColgroup()}\n {this.renderHeader(true, true)}\n </table>\n </div>\n );\n }\n\n render() {\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 'ds-table--document-sticky-header': this.documentStickyHeader,\n 'ds-table--contained-sticky-header': this.stickyHeader && !this.documentStickyHeader,\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 {this.renderDocumentStickyHeader()}\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 {this.renderColgroup()}\n {this.renderHeader(!this.documentStickyHeader)}\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",
764
+ "type": "registry:ui"
765
+ },
766
+ {
767
+ "path": "src/wc/components/Table/table-model.ts",
768
+ "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 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 }\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",
769
+ "type": "registry:ui"
770
+ },
771
+ {
772
+ "path": "src/wc/components/Table/table-types.ts",
773
+ "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\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\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, 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\nexport interface TableRowActivateDetail {\n rowId: string;\n}\n",
774
+ "type": "registry:ui"
775
+ }
776
+ ]
777
+ }