@ds-mo/ui 13.15.0 → 13.16.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 (104) hide show
  1. package/dist/.package-ready.json +1 -1
  2. package/dist/agent-patterns.json +3 -2
  3. package/dist/agent.json +59 -12
  4. package/dist/angular/ds-bar-title.d.ts +1 -1
  5. package/dist/angular/ds-bar-title.js +3 -3
  6. package/dist/angular/ds-shell-page.d.ts +1 -1
  7. package/dist/angular/ds-shell-page.js +3 -3
  8. package/dist/angular/ds-table.d.ts +1 -1
  9. package/dist/angular/ds-table.js +3 -3
  10. package/dist/components/ds-agent-questionnaire.js +1 -1
  11. package/dist/components/ds-agent-response.js +1 -1
  12. package/dist/components/ds-banner.js +1 -1
  13. package/dist/components/ds-bar-nav.js +1 -1
  14. package/dist/components/ds-bar-title.js +1 -1
  15. package/dist/components/ds-bar-workflow.js +1 -1
  16. package/dist/components/ds-card-overview.js +1 -1
  17. package/dist/components/ds-chart.js +1 -1
  18. package/dist/components/ds-code-block.js +1 -1
  19. package/dist/components/ds-conversation-list.js +1 -1
  20. package/dist/components/ds-markdown.js +1 -1
  21. package/dist/components/ds-menu.js +1 -1
  22. package/dist/components/ds-message-actions.js +1 -1
  23. package/dist/components/ds-message-composer.js +1 -1
  24. package/dist/components/ds-message-scroller.js +1 -1
  25. package/dist/components/ds-mobile-header.js +1 -1
  26. package/dist/components/ds-mobile-section-switcher.js +1 -1
  27. package/dist/components/ds-mobile-sheet-nav.js +1 -1
  28. package/dist/components/ds-modal.js +1 -1
  29. package/dist/components/ds-panel-nav.js +1 -1
  30. package/dist/components/ds-panel-tool-header.js +1 -1
  31. package/dist/components/ds-panel-tool-search.js +1 -1
  32. package/dist/components/ds-panel-tools.js +1 -1
  33. package/dist/components/ds-scroll-overlay.js +1 -1
  34. package/dist/components/ds-select.js +1 -1
  35. package/dist/components/ds-shell-app.js +1 -1
  36. package/dist/components/ds-shell-app.js.map +1 -1
  37. package/dist/components/ds-shell-page.js +1 -1
  38. package/dist/components/ds-shell-tools.js +1 -1
  39. package/dist/components/ds-table.js +1 -1
  40. package/dist/components/ds-table.js.map +1 -1
  41. package/dist/components/ds-toast.js +1 -1
  42. package/dist/components/ds-tooltip.js +1 -1
  43. package/dist/components/{p-DgIRyJoc.js → p-B53Vpuqq.js} +2 -2
  44. package/dist/components/{p-fKwbYrhi.js → p-B94M62Tx.js} +2 -2
  45. package/dist/components/p-BAAjiAIF.js +2 -0
  46. package/dist/components/p-BAAjiAIF.js.map +1 -0
  47. package/dist/components/{p-ClHpIZg0.js → p-BMAZWEDa.js} +2 -2
  48. package/dist/components/p-BMAZWEDa.js.map +1 -0
  49. package/dist/components/p-BMc0DE59.js +2 -0
  50. package/dist/components/p-BMc0DE59.js.map +1 -0
  51. package/dist/components/{p-GSnB-sa7.js → p-BPbE-Kbl.js} +2 -2
  52. package/dist/components/{p-CCkptWPj.js → p-BxR8omPQ.js} +2 -2
  53. package/dist/components/{p-CY19gMiz.js → p-CJq3n5kA.js} +2 -2
  54. package/dist/components/{p-IhToA4m4.js → p-CMbcRN92.js} +2 -2
  55. package/dist/components/{p-QDcUAz9m.js → p-CnIvWEVm.js} +2 -2
  56. package/dist/components/{p-DWjE0VNn.js → p-D5sIwLjA.js} +2 -2
  57. package/dist/components/p-D9CDoQBx.js +2 -0
  58. package/dist/components/{p-BLaqjUWu.js.map → p-D9CDoQBx.js.map} +1 -1
  59. package/dist/components/{p-CekGwihQ.js → p-D9FdOR_m.js} +2 -2
  60. package/dist/components/{p-D5aUkdTw.js → p-Dbobj7DI.js} +2 -2
  61. package/dist/components/{p-DQXzTV8N.js → p-DrvEhJhO.js} +2 -2
  62. package/dist/components/{p-BH_lVRgp.js → p-DyqLVdJp.js} +2 -2
  63. package/dist/components/{p-DWNhyLp2.js → p-m5CSrx_s.js} +2 -2
  64. package/dist/docs/components.json +301 -2
  65. package/dist/lib/components/ShellApp/shell-app-types.d.ts +12 -0
  66. package/dist/lib/utils/index.js +41 -4
  67. package/dist/lib/utils/index.js.map +2 -2
  68. package/dist/lib/utils/resolve-css-length-px.d.ts +1 -1
  69. package/dist/mcp-data/registry/bar-title.json +38 -3
  70. package/dist/mcp-data/registry/registry.json +58 -11
  71. package/dist/mcp-data/registry/shell-app.json +3 -3
  72. package/dist/mcp-data/registry/shell-page.json +77 -4
  73. package/dist/mcp-data/registry/table.json +88 -12
  74. package/dist/styles/table.css +161 -10
  75. package/dist/types/components/BarTitle/BarTitle.d.ts +5 -0
  76. package/dist/types/components/ShellApp/shell-app-types.d.ts +12 -0
  77. package/dist/types/components/ShellPage/ShellPage.d.ts +9 -0
  78. package/dist/types/components/Table/Table.d.ts +30 -0
  79. package/dist/types/components/Table/table-cell-model.d.ts +2 -0
  80. package/dist/types/components/Table/table-layout-controller.d.ts +6 -0
  81. package/dist/types/components/Table/table-types.d.ts +3 -0
  82. package/dist/types/components/Table/table-viewport-fit-controller.d.ts +53 -0
  83. package/dist/types/components.d.ts +102 -0
  84. package/dist/types/utils/resolve-css-length-px.d.ts +1 -1
  85. package/package.json +1 -1
  86. package/dist/components/p-BJMRBHKW.js +0 -2
  87. package/dist/components/p-BJMRBHKW.js.map +0 -1
  88. package/dist/components/p-BLaqjUWu.js +0 -2
  89. package/dist/components/p-ClHpIZg0.js.map +0 -1
  90. package/dist/components/p-DyVAWGL9.js +0 -2
  91. package/dist/components/p-DyVAWGL9.js.map +0 -1
  92. /package/dist/components/{p-DgIRyJoc.js.map → p-B53Vpuqq.js.map} +0 -0
  93. /package/dist/components/{p-fKwbYrhi.js.map → p-B94M62Tx.js.map} +0 -0
  94. /package/dist/components/{p-GSnB-sa7.js.map → p-BPbE-Kbl.js.map} +0 -0
  95. /package/dist/components/{p-CCkptWPj.js.map → p-BxR8omPQ.js.map} +0 -0
  96. /package/dist/components/{p-CY19gMiz.js.map → p-CJq3n5kA.js.map} +0 -0
  97. /package/dist/components/{p-IhToA4m4.js.map → p-CMbcRN92.js.map} +0 -0
  98. /package/dist/components/{p-QDcUAz9m.js.map → p-CnIvWEVm.js.map} +0 -0
  99. /package/dist/components/{p-DWjE0VNn.js.map → p-D5sIwLjA.js.map} +0 -0
  100. /package/dist/components/{p-CekGwihQ.js.map → p-D9FdOR_m.js.map} +0 -0
  101. /package/dist/components/{p-D5aUkdTw.js.map → p-Dbobj7DI.js.map} +0 -0
  102. /package/dist/components/{p-DQXzTV8N.js.map → p-DrvEhJhO.js.map} +0 -0
  103. /package/dist/components/{p-BH_lVRgp.js.map → p-DyqLVdJp.js.map} +0 -0
  104. /package/dist/components/{p-DWNhyLp2.js.map → p-m5CSrx_s.js.map} +0 -0
@@ -34,29 +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. 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.",
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 48px header bar renders as component chrome outside the horizontal scrollport. It defaults to a presentational caption title that does not duplicate the accessible name; application-owned labeled controls supplied through the named header slot replace that fallback. header-leading and header-trailing place actions in the matching compact-bar zones without transferring ownership of the bar's outer or copy padding to the application. Their slot outlets remain mounted so dynamically shown selection and filtering summaries cannot briefly enter the table's outer flow or move its frame.",
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
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
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
41
  "Scrollable table regions become keyboard focusable and named only when overflow exists.",
42
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."
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. The footer slot can replace that copy, while footer-leading and footer-trailing compose actions into the same compact-bar anatomy. Authored footer content can also opt into the 48px footer without controlled counts."
44
44
  ],
45
45
  "states": [
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
+ "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. The optional header and footer slots are composable table-local bars: the component owns their 48px chrome, 8px outer inset, md copy-container padding, balanced text inset, action zones, and sticky placement while the application owns its controls, labels, filtering behavior, and action handling. Content-sized tables remain natural for sparse or fixed-count results. height bounds the complete header, frame, and footer composition. fitViewport reserves the compact-state composition height in the nearest vertical scrollport while growing the visible table into that reservation as surrounding page chrome collapses.",
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. With stickyHeader enabled, each section header scrolls with its group, pins directly below the column header when reached, and yields when the next section arrives. It also stays pinned to the visible horizontal scrollport and spans the smaller of the visible viewport or table width.",
48
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.",
49
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.",
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.",
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, initial error, load-more, retry, loading-more, and terminal bands remain fixed to the visible table width while data columns scroll horizontally beneath them. Incremental loading retains existing rows and supports automatic sentinel, manual load-more, retry, and terminal states.",
51
51
  "Lazy loading replaces pagination. The application owns requests, cancellation, cursors, deduplication, appended records, and query identity; loadIdentity resets the component request guard.",
52
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
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.",
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. Standard secondary copy defaults to the secondary foreground and may declare a semantic Text color when the application data carries intent; dual-primary copy keeps both tracks primary unless explicitly colored. 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.",
55
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."
56
56
  ],
57
57
  "responsiveBehavior": [
58
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.",
59
+ "stickyHeader follows the active vertical scroll owner: a maxHeight, height, or fitViewport table keeps its native header and group sections inside one 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 an unbounded document scrollport edge. When the 48px table header bar is visible, it sticks at that external offset and the column header stacks immediately below it.",
60
+ "fitViewport uses viewportInsetBlockStart and viewportInsetBlockEnd to describe compact surrounding chrome and page gutters relative to the nearest vertical scroll owner. Before the table reaches that compact position, its host reserves the eventual height while the visible composition grows and vertical input remains owned by the outer page. Once settled, the internal table viewport becomes the vertical scroll owner. In both bounded and document-flow modes, native row-group headers remain the semantic source while one table-owned visual sticky lane is clamped to the active row group's ending boundary, so each incoming section pushes the previous section away without duplicate overlap.",
60
61
  "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.",
61
62
  "Explicit column sizes establish the horizontal overflow width; unsized columns share remaining space.",
62
63
  "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.",
@@ -188,6 +189,15 @@
188
189
  "required": false,
189
190
  "mutable": false
190
191
  },
192
+ "fitViewport": {
193
+ "type": "boolean",
194
+ "resolvedType": "boolean",
195
+ "attribute": "fit-viewport",
196
+ "default": "false",
197
+ "required": false,
198
+ "mutable": false,
199
+ "description": "Fit the complete table composition to its nearest vertical scrollport."
200
+ },
191
201
  "grouping": {
192
202
  "type": "TableGroupingState | null",
193
203
  "resolvedType": "TableGroupingState | null",
@@ -212,6 +222,14 @@
212
222
  "required": false,
213
223
  "mutable": false
214
224
  },
225
+ "height": {
226
+ "type": "string | number | undefined",
227
+ "resolvedType": "number | string | undefined",
228
+ "attribute": "height",
229
+ "required": false,
230
+ "mutable": false,
231
+ "description": "Fixed height for the complete header, table frame, and footer composition."
232
+ },
215
233
  "lazyLoading": {
216
234
  "type": "boolean",
217
235
  "resolvedType": "boolean",
@@ -383,6 +401,24 @@
383
401
  "attribute": "total-count",
384
402
  "required": false,
385
403
  "mutable": false
404
+ },
405
+ "viewportInsetBlockEnd": {
406
+ "type": "string | number",
407
+ "resolvedType": "number | string",
408
+ "attribute": "viewport-inset-block-end",
409
+ "default": "0",
410
+ "required": false,
411
+ "mutable": false,
412
+ "description": "Reserved space below a viewport-fitted table."
413
+ },
414
+ "viewportInsetBlockStart": {
415
+ "type": "string | number",
416
+ "resolvedType": "number | string",
417
+ "attribute": "viewport-inset-block-start",
418
+ "default": "0",
419
+ "required": false,
420
+ "mutable": false,
421
+ "description": "Reserved space above a viewport-fitted table once surrounding chrome is compact."
386
422
  }
387
423
  },
388
424
  "events": [
@@ -538,6 +574,15 @@
538
574
  "required": false,
539
575
  "mutable": false
540
576
  },
577
+ "fitViewport": {
578
+ "type": "boolean",
579
+ "resolvedType": "boolean",
580
+ "attribute": "fit-viewport",
581
+ "default": "false",
582
+ "required": false,
583
+ "mutable": false,
584
+ "description": "Fit the complete table composition to its nearest vertical scrollport."
585
+ },
541
586
  "grouping": {
542
587
  "type": "TableGroupingState | null",
543
588
  "resolvedType": "TableGroupingState | null",
@@ -562,6 +607,14 @@
562
607
  "required": false,
563
608
  "mutable": false
564
609
  },
610
+ "height": {
611
+ "type": "string | number | undefined",
612
+ "resolvedType": "number | string | undefined",
613
+ "attribute": "height",
614
+ "required": false,
615
+ "mutable": false,
616
+ "description": "Fixed height for the complete header, table frame, and footer composition."
617
+ },
565
618
  "lazyLoading": {
566
619
  "type": "boolean",
567
620
  "resolvedType": "boolean",
@@ -733,6 +786,24 @@
733
786
  "attribute": "total-count",
734
787
  "required": false,
735
788
  "mutable": false
789
+ },
790
+ "viewportInsetBlockEnd": {
791
+ "type": "string | number",
792
+ "resolvedType": "number | string",
793
+ "attribute": "viewport-inset-block-end",
794
+ "default": "0",
795
+ "required": false,
796
+ "mutable": false,
797
+ "description": "Reserved space below a viewport-fitted table."
798
+ },
799
+ "viewportInsetBlockStart": {
800
+ "type": "string | number",
801
+ "resolvedType": "number | string",
802
+ "attribute": "viewport-inset-block-start",
803
+ "default": "0",
804
+ "required": false,
805
+ "mutable": false,
806
+ "description": "Reserved space above a viewport-fitted table once surrounding chrome is compact."
736
807
  }
737
808
  },
738
809
  "events": [
@@ -836,22 +907,22 @@
836
907
  "files": [
837
908
  {
838
909
  "path": "src/wc/components/Table/Table.css",
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",
910
+ "content": "@import '../../styles/table.css';\n@import '../../styles/control-elevation.css';\n@import '../../utils/control-density.css';\n\n:host {\n display: block;\n box-sizing: border-box;\n min-width: 0;\n max-width: 100%;\n}\n\n:host(.table-host--bounded) {\n min-block-size: 0;\n}\n\n:host(.table-host--fixed-height) {\n block-size: var(--_table-fixed-block-size);\n}\n\n:host(.table-host--viewport-fit) {\n block-size: var(--_table-viewport-fit-reserved-block-size, auto);\n min-block-size: var(--_table-viewport-fit-reserved-block-size, auto);\n}\n\n/*\n * The public table recipe stays at zero specificity for product-owned native\n * markup. Inside the component, these classes sit on ds-text hosts whose reset\n * intentionally owns display, padding, and border with higher specificity.\n * Reassert only those collided properties so component geometry cannot depend\n * on whether ds-text or ds-table happens to hydrate first.\n */\nds-text.ds-table__header-label-box {\n padding-inline: var(--dimension-space-025);\n}\n\nds-text.ds-table__cell-track {\n display: flex;\n}\n\nds-text.ds-table__cell-track--text {\n padding-inline: var(--_table-cell-track-label-inset);\n}\n\nds-text.ds-table__cell-secondary {\n padding-block: var(--dimension-space-025);\n}\n\n.ds-table__cell--primary-text ds-text.ds-table__cell-secondary {\n padding-block: 0;\n}\n\n.ds-table__cell-tag-stack--tag-with-text ds-text.ds-table__cell-tag-text {\n padding-inline: calc(\n var(--_table-cell-track-padding-inline) + var(--_table-cell-track-label-inset)\n );\n padding-block: var(--dimension-space-025);\n}\n\n.ds-table__cell-tag-stack--text-with-tag ds-text.ds-table__cell-tag-text {\n padding-inline: var(--_table-cell-track-label-inset);\n}\n\nds-text.ds-table__load-content {\n display: inline-flex;\n}\n\nds-text.ds-table__bar-text {\n display: inline-flex;\n padding-inline: var(--ds-control-label-inset, var(--dimension-space-025));\n}\n",
840
911
  "type": "registry:ui"
841
912
  },
842
913
  {
843
914
  "path": "src/wc/components/Table/Table.tsx",
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",
915
+ "content": "import {\n Component,\n Element,\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 {\n TableViewportFitController,\n type TableViewportFitMetrics,\n} from './table-viewport-fit-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/focus-ring.css',\n '../../utils/interaction-fill.css',\n 'Table.css',\n ],\n scoped: true,\n})\nexport class Table {\n @Element() el!: HTMLElement;\n /** Stable column definitions. Assign through JavaScript. */\n @Prop() columns: TableColumn[] = [];\n /** Ungrouped row data. Ignored while grouping is active. Assign through JavaScript. */\n @Prop() rows: TableRow[] = [];\n /** One level of application-owned grouped data. Assign through JavaScript. */\n @Prop() groups: TableGroup[] = [];\n /** Controlled grouping column 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 /** Fixed height for the complete header, table frame, and footer composition. */\n @Prop() height: string | number | undefined;\n /** Fit the complete table composition to its nearest vertical scrollport. */\n @Prop() fitViewport: boolean = false;\n /** Reserved space above a viewport-fitted table once surrounding chrome is compact. */\n @Prop() viewportInsetBlockStart: string | number = 0;\n /** Reserved space below a viewport-fitted table. */\n @Prop() viewportInsetBlockEnd: string | number = 0;\n /** Optional explicit label for the horizontal/vertical scroll region. */\n @Prop() scrollLabel: string | undefined;\n\n @Prop() selectionMode: TableSelectionMode = 'none';\n /** Controlled selected row identities. IDs outside the loaded rows are preserved. */\n @Prop() selectedRowIds: string[] = [];\n\n /** Initial loading state. Existing rows stay visible; incremental loading uses loadingMore. */\n @Prop() loading: boolean = false;\n @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 @State() private activeStickyGroupId: string | null = null;\n @State() private viewportFitSettled = false;\n @State() private headerLeadingPresent = false;\n @State() private headerCopyPresent = false;\n @State() private headerTrailingPresent = false;\n\n private rootEl: HTMLElement | null = null;\n private viewportEl: HTMLElement | null = null;\n private frameEl: HTMLElement | null = null;\n private interactiveHeadEl: HTMLTableSectionElement | null = null;\n private collapseAllOverlayEl: HTMLElement | null = null;\n private stickyHeaderTableEl: HTMLTableElement | null = null;\n private tableEl: HTMLTableElement | null = null;\n private sentinelEl: HTMLElement | null = null;\n private previousModelWarning = '';\n private hasLoaded = false;\n private renderedModel: TableRenderModel | null = null;\n private stickyGroupConnected = false;\n private headerSlotObserver: MutationObserver | 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 clampVerticalOverscroll: this.fitViewport && this.viewportFitSettled,\n }),\n verticalEdgeWheel: deltaY => this.viewportFitController.scrollOuterBy(deltaY),\n overflowChanged: state => {\n if (state.start !== this.overflowStart) this.overflowStart = state.start;\n if (state.end !== this.overflowEnd) this.overflowEnd = state.end;\n if (state.scrollable !== this.scrollable) this.scrollable = state.scrollable;\n },\n });\n private readonly loadController = new TableLoadController({\n state: () => ({\n lazyLoading: this.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.containedScroll,\n loadingMoreLabel: this.loadingMoreLabel,\n endOfResultsLabel: this.endOfResultsLabel,\n rowsLoadedLabel: this.rowsLoadedLabel,\n loadedRowCount: this.currentLoadedRowCount(),\n viewport: this.viewportEl,\n sentinel: this.sentinelEl,\n }),\n announce: message => {\n this.announcement = message;\n },\n request: detail => {\n this.dsLoadMore.emit(detail);\n },\n });\n private readonly viewportFitController = new TableViewportFitController({\n enabled: () => this.fitViewport,\n elements: () => ({ host: this.el, surface: this.rootEl }),\n insets: () => ({\n blockStart: this.viewportInsetBlockStart,\n blockEnd: this.viewportInsetBlockEnd,\n }),\n fitChanged: (metrics: TableViewportFitMetrics | null) => {\n const settled = metrics?.settled ?? false;\n if (settled !== this.viewportFitSettled) this.viewportFitSettled = settled;\n },\n });\n\n componentWillLoad(): void {\n this.syncHeaderSlotPresence();\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 this.viewportFitController.connect();\n this.connectStickyGroup();\n this.connectHeaderSlotObserver();\n }\n\n componentDidRender(): void {\n this.layoutController.refresh();\n this.loadController.refresh();\n this.viewportFitController.refresh();\n this.updateStickyGroup();\n }\n\n connectedCallback(): void {\n if (!this.hasLoaded) return;\n this.layoutController.connect();\n this.loadController.connect();\n this.viewportFitController.connect();\n this.connectStickyGroup();\n this.connectHeaderSlotObserver();\n }\n\n disconnectedCallback(): void {\n this.layoutController.disconnect();\n this.loadController.disconnect();\n this.viewportFitController.disconnect();\n this.disconnectStickyGroup();\n this.headerSlotObserver?.disconnect();\n this.headerSlotObserver = null;\n }\n\n private syncHeaderSlotPresence = () => {\n this.headerLeadingPresent = !!this.el.querySelector('[slot=\"header-leading\"]');\n this.headerCopyPresent = !!this.el.querySelector('[slot=\"header\"]');\n this.headerTrailingPresent = !!this.el.querySelector('[slot=\"header-trailing\"]');\n };\n\n private connectHeaderSlotObserver(): void {\n if (this.headerSlotObserver || typeof MutationObserver === 'undefined') return;\n this.headerSlotObserver = new MutationObserver(this.syncHeaderSlotPresence);\n this.headerSlotObserver.observe(this.el, { childList: true });\n this.syncHeaderSlotPresence();\n }\n\n private connectStickyGroup(): void {\n if (this.stickyGroupConnected || typeof window === 'undefined') return;\n this.stickyGroupConnected = true;\n document.addEventListener('scroll', this.updateStickyGroup, {\n capture: true,\n passive: true,\n });\n window.addEventListener('resize', this.updateStickyGroup, { passive: true });\n this.updateStickyGroup();\n }\n\n private disconnectStickyGroup(): void {\n if (!this.stickyGroupConnected || typeof window === 'undefined') return;\n this.stickyGroupConnected = false;\n document.removeEventListener('scroll', this.updateStickyGroup, true);\n window.removeEventListener('resize', this.updateStickyGroup);\n }\n\n private readonly updateStickyGroup = (): void => {\n if (!this.stickyHeader || !this.grouped || !this.frameEl || !this.tableEl) {\n if (this.activeStickyGroupId !== null) this.activeStickyGroupId = null;\n return;\n }\n\n const stickyHeader = this.documentStickyHeader\n ? this.frameEl.querySelector<HTMLElement>('.ds-table__document-sticky-header')\n : this.interactiveHeadEl;\n if (!stickyHeader) return;\n const threshold = stickyHeader.getBoundingClientRect().bottom;\n let activeGroupId: string | null = null;\n let activeGroupTop = threshold;\n for (const body of this.tableEl.querySelectorAll<HTMLElement>('tbody[data-group-id]')) {\n const source = body.querySelector<HTMLElement>('.ds-table__group-cell');\n if (!source) continue;\n const sourceRect = source.getBoundingClientRect();\n const bodyRect = body.getBoundingClientRect();\n if (sourceRect.top <= threshold && bodyRect.bottom > threshold) {\n activeGroupId = body.dataset.groupId ?? null;\n // A sticky section is bounded by its own row group. As that boundary\n // approaches the sticky lane, move the outgoing section bar with it so\n // the incoming source bar pushes it away instead of rendering beneath it.\n activeGroupTop = Math.min(threshold, bodyRect.bottom - sourceRect.height);\n }\n }\n\n const frameRect = this.frameEl.getBoundingClientRect();\n this.frameEl.style.setProperty(\n '--_table-sticky-group-top',\n `${activeGroupTop - frameRect.top}px`,\n );\n\n if (activeGroupId !== this.activeStickyGroupId) {\n this.activeStickyGroupId = activeGroupId;\n return;\n }\n };\n\n @Watch('columns')\n @Watch('grouping')\n 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.containedScroll;\n }\n\n private get fixedHeight(): boolean {\n return !this.fitViewport && this.resolvedHeight !== undefined;\n }\n\n private get boundedComposition(): boolean {\n return this.fitViewport || this.fixedHeight;\n }\n\n private get containedScroll(): boolean {\n return this.boundedComposition || this.resolvedMaxHeight !== undefined;\n }\n\n private 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 get resolvedHeight(): string | undefined {\n if (this.height == null || this.height === '') return undefined;\n return typeof this.height === 'number' ? `${Math.max(0, this.height)}px` : this.height;\n }\n\n private warnModelIssues(): void {\n const issues = tableModelIssues(this.columns, this.rows, this.groups, this.grouped);\n 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-focus-ring ds-interaction-fill__content\"\n type=\"button\"\n role=\"checkbox\"\n aria-label={label}\n aria-checked={indeterminate ? 'mixed' : String(checked)}\n disabled={disabled}\n onClick={onActivate}\n >\n <ds-checkbox\n label=\"\"\n size=\"md\"\n checked={checked}\n indeterminate={indeterminate}\n presentation={true}\n />\n </button>\n );\n }\n\n private renderStickyEdge(sticky: TableColumn['sticky']) {\n if (!sticky) return null;\n return (\n <span\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 ds-focus-ring\"\n type=\"button\"\n aria-label={this.sortButtonLabel(column, segment.sortKey, segment.label)}\n data-sort-control=\"label\"\n data-sort-key={segment.sortKey}\n data-sort-active={segmentActive ? 'true' : undefined}\n onClick={() => this.emitSort(column, segment.sortKey)}\n >\n {label}\n </button>\n ) : (\n <span class=\"ds-table__header-label ds-table__header-static\">{label}</span>\n );\n\n return (\n <span class=\"ds-table__header-segment\" key={segment.sortKey}>\n {control}\n {index < headerSegments.length - 1 && (\n <ds-text\n class=\"ds-table__header-separator\"\n as=\"span\"\n variant=\"text-caption\"\n color=\"tertiary\"\n aria-hidden=\"true\"\n >\n {segment.separator ?? '/'}\n </ds-text>\n )}\n </span>\n );\n })}\n </span>\n );\n const 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={text.secondaryColor ?? (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 'ds-focus-ring': !!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 renderGroupContent(groupModel: TableRenderModel['groups'][number]) {\n const {\n group,\n count,\n countLabel,\n countIntent,\n collapsed: isCollapsed,\n labelColor,\n selection: groupSelection,\n } = groupModel;\n return (\n <span class=\"ds-table__group-content\">\n {groupSelection && (\n <span class=\"ds-table__group-selection\">\n {this.renderSelectionControl(\n groupSelection.allSelected\n ? `Deselect ${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 );\n }\n\n private renderStickyGroup(model: TableRenderModel) {\n if (!this.stickyHeader || !this.activeStickyGroupId) return null;\n const groupModel = model.groups.find(item => item.group.id === this.activeStickyGroupId);\n if (!groupModel) return null;\n return (\n <div\n class={{\n 'ds-table__sticky-group': true,\n 'ds-table__sticky-group--selectable': model.selectable,\n [groupModel.intentClass ?? '']: !!groupModel.intentClass,\n }}\n data-group-id={groupModel.group.id}\n data-group-intent={groupModel.intent}\n >\n {this.renderGroupContent(groupModel)}\n </div>\n );\n }\n\n private renderDataBodies(model: TableRenderModel) {\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 { group, collapsed: isCollapsed, intent, intentClass } = groupModel;\n const stickySourceHidden = this.stickyHeader && this.activeStickyGroupId === group.id;\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 'ds-table__group-cell--sticky-source-hidden': stickySourceHidden,\n [intentClass ?? '']: !!intentClass,\n }}\n aria-hidden={stickySourceHidden ? 'true' : undefined}\n scope=\"rowgroup\"\n colSpan={model.totalColumns}\n >\n {this.renderGroupContent(groupModel)}\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 <div class=\"ds-table__viewport-band ds-table__state-band\">\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 </div>\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 <div class=\"ds-table__viewport-band ds-table__load-band\">\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 </div>\n </td>\n </tr>\n </tbody>\n );\n }\n\n private renderDocumentStickyHeader(model: TableRenderModel) {\n if (!this.documentStickyHeader) return null;\n return (\n <div\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 get hasResultFooter(): boolean {\n return !!this.resultSummary || !!this.el.querySelector(\n '[slot=\"footer-leading\"], [slot=\"footer\"], [slot=\"footer-trailing\"]',\n );\n }\n\n private renderResultFooter() {\n const summary = this.resultSummary;\n const hasLeading = !!this.el.querySelector('[slot=\"footer-leading\"]');\n const hasCopy = !!this.el.querySelector('[slot=\"footer\"]');\n const hasTrailing = !!this.el.querySelector('[slot=\"footer-trailing\"]');\n if (!this.hasResultFooter) return null;\n return (\n <div class=\"ds-table__footer ds-table__bar ds-control--md\">\n {hasLeading && (\n <div class=\"ds-table__bar-leading\">\n <slot name=\"footer-leading\" />\n </div>\n )}\n <div class=\"ds-table__bar-copy\">\n {hasCopy ? (\n <slot name=\"footer\" />\n ) : summary ? (\n <ds-text\n class=\"ds-table__footer-summary ds-table__bar-text\"\n as=\"div\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n >\n {summary}\n </ds-text>\n ) : null}\n </div>\n {hasTrailing && (\n <div class=\"ds-table__bar-trailing\">\n <slot name=\"footer-trailing\" />\n </div>\n )}\n </div>\n );\n }\n\n private renderCaptionBar() {\n if (this.captionVisibility !== 'visible') return null;\n return (\n <div class=\"ds-table__caption-bar ds-table__bar ds-control--md\">\n <div class=\"ds-table__bar-leading\" hidden={!this.headerLeadingPresent}>\n <slot\n name=\"header-leading\"\n onSlotchange={this.syncHeaderSlotPresence}\n />\n </div>\n <div class=\"ds-table__bar-copy\">\n <slot\n name=\"header\"\n onSlotchange={this.syncHeaderSlotPresence}\n />\n {!this.headerCopyPresent ? (\n <ds-text\n class=\"ds-table__caption-title ds-table__bar-text\"\n as=\"div\"\n variant=\"text-title-small\"\n emphasis={true}\n color=\"primary\"\n aria-hidden=\"true\"\n >\n {this.caption}\n </ds-text>\n ) : null}\n </div>\n <div class=\"ds-table__bar-trailing\" hidden={!this.headerTrailingPresent}>\n <slot\n name=\"header-trailing\"\n onSlotchange={this.syncHeaderSlotPresence}\n />\n </div>\n </div>\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.boundedComposition && this.resolvedMaxHeight\n ? { '--ds-table-max-block-size': this.resolvedMaxHeight }\n : undefined;\n const hostStyle = this.fixedHeight\n ? { '--_table-fixed-block-size': this.resolvedHeight }\n : undefined;\n\n return (\n <Host\n class={{\n 'table-host': true,\n 'table-host--bounded': this.boundedComposition,\n 'table-host--fixed-height': this.fixedHeight,\n 'table-host--viewport-fit': this.fitViewport,\n }}\n style={hostStyle}\n >\n <div class={{\n 'ds-table': true,\n 'ds-table--bounded': this.boundedComposition,\n 'ds-table--contained-scroll': this.containedScroll,\n 'ds-table--fixed-height': this.fixedHeight,\n 'ds-table--viewport-fit': this.fitViewport,\n 'ds-table--viewport-fit-pending': this.fitViewport && !this.viewportFitSettled,\n 'ds-table--viewport-fit-settled': this.fitViewport && this.viewportFitSettled,\n 'ds-table--sticky-header': this.stickyHeader,\n 'ds-table--document-sticky-header': this.documentStickyHeader,\n 'ds-table--contained-sticky-header': this.stickyHeader && !this.documentStickyHeader,\n 'ds-table--caption-visible': this.captionVisibility === 'visible',\n 'ds-table--footer-visible': this.hasResultFooter,\n }} ref={element => {\n this.rootEl = element ?? null;\n }}>\n {this.renderCaptionBar()}\n <div\n class={{\n 'ds-table__frame': true,\n 'ds-table__frame--overflow-start': this.overflowStart,\n 'ds-table__frame--overflow-end': this.overflowEnd,\n }}\n ref={element => {\n this.frameEl = element ?? null;\n }}\n >\n {this.renderDocumentStickyHeader(model)}\n {this.renderStickyGroup(model)}\n {!this.documentStickyHeader && this.renderFloatingCollapseAll(model)}\n <div\n class={{\n 'ds-table__viewport': true,\n 'ds-focus-ring': this.scrollable,\n }}\n style={viewportStyle}\n ref={element => {\n this.viewportEl = element ?? null;\n }}\n role={this.scrollable ? 'region' : undefined}\n aria-label={this.scrollable ? regionLabel : undefined}\n tabIndex={this.scrollable ? 0 : undefined}\n >\n <table\n class={{\n 'ds-table__table': true,\n 'ds-table__table--selectable': model.selectable,\n 'ds-table__table--grouped': model.grouped,\n }}\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
916
  "type": "registry:ui"
846
917
  },
847
918
  {
848
919
  "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",
920
+ "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';\nimport type { TextColor } from '../Text/text-types';\n\nexport interface ResolvedTableCellText {\n primary: string | number;\n secondary?: string | number;\n secondaryColor?: TextColor;\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
921
  "type": "registry:ui"
851
922
  },
852
923
  {
853
924
  "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",
925
+ "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 clampVerticalOverscroll: boolean;\n}\n\nexport interface TableOverflowState {\n start: boolean;\n end: boolean;\n scrollable: boolean;\n}\n\nexport interface TableLayoutControllerOptions {\n elements: () => TableLayoutElements;\n mode: () => TableLayoutMode;\n overflowChanged: (state: TableOverflowState) => void;\n verticalEdgeWheel?: (deltaY: number) => boolean;\n}\n\nexport interface TableLayoutMetricInput {\n viewportInlineSize: number;\n viewportBlockSize: number;\n scrollInlineSize: number;\n scrollBlockSize: number;\n scrollInlineOffset: number;\n 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.handleScroll);\n this.connectedViewport?.removeEventListener('wheel', this.handleWheel);\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.handleScroll);\n this.connectedViewport?.removeEventListener('wheel', this.handleWheel);\n this.resizeObserver?.disconnect();\n this.connectedViewport = viewport;\n this.observedTable = table;\n viewport?.addEventListener('scroll', this.handleScroll, { passive: true });\n viewport?.addEventListener('wheel', this.handleWheel, { passive: false });\n\n if (typeof ResizeObserver !== 'undefined' && viewport) {\n this.resizeObserver ??= new ResizeObserver(this.schedule);\n this.resizeObserver.observe(viewport);\n if (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 readonly handleScroll = (): void => {\n this.clampInlineOffset();\n if (this.options.mode().clampVerticalOverscroll) this.clampBlockOffset();\n this.schedule();\n };\n\n private readonly handleWheel = (event: WheelEvent): void => {\n const viewport = this.connectedViewport;\n if (!viewport) return;\n\n if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) {\n const max = Math.max(0, viewport.scrollWidth - viewport.clientWidth);\n const atStart = viewport.scrollLeft <= 0 && event.deltaX < 0;\n const atEnd = viewport.scrollLeft >= max && event.deltaX > 0;\n if (atStart || atEnd) event.preventDefault();\n return;\n }\n\n if (!this.options.mode().clampVerticalOverscroll || event.deltaY === 0) return;\n const max = Math.max(0, viewport.scrollHeight - viewport.clientHeight);\n const atStart = viewport.scrollTop <= 0 && event.deltaY < 0;\n const atEnd = viewport.scrollTop >= max && event.deltaY > 0;\n if (!atStart && !atEnd) return;\n\n this.options.verticalEdgeWheel?.(event.deltaY);\n event.preventDefault();\n };\n\n private clampInlineOffset(): void {\n const viewport = this.connectedViewport;\n if (!viewport) return;\n const max = Math.max(0, viewport.scrollWidth - viewport.clientWidth);\n const clamped = Math.min(max, Math.max(0, viewport.scrollLeft));\n if (viewport.scrollLeft !== clamped) viewport.scrollLeft = clamped;\n }\n\n private clampBlockOffset(): void {\n const viewport = this.connectedViewport;\n if (!viewport) return;\n const max = Math.max(0, viewport.scrollHeight - viewport.clientHeight);\n const clamped = Math.min(max, Math.max(0, viewport.scrollTop));\n if (viewport.scrollTop !== clamped) viewport.scrollTop = clamped;\n }\n\n private sync(): void {\n const elements = this.options.elements();\n const viewport = elements.viewport;\n if (!viewport) return;\n\n // Read every layout value before writing styles or reactive state.\n const mode = this.options.mode();\n const measureCollapse = mode.floatingCollapseAll && !mode.documentStickyHeader &&\n elements.collapseAllOverlay && elements.frame && elements.interactiveHead;\n const metrics = resolveTableLayoutMetrics({\n viewportInlineSize: viewport.clientWidth,\n viewportBlockSize: viewport.clientHeight,\n scrollInlineSize: viewport.scrollWidth,\n scrollBlockSize: viewport.scrollHeight,\n scrollInlineOffset: viewport.scrollLeft,\n 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
926
  "type": "registry:ui"
856
927
  },
857
928
  {
@@ -871,7 +942,12 @@
871
942
  },
872
943
  {
873
944
  "path": "src/wc/components/Table/table-types.ts",
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",
945
+ "content": "import type { TagContrast, TagIntent } from '../Tag/Tag';\nimport type { IconColor } from '../Icon/Icon';\nimport type { TextColor } from '../Text/text-types';\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 /** Optional semantic foreground for secondary copy; defaults to the standard secondary color. */\n secondaryColor?: TextColor;\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",
946
+ "type": "registry:ui"
947
+ },
948
+ {
949
+ "path": "src/wc/components/Table/table-viewport-fit-controller.ts",
950
+ "content": "import { resolveCssLengthPx } from '../../utils/resolve-css-length-px';\n\nexport interface TableViewportFitMetricInput {\n scrollportBlockStart: number;\n scrollportBlockSize: number;\n hostBlockStart: number;\n insetBlockStart: number;\n insetBlockEnd: number;\n}\n\nexport interface TableViewportFitMetrics {\n reservedBlockSize: number;\n currentBlockSize: number;\n settled: boolean;\n}\n\nexport interface TableViewportFitElements {\n host: HTMLElement | null;\n surface: HTMLElement | null;\n}\n\nexport interface TableViewportFitControllerOptions {\n enabled: () => boolean;\n elements: () => TableViewportFitElements;\n insets: () => { blockStart: string | number; blockEnd: string | number };\n fitChanged: (metrics: TableViewportFitMetrics | null) => void;\n}\n\n/** Resolve the reserved compact-state height and the height currently visible. */\nexport function resolveTableViewportFitMetrics(\n input: TableViewportFitMetricInput,\n): TableViewportFitMetrics {\n const insetBlockStart = Math.max(0, input.insetBlockStart);\n const insetBlockEnd = Math.max(0, input.insetBlockEnd);\n const scrollportBlockEnd = input.scrollportBlockStart + input.scrollportBlockSize;\n const settledBlockStart = input.scrollportBlockStart + insetBlockStart;\n const reservedBlockSize = Math.max(\n 0,\n input.scrollportBlockSize - insetBlockStart - insetBlockEnd,\n );\n const currentBlockStart = Math.max(input.hostBlockStart, settledBlockStart);\n const currentBlockSize = Math.min(\n reservedBlockSize,\n Math.max(0, scrollportBlockEnd - insetBlockEnd - currentBlockStart),\n );\n\n return {\n reservedBlockSize,\n currentBlockSize,\n settled: input.hostBlockStart <= settledBlockStart + 0.5,\n };\n}\n\n/**\n * Fits the complete table chrome to its nearest vertical scrollport. The host\n * reserves its compact-state height so the outer page can consume collapsing\n * header travel without changing document height. The visible table surface\n * grows into that reservation until it reaches the supplied sticky inset.\n */\nexport class TableViewportFitController {\n private connected = false;\n private scrollRoot: HTMLElement | Window | null = null;\n private resizeObserver: ResizeObserver | null = null;\n private metrics: TableViewportFitMetrics | null = null;\n\n constructor(private readonly options: TableViewportFitControllerOptions) {}\n\n connect(): void {\n this.connected = true;\n this.refresh();\n }\n\n disconnect(): void {\n this.connected = false;\n this.disconnectRuntime();\n this.clearGeometry();\n }\n\n refresh(): void {\n if (!this.connected) return;\n if (!this.options.enabled()) {\n this.disconnectRuntime();\n this.clearGeometry();\n return;\n }\n\n const host = this.options.elements().host;\n if (!host) return;\n const nextScrollRoot = this.findScrollRoot(host);\n if (nextScrollRoot !== this.scrollRoot) {\n this.disconnectRuntime();\n this.scrollRoot = nextScrollRoot;\n this.scrollRoot.addEventListener('scroll', this.sync, { passive: true });\n window.addEventListener('resize', this.sync, { passive: true });\n if (typeof ResizeObserver !== 'undefined' && this.scrollRoot instanceof HTMLElement) {\n this.resizeObserver = new ResizeObserver(this.sync);\n this.resizeObserver.observe(this.scrollRoot);\n }\n }\n this.sync();\n }\n\n /** Pass a fitted table's boundary wheel delta to its owning page scrollport. */\n scrollOuterBy(delta: number): boolean {\n if (!this.scrollRoot || delta === 0) return false;\n if (this.scrollRoot instanceof HTMLElement) {\n const previous = this.scrollRoot.scrollTop;\n const max = Math.max(0, this.scrollRoot.scrollHeight - this.scrollRoot.clientHeight);\n this.scrollRoot.scrollTop = Math.min(max, Math.max(0, previous + delta));\n return this.scrollRoot.scrollTop !== previous;\n }\n\n const scrollingElement = document.scrollingElement;\n const previous = window.scrollY;\n const max = Math.max(\n 0,\n (scrollingElement?.scrollHeight ?? document.documentElement.scrollHeight) -\n window.innerHeight,\n );\n const next = Math.min(max, Math.max(0, previous + delta));\n if (next !== previous) window.scrollTo({ top: next, behavior: 'auto' });\n return next !== previous;\n }\n\n private disconnectRuntime(): void {\n this.scrollRoot?.removeEventListener('scroll', this.sync);\n window.removeEventListener('resize', this.sync);\n this.scrollRoot = null;\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n }\n\n private clearGeometry(): void {\n const { host, surface } = this.options.elements();\n host?.style.removeProperty('--_table-viewport-fit-reserved-block-size');\n surface?.style.removeProperty('--_table-viewport-fit-current-block-size');\n if (this.metrics !== null) {\n this.metrics = null;\n this.options.fitChanged(null);\n }\n }\n\n private readonly sync = (): void => {\n if (!this.connected || !this.options.enabled() || !this.scrollRoot) return;\n const { host, surface } = this.options.elements();\n if (!host || !surface) return;\n\n const hostRect = host.getBoundingClientRect();\n const scrollportRect = this.scrollRoot instanceof HTMLElement\n ? this.scrollRoot.getBoundingClientRect()\n : { top: 0, height: window.innerHeight };\n const insets = this.options.insets();\n const next = resolveTableViewportFitMetrics({\n scrollportBlockStart: scrollportRect.top,\n scrollportBlockSize: scrollportRect.height,\n hostBlockStart: hostRect.top,\n insetBlockStart: resolveCssLengthPx(insets.blockStart, 0, host),\n insetBlockEnd: resolveCssLengthPx(insets.blockEnd, 0, host),\n });\n\n this.setProperty(\n host,\n '--_table-viewport-fit-reserved-block-size',\n `${next.reservedBlockSize}px`,\n );\n this.setProperty(\n surface,\n '--_table-viewport-fit-current-block-size',\n `${next.currentBlockSize}px`,\n );\n if (\n !this.metrics ||\n this.metrics.reservedBlockSize !== next.reservedBlockSize ||\n this.metrics.currentBlockSize !== next.currentBlockSize ||\n this.metrics.settled !== next.settled\n ) {\n this.metrics = next;\n this.options.fitChanged(next);\n }\n };\n\n private findScrollRoot(host: HTMLElement): HTMLElement | Window {\n let ancestor = this.composedParent(host);\n while (ancestor && ancestor !== document.documentElement) {\n const overflow = getComputedStyle(ancestor).overflowY;\n if (overflow === 'auto' || overflow === 'scroll' || overflow === 'overlay') {\n return ancestor as HTMLElement;\n }\n ancestor = this.composedParent(ancestor);\n }\n return window;\n }\n\n private composedParent(element: Element): Element | null {\n return (element as HTMLElement).assignedSlot?.parentElement ?? element.parentElement;\n }\n\n private setProperty(element: HTMLElement, property: string, value: string): void {\n if (element.style.getPropertyValue(property) !== value) {\n element.style.setProperty(property, value);\n }\n }\n}\n",
875
951
  "type": "registry:ui"
876
952
  }
877
953
  ]