@ds-mo/ui 13.12.0 → 13.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/dist/.package-ready.json +1 -1
  2. package/dist/agent-patterns.json +1 -1
  3. package/dist/agent.json +22 -3
  4. package/dist/angular/ds-button-filled.d.ts +1 -1
  5. package/dist/angular/ds-button-filled.js +3 -3
  6. package/dist/angular/ds-button-unfilled.d.ts +1 -1
  7. package/dist/angular/ds-button-unfilled.js +3 -3
  8. package/dist/angular/ds-table.d.ts +4 -1
  9. package/dist/angular/ds-table.js +5 -2
  10. package/dist/components/ds-agent-response.js +1 -1
  11. package/dist/components/ds-banner.js +1 -1
  12. package/dist/components/ds-bar-nav.js +1 -1
  13. package/dist/components/ds-bar-title.js +1 -1
  14. package/dist/components/ds-bar-workflow.js +1 -1
  15. package/dist/components/ds-button-filled.js +1 -1
  16. package/dist/components/ds-button-unfilled.js +1 -1
  17. package/dist/components/ds-card-chart.js +1 -1
  18. package/dist/components/ds-card-overview.js +1 -1
  19. package/dist/components/ds-card-overview.js.map +1 -1
  20. package/dist/components/ds-card-setting.js +1 -1
  21. package/dist/components/ds-chart-legend.js +1 -1
  22. package/dist/components/ds-chart-legend.js.map +1 -1
  23. package/dist/components/ds-checkbox.js +1 -1
  24. package/dist/components/ds-chip.js +1 -1
  25. package/dist/components/ds-chip.js.map +1 -1
  26. package/dist/components/ds-code-block.js +1 -1
  27. package/dist/components/ds-conversation-list-item.js +1 -1
  28. package/dist/components/ds-conversation-list-item.js.map +1 -1
  29. package/dist/components/ds-input.js +1 -1
  30. package/dist/components/ds-input.js.map +1 -1
  31. package/dist/components/ds-markdown.js +1 -1
  32. package/dist/components/ds-menu.js +1 -1
  33. package/dist/components/ds-message-actions.js +1 -1
  34. package/dist/components/ds-message-composer.js +1 -1
  35. package/dist/components/ds-message-scroller.js +1 -1
  36. package/dist/components/ds-mobile-bar-nav.js +1 -1
  37. package/dist/components/ds-mobile-header.js +1 -1
  38. package/dist/components/ds-mobile-section-switcher.js +1 -1
  39. package/dist/components/ds-mobile-sheet-nav.js +1 -1
  40. package/dist/components/ds-modal.js +1 -1
  41. package/dist/components/ds-panel-nav.js +1 -1
  42. package/dist/components/ds-panel-sub-nav.js +1 -1
  43. package/dist/components/ds-panel-sub-nav.js.map +1 -1
  44. package/dist/components/ds-panel-tool-header.js +1 -1
  45. package/dist/components/ds-panel-tool-search.js +1 -1
  46. package/dist/components/ds-panel-tools.js +1 -1
  47. package/dist/components/ds-radio.js +1 -1
  48. package/dist/components/ds-radio.js.map +1 -1
  49. package/dist/components/ds-select.js +1 -1
  50. package/dist/components/ds-select.js.map +1 -1
  51. package/dist/components/ds-shell-app.js +1 -1
  52. package/dist/components/ds-shell-tools.js +1 -1
  53. package/dist/components/ds-tab-group.js +1 -1
  54. package/dist/components/ds-table.js +1 -1
  55. package/dist/components/ds-table.js.map +1 -1
  56. package/dist/components/ds-tag.js +1 -1
  57. package/dist/components/ds-toast.js +1 -1
  58. package/dist/components/ds-typing-indicator.js +1 -1
  59. package/dist/components/p-B1JB0Zn-.js +2 -0
  60. package/dist/components/{p-BYre-ZDg.js.map → p-B1JB0Zn-.js.map} +1 -1
  61. package/dist/components/p-B2t8alvH.js +2 -0
  62. package/dist/components/{p-DK4PGrHm.js.map → p-B2t8alvH.js.map} +1 -1
  63. package/dist/components/{p-DxygUurH.js → p-BAGTb6cZ.js} +2 -2
  64. package/dist/components/p-BEEYhbIb.js +2 -0
  65. package/dist/components/{p-CFNG0K3m.js.map → p-BEEYhbIb.js.map} +1 -1
  66. package/dist/components/{p-BRajO-Za.js → p-BFMV9TJO.js} +2 -2
  67. package/dist/components/p-BNHPCCw8.js +2 -0
  68. package/dist/components/{p-x9jO4lZ7.js.map → p-BNHPCCw8.js.map} +1 -1
  69. package/dist/components/{p-DKftz0O8.js → p-BUZLhvs1.js} +2 -2
  70. package/dist/components/p-BgaF83uh.js +2 -0
  71. package/dist/components/{p-C5gZHZHU.js.map → p-BgaF83uh.js.map} +1 -1
  72. package/dist/components/p-Br0fwWUG.js +2 -0
  73. package/dist/components/{p-BFXN4ioL.js.map → p-Br0fwWUG.js.map} +1 -1
  74. package/dist/components/{p-DcmqsAMP.js → p-BydCwx7x.js} +2 -2
  75. package/dist/components/p-C3Z01Glr.js +2 -0
  76. package/dist/components/p-C3Z01Glr.js.map +1 -0
  77. package/dist/components/{p-BX57q1Lo.js → p-CSlTJWFG.js} +2 -2
  78. package/dist/components/p-Cw1bbiCG.js +2 -0
  79. package/dist/components/{p-B_eqa09C.js.map → p-Cw1bbiCG.js.map} +1 -1
  80. package/dist/components/p-CzV4gIxd.js +2 -0
  81. package/dist/components/{p-BxJurXEn.js.map → p-CzV4gIxd.js.map} +1 -1
  82. package/dist/components/p-D-F8eyXy.js +2 -0
  83. package/dist/components/p-D-F8eyXy.js.map +1 -0
  84. package/dist/components/p-DGvzGGcu.js +2 -0
  85. package/dist/components/{p-CIlO_j3q.js.map → p-DGvzGGcu.js.map} +1 -1
  86. package/dist/components/{p-DkhE3BsH.js → p-bLCbOPYa.js} +2 -2
  87. package/dist/components/p-cPh32CFB.js +2 -0
  88. package/dist/components/{p-Bj7KrXzw.js.map → p-cPh32CFB.js.map} +1 -1
  89. package/dist/docs/components.json +87 -3
  90. package/dist/lib/components/ButtonFilled/ButtonFilled.d.ts +2 -0
  91. package/dist/mcp-data/registry/button-filled.json +20 -1
  92. package/dist/mcp-data/registry/button-unfilled.json +20 -1
  93. package/dist/mcp-data/registry/registry.json +21 -2
  94. package/dist/mcp-data/registry/table.json +24 -5
  95. package/dist/react/ds-table.d.ts +2 -1
  96. package/dist/react/ds-table.js +2 -1
  97. package/dist/styles/table.css +258 -61
  98. package/dist/types/components/ButtonFilled/ButtonFilled.d.ts +2 -0
  99. package/dist/types/components/ButtonUnfilled/ButtonUnfilled.d.ts +2 -0
  100. package/dist/types/components/Table/Table.d.ts +12 -1
  101. package/dist/types/components/Table/index.d.ts +1 -1
  102. package/dist/types/components/Table/table-types.d.ts +11 -0
  103. package/dist/types/components.d.ts +26 -2
  104. package/package.json +1 -1
  105. package/dist/components/p-BFXN4ioL.js +0 -2
  106. package/dist/components/p-BYre-ZDg.js +0 -2
  107. package/dist/components/p-B_eqa09C.js +0 -2
  108. package/dist/components/p-Bj7KrXzw.js +0 -2
  109. package/dist/components/p-BxJurXEn.js +0 -2
  110. package/dist/components/p-C5gZHZHU.js +0 -2
  111. package/dist/components/p-CFNG0K3m.js +0 -2
  112. package/dist/components/p-CIlO_j3q.js +0 -2
  113. package/dist/components/p-DAbaMtEX.js +0 -2
  114. package/dist/components/p-DAbaMtEX.js.map +0 -1
  115. package/dist/components/p-DK4PGrHm.js +0 -2
  116. package/dist/components/p-c1pY-Mqq.js +0 -2
  117. package/dist/components/p-c1pY-Mqq.js.map +0 -1
  118. package/dist/components/p-x9jO4lZ7.js +0 -2
  119. /package/dist/components/{p-DxygUurH.js.map → p-BAGTb6cZ.js.map} +0 -0
  120. /package/dist/components/{p-BRajO-Za.js.map → p-BFMV9TJO.js.map} +0 -0
  121. /package/dist/components/{p-DKftz0O8.js.map → p-BUZLhvs1.js.map} +0 -0
  122. /package/dist/components/{p-DcmqsAMP.js.map → p-BydCwx7x.js.map} +0 -0
  123. /package/dist/components/{p-BX57q1Lo.js.map → p-CSlTJWFG.js.map} +0 -0
  124. /package/dist/components/{p-DkhE3BsH.js.map → p-bLCbOPYa.js.map} +0 -0
@@ -64,6 +64,7 @@
64
64
  ],
65
65
  "states": [
66
66
  "Every size applies one complete control-density recipe across frame height, inline padding, icon zone, text-container inset, gap, and radius.",
67
+ "Set isInset only when the button is nested inside a same-size control; it reduces the outer geometry while preserving the size's icon, label inset, gap, radius, and typography.",
67
68
  "A true pressed toggle and owner-controlled isActive emphasis share the selected visual treatment, but only pressed supplies toggle semantics and dsChange.",
68
69
  "General content UI keeps the selected interaction fill and border for pressed or active emphasis.",
69
70
  "Shell, navigation, and tool-rail composites use isActive with foreground-only emphasis and no selected fill or border.",
@@ -211,6 +212,15 @@
211
212
  "mutable": false,
212
213
  "description": "Disables interaction."
213
214
  },
215
+ "isInset": {
216
+ "type": "boolean",
217
+ "resolvedType": "boolean",
218
+ "attribute": "is-inset",
219
+ "default": "false",
220
+ "required": false,
221
+ "mutable": false,
222
+ "description": "Use reduced outer geometry when nested inside a control of the same size."
223
+ },
214
224
  "isLoading": {
215
225
  "type": "boolean",
216
226
  "resolvedType": "boolean",
@@ -426,6 +436,15 @@
426
436
  "mutable": false,
427
437
  "description": "Disables interaction."
428
438
  },
439
+ "isInset": {
440
+ "type": "boolean",
441
+ "resolvedType": "boolean",
442
+ "attribute": "is-inset",
443
+ "default": "false",
444
+ "required": false,
445
+ "mutable": false,
446
+ "description": "Use reduced outer geometry when nested inside a control of the same size."
447
+ },
429
448
  "isLoading": {
430
449
  "type": "boolean",
431
450
  "resolvedType": "boolean",
@@ -578,7 +597,7 @@
578
597
  },
579
598
  {
580
599
  "path": "src/wc/components/ButtonUnfilled/ButtonUnfilled.tsx",
581
- "content": "import { Component, Element, Event, EventEmitter, h, Host, Method, Prop } from '@stencil/core';\nimport { controlWidthClass } from '../../utils';\nimport { beginElevatedControlPress } from '../../utils/control-press';\nimport { renderButtonContent } from '../../utils/button-render';\nimport type {\n ButtonPopup,\n ButtonSize,\n ButtonVariant,\n ButtonWidth,\n} from '../../utils/button-types';\n\nexport type ButtonUnfilledBackground =\n | 'faint'\n | 'medium'\n | 'bold'\n | 'strong'\n | 'translucent'\n | 'inverted'\n | 'media'\n | 'always-dark';\n\nexport type ButtonUnfilledVariant = ButtonVariant;\nexport type ButtonUnfilledSize = ButtonSize;\nexport type ButtonUnfilledWidth = ButtonWidth;\nexport type ButtonUnfilledPopup = ButtonPopup;\n\n@Component({\n tag: 'ds-button-unfilled',\n styleUrl: 'ButtonUnfilled.css',\n scoped: true,\n})\nexport class ButtonUnfilled {\n @Element() el!: HTMLElement;\n\n /**\n * Content layout. Default is label-only; pass `icon` for icon-only chrome\n * (nav / tool rails) or `icon-label` for leading icon + text.\n */\n @Prop() variant: ButtonUnfilledVariant = 'label';\n\n /** Control density (height, padding, icon, type). */\n @Prop() size: ButtonUnfilledSize = 'md';\n\n /** Width fit — hug content (default) or fill the parent. */\n @Prop() width: ButtonUnfilledWidth = 'hug';\n\n /** Visible text for `label` / `icon-label` variants. */\n @Prop() label: string = '';\n\n /** Icon name passed to <ds-icon> for `icon` / `icon-label` variants. */\n @Prop() icon: string = '';\n\n /**\n * Owner-controlled visual emphasis inside a composite. This does not add\n * toggle semantics and does not emit dsChange.\n */\n @Prop() isActive: boolean = false;\n\n /**\n * When active, render the selected interaction fill.\n * Default `true` for general UI. Shell chrome (nav / tool rails) should pass\n * `false` so selection is foreground-only (primary color, no fill).\n */\n @Prop() activeFill: boolean = true;\n\n /** Show a 1px secondary inset border. Default on; shell chrome can pass `false`. */\n @Prop() hasBorder: boolean = true;\n\n /** Use the half-radius treatment instead of the default control radius. */\n @Prop() rounded: boolean = false;\n\n /**\n * Scale down during a physical pointer press.\n * Disable when an owning composite requires fixed child or background geometry.\n */\n @Prop() pressScale: boolean = true;\n\n /** Show a notification dot at the top-right of the icon zone (icon variant only). */\n @Prop() dot: boolean = false;\n\n /** Disables interaction. */\n @Prop() isInactive: boolean = false;\n\n /** Shows an inline loader and prevents interaction without applying inactive opacity. */\n @Prop() isLoading: boolean = false;\n\n /** Native button type. */\n @Prop() type: 'button' | 'submit' | 'reset' = 'button';\n\n /** Actual parent surface context. Omit on primary and secondary surfaces. */\n @Prop() background: ButtonUnfilledBackground | undefined;\n\n /** Accessible name override. Required for icon-only buttons. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n @Prop() controls: string | undefined;\n @Prop() expanded: boolean | undefined;\n @Prop() haspopup: ButtonUnfilledPopup | undefined;\n /**\n * Controlled state for a genuine toggle button. Adds aria-pressed, promotes\n * active styling, and makes activation emit dsChange with the requested state.\n */\n @Prop() pressed: boolean | undefined;\n\n /**\n * Mark the button as a Menu trigger. Implies `aria-haspopup=\"menu\"` and covers\n * both menu-button shapes:\n *\n * - `label` / `icon-label` — the action *has* a menu; a trailing chevron carries\n * the affordance.\n * - `icon` — the button *is* a menu, i.e. the overflow / more-options control.\n * No chevron is added, so the glyph must convey it on its own; use `Ellipses`.\n *\n * Use `haspopup` directly for non-menu popups.\n */\n @Prop() hasMenu: boolean = false;\n\n /**\n * Native `tabindex` for roving keyboard groups in shell chrome.\n * Omit for the default button tab stop (`0`).\n */\n @Prop({ attribute: 'tab-index' }) focusTabIndex?: number;\n\n @Event() dsClick!: EventEmitter<MouseEvent>;\n @Event() dsChange!: EventEmitter<boolean>;\n\n private buttonEl: HTMLButtonElement | null = null;\n\n @Method()\n async setFocus() {\n this.buttonEl?.focus();\n }\n\n private handleClick = (event: MouseEvent) => {\n if (this.isInactive || this.isLoading) {\n event.preventDefault();\n event.stopPropagation();\n return;\n }\n this.dsClick.emit(event);\n if (this.pressed !== undefined) this.dsChange.emit(!this.pressed);\n };\n\n private get showDot(): boolean {\n return this.variant === 'icon' && this.dot && !this.isLoading;\n }\n\n private get resolvedHaspopup(): ButtonUnfilledPopup | undefined {\n return this.haspopup ?? (this.hasMenu ? 'menu' : undefined);\n }\n\n /** An expanded popup trigger is visually active even when selection is controlled separately. */\n private get visuallyActive(): boolean {\n return this.isActive || this.pressed === true || this.expanded === true;\n }\n\n /** Knock-out ring: selected fill → active wash; otherwise surface token. */\n private get dotRing(): string {\n if ((this.isActive || this.pressed === true) && this.activeFill) {\n return 'var(--ds-interaction-active)';\n }\n return 'var(--ds-button-unfilled-dot-ring)';\n }\n\n private get accessibleName(): string | undefined {\n if (this.ariaLabel) return this.ariaLabel;\n if (this.isLoading && this.variant === 'label' && this.label) return this.label;\n return undefined;\n }\n\n render() {\n const bg = this.background;\n const cls: Record<string, boolean> = {\n 'button-unfilled': true,\n 'ds-button': true,\n 'ds-focus-ring-inset': true,\n 'ds-control-press-scale': this.pressScale,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--selected':\n (this.isActive || this.pressed === true) &&\n this.activeFill &&\n !this.isInactive,\n 'ds-interaction-fill--on-faint': bg === 'faint',\n 'ds-interaction-fill--on-medium': bg === 'medium',\n 'ds-interaction-fill--on-bold': bg === 'bold',\n 'ds-interaction-fill--on-strong': bg === 'strong',\n 'ds-interaction-fill--on-translucent': bg === 'translucent',\n 'ds-interaction-fill--on-inverted': bg === 'inverted',\n 'ds-interaction-fill--on-media': bg === 'media',\n 'ds-interaction-fill--on-always-dark': bg === 'always-dark',\n 'button-unfilled--active': this.visuallyActive,\n 'button-unfilled--expanded': this.expanded === true && !this.isInactive,\n 'ds-button--expanded': this.expanded === true && !this.isInactive,\n 'button-unfilled--bordered': this.hasBorder,\n 'ds-button--bordered': this.hasBorder,\n 'button-unfilled--rounded': this.rounded,\n 'ds-button--rounded': this.rounded,\n 'ds-control-inactive': this.isInactive,\n 'ds-control--lg': this.size === 'lg',\n 'ds-control--md': this.size === 'md',\n 'ds-control--sm': this.size === 'sm',\n 'ds-control--xs': this.size === 'xs',\n 'ds-control-frame': true,\n 'button-unfilled--icon': this.variant === 'icon',\n 'ds-button--icon': this.variant === 'icon',\n 'button-unfilled--label': this.variant === 'label',\n 'button-unfilled--icon-label': this.variant === 'icon-label',\n 'button-unfilled--background-faint': bg === 'faint',\n 'button-unfilled--background-medium': bg === 'medium',\n 'button-unfilled--background-bold': bg === 'bold',\n 'button-unfilled--background-strong': bg === 'strong',\n 'button-unfilled--background-translucent': bg === 'translucent',\n 'button-unfilled--background-inverted': bg === 'inverted',\n 'button-unfilled--background-media': bg === 'media',\n 'button-unfilled--on-always-dark': bg === 'always-dark',\n };\n\n return (\n <Host\n class={{\n 'button-unfilled-host': true,\n 'button-unfilled-host--icon': this.variant === 'icon',\n 'ds-button-host--icon': this.variant === 'icon',\n 'ds-control--lg': this.size === 'lg',\n 'ds-control--md': this.size === 'md',\n 'ds-control--sm': this.size === 'sm',\n 'ds-control--xs': this.size === 'xs',\n ...controlWidthClass(this.width),\n }}\n tabIndex={-1}\n >\n <button\n ref={el => {\n this.buttonEl = el ?? null;\n }}\n type={this.type}\n class={cls}\n disabled={this.isInactive}\n tabIndex={this.focusTabIndex ?? 0}\n aria-label={this.accessibleName}\n aria-busy={this.isLoading ? 'true' : undefined}\n aria-disabled={this.isLoading ? 'true' : undefined}\n aria-controls={this.controls}\n aria-expanded={this.expanded === undefined ? undefined : String(this.expanded)}\n aria-haspopup={this.resolvedHaspopup}\n aria-pressed={this.pressed === undefined ? undefined : String(this.pressed)}\n onPointerDown={event =>\n beginElevatedControlPress(\n event,\n this.pressScale && !this.isInactive && !this.isLoading,\n )\n }\n onClick={this.handleClick}\n >\n {renderButtonContent({\n namespace: 'button-unfilled',\n variant: this.variant,\n size: this.size,\n label: this.label,\n icon: this.icon,\n hasMenu: this.hasMenu,\n isLoading: this.isLoading,\n dot: {\n visible: this.showDot,\n background: this.dotRing,\n },\n })}\n </button>\n </Host>\n );\n }\n}\n",
600
+ "content": "import { Component, Element, Event, EventEmitter, h, Host, Method, Prop } from '@stencil/core';\nimport { controlWidthClass } from '../../utils';\nimport { beginElevatedControlPress } from '../../utils/control-press';\nimport { renderButtonContent } from '../../utils/button-render';\nimport type {\n ButtonPopup,\n ButtonSize,\n ButtonVariant,\n ButtonWidth,\n} from '../../utils/button-types';\n\nexport type ButtonUnfilledBackground =\n | 'faint'\n | 'medium'\n | 'bold'\n | 'strong'\n | 'translucent'\n | 'inverted'\n | 'media'\n | 'always-dark';\n\nexport type ButtonUnfilledVariant = ButtonVariant;\nexport type ButtonUnfilledSize = ButtonSize;\nexport type ButtonUnfilledWidth = ButtonWidth;\nexport type ButtonUnfilledPopup = ButtonPopup;\n\n@Component({\n tag: 'ds-button-unfilled',\n styleUrl: 'ButtonUnfilled.css',\n scoped: true,\n})\nexport class ButtonUnfilled {\n @Element() el!: HTMLElement;\n\n /**\n * Content layout. Default is label-only; pass `icon` for icon-only chrome\n * (nav / tool rails) or `icon-label` for leading icon + text.\n */\n @Prop() variant: ButtonUnfilledVariant = 'label';\n\n /** Control density (height, padding, icon, type). */\n @Prop() size: ButtonUnfilledSize = 'md';\n\n /** Use reduced outer geometry when nested inside a control of the same size. */\n @Prop() isInset: boolean = false;\n\n /** Width fit — hug content (default) or fill the parent. */\n @Prop() width: ButtonUnfilledWidth = 'hug';\n\n /** Visible text for `label` / `icon-label` variants. */\n @Prop() label: string = '';\n\n /** Icon name passed to <ds-icon> for `icon` / `icon-label` variants. */\n @Prop() icon: string = '';\n\n /**\n * Owner-controlled visual emphasis inside a composite. This does not add\n * toggle semantics and does not emit dsChange.\n */\n @Prop() isActive: boolean = false;\n\n /**\n * When active, render the selected interaction fill.\n * Default `true` for general UI. Shell chrome (nav / tool rails) should pass\n * `false` so selection is foreground-only (primary color, no fill).\n */\n @Prop() activeFill: boolean = true;\n\n /** Show a 1px secondary inset border. Default on; shell chrome can pass `false`. */\n @Prop() hasBorder: boolean = true;\n\n /** Use the half-radius treatment instead of the default control radius. */\n @Prop() rounded: boolean = false;\n\n /**\n * Scale down during a physical pointer press.\n * Disable when an owning composite requires fixed child or background geometry.\n */\n @Prop() pressScale: boolean = true;\n\n /** Show a notification dot at the top-right of the icon zone (icon variant only). */\n @Prop() dot: boolean = false;\n\n /** Disables interaction. */\n @Prop() isInactive: boolean = false;\n\n /** Shows an inline loader and prevents interaction without applying inactive opacity. */\n @Prop() isLoading: boolean = false;\n\n /** Native button type. */\n @Prop() type: 'button' | 'submit' | 'reset' = 'button';\n\n /** Actual parent surface context. Omit on primary and secondary surfaces. */\n @Prop() background: ButtonUnfilledBackground | undefined;\n\n /** Accessible name override. Required for icon-only buttons. */\n @Prop({ attribute: 'aria-label' }) ariaLabel: string | null = null;\n @Prop() controls: string | undefined;\n @Prop() expanded: boolean | undefined;\n @Prop() haspopup: ButtonUnfilledPopup | undefined;\n /**\n * Controlled state for a genuine toggle button. Adds aria-pressed, promotes\n * active styling, and makes activation emit dsChange with the requested state.\n */\n @Prop() pressed: boolean | undefined;\n\n /**\n * Mark the button as a Menu trigger. Implies `aria-haspopup=\"menu\"` and covers\n * both menu-button shapes:\n *\n * - `label` / `icon-label` — the action *has* a menu; a trailing chevron carries\n * the affordance.\n * - `icon` — the button *is* a menu, i.e. the overflow / more-options control.\n * No chevron is added, so the glyph must convey it on its own; use `Ellipses`.\n *\n * Use `haspopup` directly for non-menu popups.\n */\n @Prop() hasMenu: boolean = false;\n\n /**\n * Native `tabindex` for roving keyboard groups in shell chrome.\n * Omit for the default button tab stop (`0`).\n */\n @Prop({ attribute: 'tab-index' }) focusTabIndex?: number;\n\n @Event() dsClick!: EventEmitter<MouseEvent>;\n @Event() dsChange!: EventEmitter<boolean>;\n\n private buttonEl: HTMLButtonElement | null = null;\n\n @Method()\n async setFocus() {\n this.buttonEl?.focus();\n }\n\n private handleClick = (event: MouseEvent) => {\n if (this.isInactive || this.isLoading) {\n event.preventDefault();\n event.stopPropagation();\n return;\n }\n this.dsClick.emit(event);\n if (this.pressed !== undefined) this.dsChange.emit(!this.pressed);\n };\n\n private get showDot(): boolean {\n return this.variant === 'icon' && this.dot && !this.isLoading;\n }\n\n private get resolvedHaspopup(): ButtonUnfilledPopup | undefined {\n return this.haspopup ?? (this.hasMenu ? 'menu' : undefined);\n }\n\n /** An expanded popup trigger is visually active even when selection is controlled separately. */\n private get visuallyActive(): boolean {\n return this.isActive || this.pressed === true || this.expanded === true;\n }\n\n /** Knock-out ring: selected fill → active wash; otherwise surface token. */\n private get dotRing(): string {\n if ((this.isActive || this.pressed === true) && this.activeFill) {\n return 'var(--ds-interaction-active)';\n }\n return 'var(--ds-button-unfilled-dot-ring)';\n }\n\n private get accessibleName(): string | undefined {\n if (this.ariaLabel) return this.ariaLabel;\n if (this.isLoading && this.variant === 'label' && this.label) return this.label;\n return undefined;\n }\n\n render() {\n const bg = this.background;\n const cls: Record<string, boolean> = {\n 'button-unfilled': true,\n 'ds-button': true,\n 'ds-focus-ring-inset': true,\n 'ds-control-press-scale': this.pressScale,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--selected':\n (this.isActive || this.pressed === true) &&\n this.activeFill &&\n !this.isInactive,\n 'ds-interaction-fill--on-faint': bg === 'faint',\n 'ds-interaction-fill--on-medium': bg === 'medium',\n 'ds-interaction-fill--on-bold': bg === 'bold',\n 'ds-interaction-fill--on-strong': bg === 'strong',\n 'ds-interaction-fill--on-translucent': bg === 'translucent',\n 'ds-interaction-fill--on-inverted': bg === 'inverted',\n 'ds-interaction-fill--on-media': bg === 'media',\n 'ds-interaction-fill--on-always-dark': bg === 'always-dark',\n 'button-unfilled--active': this.visuallyActive,\n 'button-unfilled--expanded': this.expanded === true && !this.isInactive,\n 'ds-button--expanded': this.expanded === true && !this.isInactive,\n 'button-unfilled--bordered': this.hasBorder,\n 'ds-button--bordered': this.hasBorder,\n 'button-unfilled--rounded': this.rounded,\n 'ds-button--rounded': this.rounded,\n 'ds-control-inactive': this.isInactive,\n 'ds-control--lg': this.size === 'lg',\n 'ds-control--md': this.size === 'md',\n 'ds-control--sm': this.size === 'sm',\n 'ds-control--xs': this.size === 'xs',\n 'ds-control--inset': this.isInset,\n 'ds-control-frame': true,\n 'button-unfilled--icon': this.variant === 'icon',\n 'ds-button--icon': this.variant === 'icon',\n 'button-unfilled--label': this.variant === 'label',\n 'button-unfilled--icon-label': this.variant === 'icon-label',\n 'button-unfilled--background-faint': bg === 'faint',\n 'button-unfilled--background-medium': bg === 'medium',\n 'button-unfilled--background-bold': bg === 'bold',\n 'button-unfilled--background-strong': bg === 'strong',\n 'button-unfilled--background-translucent': bg === 'translucent',\n 'button-unfilled--background-inverted': bg === 'inverted',\n 'button-unfilled--background-media': bg === 'media',\n 'button-unfilled--on-always-dark': bg === 'always-dark',\n };\n\n return (\n <Host\n class={{\n 'button-unfilled-host': true,\n 'button-unfilled-host--icon': this.variant === 'icon',\n 'ds-button-host--icon': this.variant === 'icon',\n 'ds-control--lg': this.size === 'lg',\n 'ds-control--md': this.size === 'md',\n 'ds-control--sm': this.size === 'sm',\n 'ds-control--xs': this.size === 'xs',\n 'ds-control--inset': this.isInset,\n ...controlWidthClass(this.width),\n }}\n tabIndex={-1}\n >\n <button\n ref={el => {\n this.buttonEl = el ?? null;\n }}\n type={this.type}\n class={cls}\n disabled={this.isInactive}\n tabIndex={this.focusTabIndex ?? 0}\n aria-label={this.accessibleName}\n aria-busy={this.isLoading ? 'true' : undefined}\n aria-disabled={this.isLoading ? 'true' : undefined}\n aria-controls={this.controls}\n aria-expanded={this.expanded === undefined ? undefined : String(this.expanded)}\n aria-haspopup={this.resolvedHaspopup}\n aria-pressed={this.pressed === undefined ? undefined : String(this.pressed)}\n onPointerDown={event =>\n beginElevatedControlPress(\n event,\n this.pressScale && !this.isInactive && !this.isLoading,\n )\n }\n onClick={this.handleClick}\n >\n {renderButtonContent({\n namespace: 'button-unfilled',\n variant: this.variant,\n size: this.size,\n label: this.label,\n icon: this.icon,\n hasMenu: this.hasMenu,\n isLoading: this.isLoading,\n dot: {\n visible: this.showDot,\n background: this.dotRing,\n },\n })}\n </button>\n </Host>\n );\n }\n}\n",
582
601
  "type": "registry:ui"
583
602
  }
584
603
  ]
@@ -1523,6 +1523,7 @@
1523
1523
  ],
1524
1524
  "states": [
1525
1525
  "Every size applies one complete control-density recipe across frame height, inline padding, icon zone, text-container inset, gap, and radius.",
1526
+ "Set isInset only when the button is nested inside a same-size control; it reduces the outer geometry while preserving the size's icon, label inset, gap, radius, and typography.",
1526
1527
  "Use only one filled action per local decision area; render all secondary actions with ButtonUnfilled.",
1527
1528
  "Choose brand or neutral for general commands. Reserve positive, negative, warning, and caution for matching action consequences, and AI, guide, or walkthrough for actions that enter those feature experiences.",
1528
1529
  "Contrast serves both surface adaptation and hierarchy, but never permits a second filled action in the same decision area.",
@@ -1602,6 +1603,10 @@
1602
1603
  "type": "boolean",
1603
1604
  "required": false
1604
1605
  },
1606
+ "isInset": {
1607
+ "type": "boolean",
1608
+ "required": false
1609
+ },
1605
1610
  "isLoading": {
1606
1611
  "type": "boolean",
1607
1612
  "required": false
@@ -1725,6 +1730,7 @@
1725
1730
  ],
1726
1731
  "states": [
1727
1732
  "Every size applies one complete control-density recipe across frame height, inline padding, icon zone, text-container inset, gap, and radius.",
1733
+ "Set isInset only when the button is nested inside a same-size control; it reduces the outer geometry while preserving the size's icon, label inset, gap, radius, and typography.",
1728
1734
  "A true pressed toggle and owner-controlled isActive emphasis share the selected visual treatment, but only pressed supplies toggle semantics and dsChange.",
1729
1735
  "General content UI keeps the selected interaction fill and border for pressed or active emphasis.",
1730
1736
  "Shell, navigation, and tool-rail composites use isActive with foreground-only emphasis and no selected fill or border.",
@@ -1814,6 +1820,10 @@
1814
1820
  "type": "boolean",
1815
1821
  "required": false
1816
1822
  },
1823
+ "isInset": {
1824
+ "type": "boolean",
1825
+ "required": false
1826
+ },
1817
1827
  "isLoading": {
1818
1828
  "type": "boolean",
1819
1829
  "required": false
@@ -8055,7 +8065,7 @@
8055
8065
  "avoidWhen": [
8056
8066
  "Content is primarily editorial prose; use semantic product-owned markup with the prose recipe instead.",
8057
8067
  "A small label/value set reads more clearly as a list, description list, or card.",
8058
- "The interface requires tree-grid behavior, nested grouping, inline cell editing, row reordering, column pinning, or virtualization; those are outside this first contract.",
8068
+ "The interface requires tree-grid behavior, nested grouping, inline cell editing, row reordering, multiple pinned columns per edge, or virtualization; those are outside this first contract.",
8059
8069
  "The application expects the component to fetch, paginate, sort, group, or mutate records internally."
8060
8070
  ],
8061
8071
  "commonlyComposedWith": [
@@ -8070,6 +8080,7 @@
8070
8080
  "The rendered structure uses a native table, caption, column headers, row groups, and data cells; keep the caption non-empty even when it is visually hidden.",
8071
8081
  "Sorting controls remain buttons inside column headers. Member-row sort uses aria-sort, while independent group order is described by the grouped header control.",
8072
8082
  "Selection controls expose checkbox semantics and explicit row names; select-all affects selectable loaded rows only and preserves selected identities outside the loaded window.",
8083
+ "Interactive rows are focusable as native rows, activate with Enter or Space, and do not steal activation from their nested selection or action controls.",
8073
8084
  "Scrollable table regions become keyboard focusable and named only when overflow exists.",
8074
8085
  "Incremental loading, newly appended row counts, failures, and end-of-results transitions are announced through a polite status region without replacing visible state."
8075
8086
  ],
@@ -8080,11 +8091,15 @@
8080
8091
  "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.",
8081
8092
  "Initial loading uses row-shaped skeletons. Empty and initial error states remain inside the table. Incremental loading retains existing rows and supports automatic sentinel, manual load-more, retry, and terminal states.",
8082
8093
  "Lazy loading replaces pagination. The application owns requests, cancellation, cursors, deduplication, appended records, and query identity; loadIdentity resets the component request guard.",
8094
+ "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.",
8095
+ "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.",
8083
8096
  "Cells accept scalar content, standard primary-and-secondary or dual-primary text primitives, declarative Tag, icon-only, 16:9 Image, or ButtonUnfilled Action content, or explicit Empty and Blank absence values. Single-track text, selection, tag-only, icon-only, Action, Empty, and Blank cells resolve a 40px base contract while multi-track cells grow downward. Action cells primarily use an icon-only Ellipses treatment, render a 24px small ButtonUnfilled inside 8px padding, default to unbordered, and emit row, column, and action identities through dsCellAction. Image cells render a 52px-high bordered preview inside 6px outer padding, establishing a 64px row, and fall back to an accessible placeholder when no source is supplied. Empty means the data applies but has no value and renders an em dash with localized assistive text; Blank means the data is not applicable and renders no content. Nullish values remain backward-compatible Empty values. Stable type and variant hooks keep every cell family independently styleable.",
8084
8097
  "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."
8085
8098
  ],
8086
8099
  "responsiveBehavior": [
8087
8100
  "The table keeps native semantics at narrow widths and scrolls horizontally instead of transforming into cards.",
8101
+ "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.",
8102
+ "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.",
8088
8103
  "Explicit column sizes establish the horizontal overflow width; unsized columns share remaining space.",
8089
8104
  "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.",
8090
8105
  "Column headers use one 32px geometry. Body rows use a 40px single-track minimum; cell-specific outer padding and track geometry align text, selection, Tag, icon, Action, Empty, Blank, and Image primitives while additional tracks grow downward.",
@@ -8093,7 +8108,7 @@
8093
8108
  "frameworkCaveats": {
8094
8109
  "customElements": [
8095
8110
  "Assign columns, rows, groups, grouping, sort, and selectedRowIds through JavaScript properties rather than HTML attribute strings.",
8096
- "Replace controlled property values in response to dsSortChange, dsGroupingChange, dsSelectionChange, and dsLoadMore."
8111
+ "Replace controlled property values in response to dsSortChange, dsGroupingChange, dsSelectionChange, and dsLoadMore; route dsRowActivate in the application owner."
8097
8112
  ],
8098
8113
  "react": [
8099
8114
  "Pass new array and state-object references when the controlled data or query state changes.",
@@ -8258,6 +8273,10 @@
8258
8273
  "name": "dsLoadMore",
8259
8274
  "detail": "TableLoadMoreDetail"
8260
8275
  },
8276
+ {
8277
+ "name": "dsRowActivate",
8278
+ "detail": "TableRowActivateDetail"
8279
+ },
8261
8280
  {
8262
8281
  "name": "dsSelectionChange",
8263
8282
  "detail": "TableSelectionChangeDetail"
@@ -22,7 +22,7 @@
22
22
  "avoidWhen": [
23
23
  "Content is primarily editorial prose; use semantic product-owned markup with the prose recipe instead.",
24
24
  "A small label/value set reads more clearly as a list, description list, or card.",
25
- "The interface requires tree-grid behavior, nested grouping, inline cell editing, row reordering, column pinning, or virtualization; those are outside this first contract.",
25
+ "The interface requires tree-grid behavior, nested grouping, inline cell editing, row reordering, multiple pinned columns per edge, or virtualization; those are outside this first contract.",
26
26
  "The application expects the component to fetch, paginate, sort, group, or mutate records internally."
27
27
  ],
28
28
  "commonlyComposedWith": [
@@ -37,6 +37,7 @@
37
37
  "The rendered structure uses a native table, caption, column headers, row groups, and data cells; keep the caption non-empty even when it is visually hidden.",
38
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.",
40
+ "Interactive rows are focusable as native rows, activate with Enter or Space, and do not steal activation from their nested selection or action controls.",
40
41
  "Scrollable table regions become keyboard focusable and named only when overflow exists.",
41
42
  "Incremental loading, newly appended row counts, failures, and end-of-results transitions are announced through a polite status region without replacing visible state."
42
43
  ],
@@ -47,11 +48,15 @@
47
48
  "Compound headers can expose multiple related labels with independent sort keys. Each label owns only its content-width interaction target while the active direction remains in the column's alignment-aware sort lane.",
48
49
  "Initial loading uses row-shaped skeletons. Empty and initial error states remain inside the table. Incremental loading retains existing rows and supports automatic sentinel, manual load-more, retry, and terminal states.",
49
50
  "Lazy loading replaces pagination. The application owns requests, cancellation, cursors, deduplication, appended records, and query identity; loadIdentity resets the component request guard.",
51
+ "Rows opt into complete-row activation with interactive and emit dsRowActivate without owning navigation. Selection paints through the shared selected ::before layer; interactive hover and press paint through the shared ::after layer.",
52
+ "Row selection owns the single sticky start lane. One explicitly sized application column can own the sticky end lane, or the start lane when selection is absent. Each sticky boundary owns one fixed divider and a row-clipped directional shadow that paints into the adjacent scrolling columns only while more inline content remains beyond that edge.",
50
53
  "Cells accept scalar content, standard primary-and-secondary or dual-primary text primitives, declarative Tag, icon-only, 16:9 Image, or ButtonUnfilled Action content, or explicit Empty and Blank absence values. Single-track text, selection, tag-only, icon-only, Action, Empty, and Blank cells resolve a 40px base contract while multi-track cells grow downward. Action cells primarily use an icon-only Ellipses treatment, render a 24px small ButtonUnfilled inside 8px padding, default to unbordered, and emit row, column, and action identities through dsCellAction. Image cells render a 52px-high bordered preview inside 6px outer padding, establishing a 64px row, and fall back to an accessible placeholder when no source is supplied. Empty means the data applies but has no value and renders an em dash with localized assistive text; Blank means the data is not applicable and renders no content. Nullish values remain backward-compatible Empty values. Stable type and variant hooks keep every cell family independently styleable.",
51
54
  "The public @ds-mo/ui/table.css export is the authoritative zero-specificity visual recipe. Override its --ds-table-* properties or target its stable element classes for product-specific styling."
52
55
  ],
53
56
  "responsiveBehavior": [
54
57
  "The table keeps native semantics at narrow widths and scrolls horizontally instead of transforming into cards.",
58
+ "stickyHeader follows the active vertical scroll owner: a maxHeight table keeps its native header inside the bounded table scrollport, while a document-flow table retains a semantic native header and renders a sticky control header whose columns follow the body's native inline scroll timeline. Consumers set --ds-table-sticky-header-offset when page chrome occupies the scrollport edge.",
59
+ "Without maxHeight the table grows the document and automatic lazy loading observes the browser viewport. Horizontal scrolling chains vertical wheel and trackpad movement to the page instead of trapping it.",
55
60
  "Explicit column sizes establish the horizontal overflow width; unsized columns share remaining space.",
56
61
  "Prefer the named xs through xl column sizes, which resolve through TokoMo's table-column width tokens; numeric pixel sizes remain an escape hatch for exceptional application-owned widths.",
57
62
  "Column headers use one 32px geometry. Body rows use a 40px single-track minimum; cell-specific outer padding and track geometry align text, selection, Tag, icon, Action, Empty, Blank, and Image primitives while additional tracks grow downward.",
@@ -60,7 +65,7 @@
60
65
  "frameworkCaveats": {
61
66
  "customElements": [
62
67
  "Assign columns, rows, groups, grouping, sort, and selectedRowIds through JavaScript properties rather than HTML attribute strings.",
63
- "Replace controlled property values in response to dsSortChange, dsGroupingChange, dsSelectionChange, and dsLoadMore."
68
+ "Replace controlled property values in response to dsSortChange, dsGroupingChange, dsSelectionChange, and dsLoadMore; route dsRowActivate in the application owner."
64
69
  ],
65
70
  "react": [
66
71
  "Pass new array and state-object references when the controlled data or query state changes.",
@@ -368,6 +373,13 @@
368
373
  "cancelable": true,
369
374
  "composed": true
370
375
  },
376
+ {
377
+ "name": "dsRowActivate",
378
+ "detail": "TableRowActivateDetail",
379
+ "bubbles": true,
380
+ "cancelable": true,
381
+ "composed": true
382
+ },
371
383
  {
372
384
  "name": "dsSelectionChange",
373
385
  "detail": "TableSelectionChangeDetail",
@@ -671,6 +683,13 @@
671
683
  "cancelable": true,
672
684
  "composed": true
673
685
  },
686
+ {
687
+ "name": "dsRowActivate",
688
+ "detail": "TableRowActivateDetail",
689
+ "bubbles": true,
690
+ "cancelable": true,
691
+ "composed": true
692
+ },
674
693
  {
675
694
  "name": "dsSelectionChange",
676
695
  "detail": "TableSelectionChangeDetail",
@@ -741,17 +760,17 @@
741
760
  },
742
761
  {
743
762
  "path": "src/wc/components/Table/Table.tsx",
744
- "content": "import {\n Component,\n Event,\n EventEmitter,\n h,\n Host,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport {\n deriveTableSelectionState,\n isTableCellAction,\n isTableCellBlank,\n isTableCellEmpty,\n isTableCellIcon,\n isTableCellImage,\n isTableCellPrimaryText,\n isTableCellText,\n isTableCellTag,\n nextTableGroupOrder,\n nextTableSortState,\n resolvedTableGroupCount,\n tableColumnSize,\n tableExplicitMinWidth,\n tableModelIssues,\n tableRows,\n tableRowSelectionLabel,\n toggleAllLoadedTableRows,\n toggleTableRowSelection,\n} from './table-model';\nimport type {\n TableCaptionVisibility,\n TableCellActionDetail,\n TableCellValue,\n TableColumn,\n TableGroup,\n TableGroupingChangeDetail,\n TableGroupingState,\n TableLoadMoreDetail,\n TableLoadMoreMode,\n TableLoadMoreReason,\n TableRow,\n TableSelectionChangeDetail,\n TableSelectionMode,\n TableSortChangeDetail,\n TableSortState,\n} from './table-types';\n\n@Component({\n tag: 'ds-table',\n styleUrl: 'Table.css',\n scoped: true,\n})\nexport class Table {\n /** Stable column definitions. Assign through JavaScript. */\n @Prop() columns: TableColumn[] = [];\n /** Ungrouped row data. Ignored while grouping is active. Assign through JavaScript. */\n @Prop() rows: TableRow[] = [];\n /** One level of application-owned grouped data. Assign through JavaScript. */\n @Prop() groups: TableGroup[] = [];\n /** Controlled grouping column and group-order direction. */\n @Prop() grouping: TableGroupingState | null = null;\n /** Controlled member-row sort state. */\n @Prop() sort: TableSortState | null = null;\n\n /** Required accessible table name, rendered as a native caption. */\n @Prop() caption!: string;\n @Prop() captionVisibility: TableCaptionVisibility = 'hidden';\n @Prop() stickyHeader: boolean = false;\n /** Maximum scroll-region height. Numbers resolve to CSS pixels. */\n @Prop() maxHeight: string | number | undefined;\n /** Optional explicit label for the horizontal/vertical scroll region. */\n @Prop() scrollLabel: string | undefined;\n\n @Prop() selectionMode: TableSelectionMode = 'none';\n /** Controlled selected row identities. IDs outside the loaded rows are preserved. */\n @Prop() selectedRowIds: string[] = [];\n\n /** Initial loading state. Existing rows stay visible; incremental loading uses loadingMore. */\n @Prop() loading: boolean = false;\n @Prop() skeletonRows: number = 5;\n @Prop() emptyHeading: string = 'No results';\n @Prop() emptyBody: string = 'No data is available.';\n /** Initial error state. Existing rows stay visible; incremental failures use loadMoreError. */\n @Prop() error: boolean = false;\n @Prop() errorHeading: string = 'Unable to load data';\n @Prop() errorBody: string = 'The data could not be loaded.';\n @Prop() emptyCellLabel: string = 'Not available';\n\n /** Enable application-owned incremental loading without pagination. */\n @Prop() lazyLoading: boolean = false;\n @Prop() loadMoreMode: TableLoadMoreMode = 'auto';\n @Prop() hasMore: boolean = false;\n @Prop() loadingMore: boolean = false;\n @Prop() loadMoreError: string | undefined;\n /** Reset key for a new query/group/sort dataset. */\n @Prop() loadIdentity: string | number = 'default';\n /** IntersectionObserver root margin in CSS pixels. */\n @Prop() loadMoreThreshold: number = 0;\n @Prop() loadMoreLabel: string = 'Load more';\n @Prop() retryLabel: string = 'Retry';\n @Prop() loadingMoreLabel: string = 'Loading more results';\n @Prop() endOfResultsLabel: string = 'All results loaded';\n /** Supports {count} and {total} placeholders. */\n @Prop() rowsLoadedLabel: string = '{count} more rows loaded. {total} rows loaded.';\n\n @Event() dsSortChange!: EventEmitter<TableSortChangeDetail>;\n @Event() dsGroupingChange!: EventEmitter<TableGroupingChangeDetail>;\n @Event() dsSelectionChange!: EventEmitter<TableSelectionChangeDetail>;\n @Event() dsLoadMore!: EventEmitter<TableLoadMoreDetail>;\n @Event() dsCellAction!: EventEmitter<TableCellActionDetail>;\n\n @State() private overflowStart = false;\n @State() private overflowEnd = false;\n @State() private scrollable = false;\n @State() private intersectionSupported = true;\n @State() private announcement = '';\n\n private viewportEl: HTMLElement | null = null;\n private tableEl: HTMLTableElement | null = null;\n private sentinelEl: HTMLElement | null = null;\n private resizeObserver: ResizeObserver | null = null;\n private intersectionObserver: IntersectionObserver | null = null;\n private observedSentinel: HTMLElement | null = null;\n private requestPending = false;\n private requestedRowCount = 0;\n private previousLoadedRowCount = 0;\n private previousModelWarning = '';\n private hasLoaded = false;\n private reconnectFrame: number | null = null;\n\n componentWillLoad(): void {\n this.previousLoadedRowCount = this.loadedRows.length;\n this.intersectionSupported = typeof IntersectionObserver !== 'undefined';\n this.warnModelIssues();\n }\n\n componentDidLoad(): void {\n this.hasLoaded = true;\n this.connectViewportObserver();\n this.syncOverflow();\n this.connectIntersectionObserver();\n }\n\n componentDidRender(): void {\n this.connectIntersectionObserver();\n }\n\n connectedCallback(): void {\n if (!this.hasLoaded) return;\n if (this.reconnectFrame !== null) cancelAnimationFrame(this.reconnectFrame);\n this.reconnectFrame = requestAnimationFrame(() => {\n this.reconnectFrame = null;\n this.connectViewportObserver();\n this.syncOverflow();\n this.connectIntersectionObserver();\n });\n }\n\n disconnectedCallback(): void {\n if (this.reconnectFrame !== null) cancelAnimationFrame(this.reconnectFrame);\n this.reconnectFrame = null;\n this.viewportEl?.removeEventListener('scroll', this.syncOverflow);\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n this.disconnectIntersectionObserver();\n }\n\n @Watch('columns')\n @Watch('grouping')\n handleStructureChange(): void {\n this.warnModelIssues();\n this.resetLoadRequest();\n }\n\n @Watch('rows')\n @Watch('groups')\n handleDataChange(): void {\n const nextCount = this.loadedRows.length;\n if (nextCount > this.previousLoadedRowCount) {\n const added = nextCount - this.previousLoadedRowCount;\n this.announcement = this.rowsLoadedLabel\n .replace('{count}', String(added))\n .replace('{total}', String(nextCount));\n }\n if (nextCount > this.requestedRowCount) this.requestPending = false;\n this.previousLoadedRowCount = nextCount;\n this.warnModelIssues();\n this.disconnectIntersectionObserver();\n }\n\n @Watch('loadIdentity')\n handleLoadIdentityChange(): void {\n this.previousLoadedRowCount = this.loadedRows.length;\n this.resetLoadRequest();\n }\n\n @Watch('lazyLoading')\n @Watch('loadMoreMode')\n @Watch('loadMoreThreshold')\n handleLazyConfigurationChange(): void {\n this.disconnectIntersectionObserver();\n }\n\n @Watch('loadingMore')\n handleLoadingMoreChange(loading: boolean): void {\n if (loading) {\n this.requestPending = true;\n this.announcement = this.loadingMoreLabel;\n } else if (this.loadMoreMode === 'manual') {\n // A completed manual request may legitimately append no rows. Permit a\n // deliberate follow-up activation; automatic mode stays guarded until\n // the data shape or query identity changes.\n this.requestPending = false;\n }\n this.disconnectIntersectionObserver();\n }\n\n @Watch('loadMoreError')\n handleLoadMoreErrorChange(error: string | undefined): void {\n if (error?.trim()) {\n this.requestPending = false;\n this.announcement = error;\n }\n this.disconnectIntersectionObserver();\n }\n\n @Watch('hasMore')\n handleHasMoreChange(hasMore: boolean, hadMore: boolean): void {\n if (!hasMore) {\n this.requestPending = false;\n if (hadMore) this.announcement = this.endOfResultsLabel;\n }\n this.disconnectIntersectionObserver();\n }\n\n private get grouped(): boolean {\n return this.grouping !== null;\n }\n\n private get selectable(): boolean {\n return this.selectionMode === 'multiple';\n }\n\n private get loadedRows(): TableRow[] {\n return tableRows(this.rows, this.groups, this.grouped);\n }\n\n private get hasData(): boolean {\n return this.loadedRows.length > 0;\n }\n\n private get totalColumns(): number {\n return this.columns.length + (this.selectable ? 1 : 0);\n }\n\n private get selectedSet(): Set<string> {\n return new Set(this.selectedRowIds);\n }\n\n private get selectionState() {\n return deriveTableSelectionState(this.loadedRows, this.selectedRowIds);\n }\n\n private get resolvedMaxHeight(): string | undefined {\n if (this.maxHeight == null || this.maxHeight === '') return undefined;\n return typeof this.maxHeight === 'number' ? `${Math.max(0, this.maxHeight)}px` : this.maxHeight;\n }\n\n private get tableStyle(): Record<string, string> | undefined {\n const width = tableExplicitMinWidth(this.columns);\n return width == null ? undefined : { '--ds-table-explicit-min-inline-size': width };\n }\n\n private warnModelIssues(): void {\n const issues = tableModelIssues(this.columns, this.rows, this.groups, this.grouped);\n if (!this.caption?.trim()) issues.unshift('A non-empty caption is required.');\n if (this.grouping && !this.columns.some(column => column.id === this.grouping!.columnId)) {\n issues.push(`Grouping references unknown column id: ${this.grouping.columnId}`);\n }\n if (\n this.sort &&\n !this.columns.some(column =>\n column.id === this.sort!.columnId ||\n column.headerSegments?.some(segment => segment.sortKey === this.sort!.columnId),\n )\n ) {\n issues.push(`Sorting references unknown column id: ${this.sort.columnId}`);\n }\n const message = issues.join(' ');\n if (!message || message === this.previousModelWarning) return;\n this.previousModelWarning = message;\n console.warn(`[ds-table] ${message}`);\n }\n\n private connectViewportObserver(): void {\n if (!this.viewportEl) return;\n this.viewportEl.removeEventListener('scroll', this.syncOverflow);\n this.viewportEl.addEventListener('scroll', this.syncOverflow, { passive: true });\n\n this.resizeObserver?.disconnect();\n if (typeof ResizeObserver === 'undefined') return;\n this.resizeObserver = new ResizeObserver(this.syncOverflow);\n this.resizeObserver.observe(this.viewportEl);\n if (this.tableEl) this.resizeObserver.observe(this.tableEl);\n }\n\n private syncOverflow = (): void => {\n const viewport = this.viewportEl;\n if (!viewport) return;\n const overflows = viewport.scrollWidth - viewport.clientWidth > 1;\n const scrollable = overflows || viewport.scrollHeight - viewport.clientHeight > 1;\n const nextStart = overflows && viewport.scrollLeft > 1;\n const nextEnd = overflows && viewport.scrollLeft + viewport.clientWidth < viewport.scrollWidth - 1;\n if (nextStart !== this.overflowStart) this.overflowStart = nextStart;\n if (nextEnd !== this.overflowEnd) this.overflowEnd = nextEnd;\n if (scrollable !== this.scrollable) this.scrollable = scrollable;\n };\n\n private resetLoadRequest(): void {\n this.requestPending = false;\n this.requestedRowCount = this.loadedRows.length;\n this.disconnectIntersectionObserver();\n }\n\n private disconnectIntersectionObserver(): void {\n this.intersectionObserver?.disconnect();\n this.intersectionObserver = null;\n this.observedSentinel = null;\n }\n\n private connectIntersectionObserver(): void {\n if (\n !this.lazyLoading ||\n this.loadMoreMode !== 'auto' ||\n !this.hasMore ||\n !!this.loadMoreError?.trim() ||\n !this.intersectionSupported ||\n !this.viewportEl ||\n !this.sentinelEl\n ) {\n this.disconnectIntersectionObserver();\n return;\n }\n\n if (this.intersectionObserver && this.observedSentinel === this.sentinelEl) return;\n this.disconnectIntersectionObserver();\n\n const threshold = Number.isFinite(this.loadMoreThreshold)\n ? Math.max(0, this.loadMoreThreshold)\n : 0;\n this.intersectionObserver = new IntersectionObserver(\n entries => {\n if (entries.some(entry => entry.isIntersecting)) this.requestLoadMore('auto');\n },\n {\n root: this.viewportEl,\n rootMargin: `0px 0px ${threshold}px 0px`,\n },\n );\n this.observedSentinel = this.sentinelEl;\n this.intersectionObserver.observe(this.sentinelEl);\n }\n\n private requestLoadMore(reason: TableLoadMoreReason): void {\n if (\n !this.lazyLoading ||\n !this.hasMore ||\n this.loadingMore ||\n (reason !== 'retry' && !!this.loadMoreError?.trim()) ||\n this.requestPending\n ) return;\n\n this.requestPending = true;\n this.requestedRowCount = this.loadedRows.length;\n this.announcement = this.loadingMoreLabel;\n this.dsLoadMore.emit({\n reason,\n loadIdentity: this.loadIdentity,\n loadedRowCount: this.requestedRowCount,\n });\n }\n\n private emitSort(column: TableColumn, sortKey = column.id): void {\n if (this.grouping?.columnId === column.id && sortKey === column.id) {\n this.dsGroupingChange.emit({ grouping: nextTableGroupOrder(this.grouping) });\n return;\n }\n if (!column.sortable) return;\n this.dsSortChange.emit({ sort: nextTableSortState(this.sort, sortKey) });\n }\n\n private sortButtonLabel(column: TableColumn, sortKey = column.id, label = column.header): string {\n if (this.grouping?.columnId === column.id && sortKey === column.id) {\n const next = this.grouping.direction === 'asc' ? 'descending' : 'ascending';\n return `Sort ${column.header} groups ${next}. Currently grouped ${this.grouping.direction === 'asc' ? 'ascending' : 'descending'}.`;\n }\n\n if (this.sort?.columnId !== sortKey) return `Sort ${label} ascending`;\n if (this.sort.direction === 'asc') return `Sort ${label} descending. Currently ascending.`;\n return `Sort ${label} ascending. Currently descending.`;\n }\n\n private emitRowSelection(row: TableRow): void {\n const next = toggleTableRowSelection(this.selectedRowIds, row);\n const selected = next.includes(row.id);\n this.dsSelectionChange.emit({\n selectedRowIds: next,\n scope: 'row',\n changedRowId: row.id,\n selected,\n });\n }\n\n private emitAllSelection(): void {\n this.dsSelectionChange.emit({\n selectedRowIds: toggleAllLoadedTableRows(this.selectedRowIds, this.loadedRows),\n scope: 'all-loaded',\n selected: !this.selectionState.allSelected,\n });\n }\n\n private renderSelectionControl(\n label: string,\n checked: boolean,\n indeterminate: boolean,\n disabled: boolean,\n onActivate: () => void,\n ) {\n return (\n <button\n class=\"ds-table__selection-control\"\n type=\"button\"\n role=\"checkbox\"\n aria-label={label}\n aria-checked={indeterminate ? 'mixed' : String(checked)}\n disabled={disabled}\n onClick={onActivate}\n >\n <ds-checkbox\n label=\"\"\n size=\"md\"\n checked={checked}\n indeterminate={indeterminate}\n presentation={true}\n />\n </button>\n );\n }\n\n private renderColumnHeader(column: TableColumn) {\n const groupedColumn = this.grouping?.columnId === column.id;\n const headerSegments = column.headerSegments?.length\n ? column.headerSegments\n : [{ label: column.header, sortKey: column.id }];\n const activeMemberSegment = !groupedColumn\n ? headerSegments.find(segment => segment.sortKey === this.sort?.columnId)\n : undefined;\n const activeMemberSort = !!activeMemberSegment;\n const activeSort = groupedColumn || activeMemberSort;\n const align = column.align ?? 'start';\n const direction = groupedColumn\n ? this.grouping!.direction\n : activeMemberSort\n ? this.sort!.direction\n : undefined;\n const memberAriaSort = activeMemberSort\n ? (this.sort!.direction === 'asc' ? 'ascending' : 'descending')\n : undefined;\n\n const labelControl = (\n <span class=\"ds-table__header-labels\">\n {headerSegments.map((segment, index) => {\n const segmentActive = groupedColumn\n ? headerSegments.length === 1\n : activeMemberSegment?.sortKey === segment.sortKey;\n const segmentInteractive = !!column.sortable || (groupedColumn && segment.sortKey === column.id);\n const label = (\n <ds-text\n class=\"ds-table__header-label-box ds-control-label-box\"\n as=\"span\"\n variant=\"text-caption\"\n emphasis={segmentActive}\n color=\"inherit\"\n lineTruncation={1}\n >\n {segment.label}\n </ds-text>\n );\n const control = segmentInteractive ? (\n <button\n class=\"ds-table__header-label ds-table__header-label--interactive\"\n type=\"button\"\n aria-label={this.sortButtonLabel(column, segment.sortKey, segment.label)}\n data-sort-control=\"label\"\n data-sort-key={segment.sortKey}\n data-sort-active={segmentActive ? 'true' : undefined}\n onClick={() => this.emitSort(column, segment.sortKey)}\n >\n {label}\n </button>\n ) : (\n <span class=\"ds-table__header-label ds-table__header-static\">{label}</span>\n );\n\n return (\n <span class=\"ds-table__header-segment\" key={segment.sortKey}>\n {control}\n {index < headerSegments.length - 1 && (\n <ds-text\n class=\"ds-table__header-separator\"\n as=\"span\"\n variant=\"text-caption\"\n color=\"tertiary\"\n aria-hidden=\"true\"\n >\n {segment.separator ?? '/'}\n </ds-text>\n )}\n </span>\n );\n })}\n </span>\n );\n const sortControl = (\n <span class=\"ds-table__sort-slot\">\n {activeSort && (\n <ds-button-unfilled\n class=\"ds-table__sort-direction\"\n variant=\"icon\"\n icon={direction === 'asc' ? 'ArrowUp' : 'ArrowDown'}\n size=\"xs\"\n aria-label={this.sortButtonLabel(\n column,\n activeMemberSegment?.sortKey ?? column.id,\n activeMemberSegment?.label ?? column.header,\n )}\n hasBorder={false}\n activeFill={false}\n isActive={true}\n pressScale={false}\n data-sort-control=\"direction\"\n onDsClick={() => this.emitSort(column, activeMemberSegment?.sortKey ?? column.id)}\n />\n )}\n </span>\n );\n\n return (\n <th\n key={column.id}\n class={`ds-table__header-cell ds-table__cell--align-${align}`}\n scope=\"col\"\n aria-sort={memberAriaSort}\n data-column-id={column.id}\n data-grouped={groupedColumn ? 'true' : undefined}\n data-sort-active={activeSort ? 'true' : undefined}\n >\n <span class=\"ds-table__header-content\">\n {align === 'end' && sortControl}\n {align === 'center' && (\n <span\n class=\"ds-table__sort-slot ds-table__sort-slot--balance\"\n data-sort-balance=\"true\"\n aria-hidden=\"true\"\n />\n )}\n {labelControl}\n {align !== 'end' && sortControl}\n </span>\n </th>\n );\n }\n\n private renderCellValue(value: TableCellValue, column: TableColumn, row: TableRow) {\n if (isTableCellBlank(value)) return null;\n\n if (value == null || isTableCellEmpty(value)) {\n return (\n <ds-text\n class=\"ds-table__cell-track ds-table__cell-track--text\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n >\n <span aria-hidden=\"true\">—</span>\n <span class=\"ds-visually-hidden\">{this.emptyCellLabel}</span>\n </ds-text>\n );\n }\n\n if (isTableCellIcon(value)) {\n return (\n <ds-icon\n name={value.icon}\n size=\"md\"\n color={value.color ?? 'secondary'}\n label={value.label}\n />\n );\n }\n\n if (isTableCellImage(value)) {\n return (\n <span class=\"ds-table__cell-image\">\n {value.src ? (\n <img class=\"ds-table__cell-image-content\" src={value.src} alt={value.alt} loading=\"lazy\" />\n ) : (\n <span\n class=\"ds-table__cell-image-placeholder\"\n role=\"img\"\n aria-label={value.alt}\n />\n )}\n </span>\n );\n }\n\n if (isTableCellAction(value)) {\n return (\n <ds-button-unfilled\n variant={value.variant ?? 'label'}\n size=\"sm\"\n label={value.label ?? ''}\n icon={value.icon ?? ''}\n aria-label={value.ariaLabel ?? null}\n hasBorder={value.hasBorder ?? false}\n isInactive={value.isInactive ?? false}\n isLoading={value.isLoading ?? false}\n onDsClick={event => {\n event.stopPropagation();\n this.dsCellAction.emit({\n actionId: value.actionId,\n rowId: row.id,\n columnId: column.id,\n });\n }}\n />\n );\n }\n\n if (isTableCellTag(value)) {\n const variant = value.variant ?? 'tag-only';\n const tag = (\n <ds-tag\n label={value.label}\n intent={value.intent ?? 'neutral'}\n contrast={value.contrast ?? 'faint'}\n size={variant === 'text-with-tag' ? 'sm' : 'md'}\n icon={value.icon ?? ''}\n rounded={value.rounded ?? false}\n isInset\n />\n );\n\n if (variant === 'tag-only') return tag;\n\n return (\n <span class={`ds-table__cell-tag-stack ds-table__cell-tag-stack--${variant}`}>\n {variant === 'tag-with-text' && tag}\n <ds-text\n class=\"ds-table__cell-tag-text ds-table__cell-track\"\n as=\"span\"\n variant={variant === 'tag-with-text' ? 'text-body-small' : 'text-body-medium'}\n color=\"secondary\"\n lineTruncation={1}\n >\n {value.text}\n </ds-text>\n {variant === 'text-with-tag' && (\n <span class=\"ds-table__cell-tag-control-track\">{tag}</span>\n )}\n </span>\n );\n }\n\n const primaryText = isTableCellPrimaryText(value);\n const text = isTableCellText(value) || primaryText\n ? value\n : { primary: value, fontFeature: typeof value === 'number' ? 'tabular-nums' as const : 'normal' as const };\n const wraps = text.wrap ?? column.wrap ?? false;\n\n return (\n <span class={{ 'ds-table__cell-copy': true, 'ds-table__cell-copy--wrap': wraps }}>\n <ds-text\n class=\"ds-table__cell-primary ds-table__cell-track ds-table__cell-track--text\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"primary\"\n lineTruncation={wraps ? 'none' : 1}\n wrap={wraps ? 'wrap' : 'nowrap'}\n fontFeature={text.fontFeature ?? 'normal'}\n >\n {text.primary}\n </ds-text>\n {text.secondary !== undefined && text.secondary !== '' && (\n <ds-text\n class=\"ds-table__cell-secondary ds-table__cell-track ds-table__cell-track--text\"\n as=\"span\"\n variant={primaryText ? 'text-body-medium' : 'text-body-small'}\n color={primaryText ? 'primary' : 'secondary'}\n lineTruncation={wraps ? 'none' : 1}\n wrap={wraps ? 'wrap' : 'nowrap'}\n >\n {text.secondary}\n </ds-text>\n )}\n </span>\n );\n }\n\n private renderRow(row: TableRow) {\n const selected = this.selectedSet.has(row.id);\n const rowSelectable = row.selectable !== false && !row.disabled;\n return (\n <tr\n key={row.id}\n class={{\n 'ds-table__row': true,\n 'ds-table__row--selected': selected,\n 'ds-table__row--disabled': !!row.disabled,\n }}\n data-row-id={row.id}\n data-selected={selected ? 'true' : undefined}\n aria-disabled={row.disabled ? 'true' : undefined}\n >\n {this.selectable && (\n <td class=\"ds-table__cell ds-table__selection-cell\">\n {this.renderSelectionControl(\n `${selected ? 'Deselect' : 'Select'} ${tableRowSelectionLabel(row, this.columns)}`,\n selected,\n false,\n !rowSelectable,\n () => this.emitRowSelection(row),\n )}\n </td>\n )}\n {this.columns.map(column => {\n const align = column.align ?? 'start';\n const value = row.cells[column.id];\n const tagCell = isTableCellTag(value);\n const iconCell = isTableCellIcon(value);\n const imageCell = isTableCellImage(value);\n const actionCell = isTableCellAction(value);\n const primaryTextCell = isTableCellPrimaryText(value);\n const emptyCell = value == null || isTableCellEmpty(value);\n const blankCell = isTableCellBlank(value);\n const textCell = primaryTextCell || (!tagCell && !iconCell && !imageCell && !actionCell && !emptyCell && !blankCell);\n const singleTextCell = textCell && !primaryTextCell && (!isTableCellText(value) || !value.secondary);\n const tagVariant = tagCell ? value.variant ?? 'tag-only' : undefined;\n const textVariant = textCell\n ? primaryTextCell\n ? 'primary-pair'\n : singleTextCell\n ? 'single'\n : 'multi'\n : undefined;\n const cellType = tagCell\n ? 'tag'\n : iconCell\n ? 'icon'\n : imageCell\n ? 'image'\n : actionCell\n ? 'action'\n : primaryTextCell\n ? 'primary-text'\n : emptyCell\n ? 'empty'\n : blankCell\n ? 'blank'\n : 'text';\n return (\n <td\n key={`${row.id}:${column.id}`}\n class={{\n 'ds-table__cell': true,\n [`ds-table__cell--align-${align}`]: true,\n 'ds-table__cell--tag': tagCell,\n [`ds-table__cell--tag-${tagVariant}`]: tagCell,\n 'ds-table__cell--icon': iconCell,\n 'ds-table__cell--image': imageCell,\n 'ds-table__cell--action': actionCell,\n 'ds-table__cell--primary-text': primaryTextCell,\n 'ds-table__cell--text-single': singleTextCell,\n 'ds-table__cell--text-multi': textCell && !singleTextCell,\n 'ds-table__cell--empty': emptyCell,\n 'ds-table__cell--blank': blankCell,\n }}\n data-column-id={column.id}\n data-cell-type={cellType}\n data-cell-variant={tagVariant ?? textVariant}\n >\n <span class=\"ds-table__cell-content\">\n {this.renderCellValue(value, column, row)}\n </span>\n </td>\n );\n })}\n </tr>\n );\n }\n\n private renderDataBodies() {\n if (!this.grouped) {\n return <tbody class=\"ds-table__body\">{this.rows.map(row => this.renderRow(row))}</tbody>;\n }\n\n return this.groups.map(group => {\n const count = resolvedTableGroupCount(group);\n const countLabel = group.countLabel ?? `${count} ${count === 1 ? 'item' : 'items'}`;\n return (\n <tbody class=\"ds-table__body ds-table__group\" data-group-id={group.id} key={group.id}>\n <tr class=\"ds-table__group-row\">\n <th class=\"ds-table__group-cell\" scope=\"rowgroup\" colSpan={this.totalColumns}>\n <span class=\"ds-table__group-content\">\n <ds-icon name=\"GroupBy\" size=\"sm\" color=\"secondary\" aria-hidden=\"true\" />\n <ds-text as=\"span\" variant=\"text-body-small\" emphasis={true} color=\"primary\">\n {group.label}\n </ds-text>\n <ds-text as=\"span\" variant=\"text-body-small\" color=\"secondary\">\n {countLabel}\n </ds-text>\n </span>\n </th>\n </tr>\n {group.rows.map(row => this.renderRow(row))}\n </tbody>\n );\n });\n }\n\n private renderSkeletonBody() {\n const count = Math.min(20, Math.max(1, Math.round(this.skeletonRows) || 1));\n return (\n <tbody class=\"ds-table__body ds-table__skeleton-body\">\n {Array.from({ length: count }, (_, index) => (\n <tr class=\"ds-table__row ds-table__skeleton-row\" key={`skeleton-${index}`}>\n {this.selectable && (\n <td class=\"ds-table__cell ds-table__selection-cell\">\n <ds-skeleton variant=\"icon\" iconSize=\"md\" />\n </td>\n )}\n {this.columns.map(column => (\n <td class=\"ds-table__cell\" key={`skeleton-${index}:${column.id}`}>\n <span class=\"ds-table__cell-content\">\n <ds-skeleton variant=\"text\" textVariant=\"text-body-medium\" width=\"100%\" />\n </span>\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n );\n }\n\n private renderStateBody(kind: 'empty' | 'error') {\n const error = kind === 'error';\n return (\n <tbody class=\"ds-table__body ds-table__state-body\">\n <tr class=\"ds-table__state-row\">\n <td class=\"ds-table__state-cell\" colSpan={this.totalColumns}>\n <ds-empty-state\n icon={error ? 'ErrorTriangle' : 'Inbox'}\n heading={error ? this.errorHeading : this.emptyHeading}\n body={error ? this.errorBody : this.emptyBody}\n />\n </td>\n </tr>\n </tbody>\n );\n }\n\n private renderLazyBody() {\n if (!this.lazyLoading || !this.hasData) return null;\n const error = this.loadMoreError?.trim();\n const manualFallback = this.loadMoreMode === 'manual' || !this.intersectionSupported;\n\n return (\n <tbody class=\"ds-table__body ds-table__load-body\">\n <tr\n class=\"ds-table__load-row\"\n ref={element => {\n this.sentinelEl = element ?? null;\n }}\n >\n <td class=\"ds-table__load-cell\" colSpan={this.totalColumns}>\n {error ? (\n <span class=\"ds-table__load-content ds-table__load-content--error\">\n <ds-icon name=\"ErrorTriangle\" size=\"sm\" color=\"negative\" aria-hidden=\"true\" />\n <ds-text as=\"span\" variant=\"text-body-small\" color=\"secondary\">{error}</ds-text>\n <ds-button-unfilled\n label={this.retryLabel}\n size=\"sm\"\n onDsClick={() => this.requestLoadMore('retry')}\n />\n </span>\n ) : this.loadingMore ? (\n <span class=\"ds-table__load-content\">\n <ds-loader size=\"sm\" color=\"secondary\" />\n <ds-text as=\"span\" variant=\"text-body-small\" color=\"secondary\">\n {this.loadingMoreLabel}\n </ds-text>\n </span>\n ) : this.hasMore && manualFallback ? (\n <ds-button-unfilled\n label={this.loadMoreLabel}\n size=\"sm\"\n onDsClick={() => this.requestLoadMore('manual')}\n />\n ) : this.hasMore ? (\n <span class=\"ds-table__auto-sentinel\" aria-hidden=\"true\" />\n ) : (\n <ds-text as=\"span\" variant=\"text-body-small\" color=\"secondary\">\n {this.endOfResultsLabel}\n </ds-text>\n )}\n </td>\n </tr>\n </tbody>\n );\n }\n\n render() {\n const selection = this.selectionState;\n const regionLabel = this.scrollLabel?.trim() || `${this.caption} scroll area`;\n const initialLoading = this.loading && !this.hasData;\n const initialError = !this.hasData && this.error;\n const viewportStyle = this.resolvedMaxHeight\n ? { '--ds-table-max-block-size': this.resolvedMaxHeight }\n : undefined;\n\n return (\n <Host class=\"table-host\">\n <div class={{\n 'ds-table': true,\n 'ds-table--sticky-header': this.stickyHeader,\n }}>\n <div\n class={{\n 'ds-table__frame': true,\n 'ds-table__frame--overflow-start': this.overflowStart,\n 'ds-table__frame--overflow-end': this.overflowEnd,\n }}\n >\n <div\n class=\"ds-table__viewport\"\n style={viewportStyle}\n ref={element => {\n this.viewportEl = element ?? null;\n }}\n role={this.scrollable ? 'region' : undefined}\n aria-label={this.scrollable ? regionLabel : undefined}\n tabIndex={this.scrollable ? 0 : undefined}\n >\n <table\n class={{\n 'ds-table__table': true,\n 'ds-table__table--selectable': this.selectable,\n }}\n style={this.tableStyle}\n aria-busy={initialLoading || this.loadingMore ? 'true' : undefined}\n ref={element => {\n this.tableEl = element ?? null;\n }}\n >\n <caption\n class={{\n 'ds-table__caption': true,\n 'ds-visually-hidden': this.captionVisibility === 'hidden',\n }}\n >\n <ds-text as=\"span\" variant=\"text-title-small\" emphasis={true} color=\"primary\">\n {this.caption}\n </ds-text>\n </caption>\n <colgroup>\n {this.selectable && <col class=\"ds-table__selection-column\" />}\n {this.columns.map(column => {\n const width = tableColumnSize(column);\n return <col key={column.id} style={width ? { width } : undefined} />;\n })}\n </colgroup>\n <thead class=\"ds-table__head\">\n <tr class=\"ds-table__header-row\">\n {this.selectable && (\n <th class=\"ds-table__header-cell ds-table__selection-cell\" scope=\"col\">\n {this.renderSelectionControl(\n selection.allSelected ? 'Deselect all loaded rows' : 'Select all loaded rows',\n selection.allSelected,\n selection.indeterminate,\n selection.selectableRowIds.length === 0,\n () => this.emitAllSelection(),\n )}\n </th>\n )}\n {this.columns.map(column => this.renderColumnHeader(column))}\n </tr>\n </thead>\n {initialLoading\n ? this.renderSkeletonBody()\n : initialError\n ? this.renderStateBody('error')\n : this.hasData\n ? this.renderDataBodies()\n : this.renderStateBody('empty')}\n {!initialLoading && !initialError && this.renderLazyBody()}\n </table>\n </div>\n </div>\n <div class=\"ds-visually-hidden\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n {this.announcement}\n </div>\n </div>\n </Host>\n );\n }\n}\n",
763
+ "content": "import {\n Component,\n Event,\n EventEmitter,\n h,\n Host,\n Prop,\n State,\n Watch,\n} from '@stencil/core';\nimport {\n deriveTableSelectionState,\n isTableCellAction,\n isTableCellBlank,\n isTableCellEmpty,\n isTableCellIcon,\n isTableCellImage,\n isTableCellPrimaryText,\n isTableCellText,\n isTableCellTag,\n nextTableGroupOrder,\n nextTableSortState,\n resolvedTableGroupCount,\n tableColumnSize,\n tableExplicitMinWidth,\n tableModelIssues,\n tableRows,\n tableRowSelectionLabel,\n toggleAllLoadedTableRows,\n toggleTableRowSelection,\n} from './table-model';\nimport type {\n TableCaptionVisibility,\n TableCellActionDetail,\n TableCellValue,\n TableColumn,\n TableGroup,\n TableGroupingChangeDetail,\n TableGroupingState,\n TableLoadMoreDetail,\n TableLoadMoreMode,\n TableLoadMoreReason,\n TableRow,\n TableRowActivateDetail,\n TableSelectionChangeDetail,\n TableSelectionMode,\n TableSortChangeDetail,\n TableSortState,\n} from './table-types';\n\n@Component({\n tag: 'ds-table',\n styleUrls: [\n '../../utils/interaction-fill.css',\n 'Table.css',\n ],\n scoped: true,\n})\nexport class Table {\n /** Stable column definitions. Assign through JavaScript. */\n @Prop() columns: TableColumn[] = [];\n /** Ungrouped row data. Ignored while grouping is active. Assign through JavaScript. */\n @Prop() rows: TableRow[] = [];\n /** One level of application-owned grouped data. Assign through JavaScript. */\n @Prop() groups: TableGroup[] = [];\n /** Controlled grouping column and group-order direction. */\n @Prop() grouping: TableGroupingState | null = null;\n /** Controlled member-row sort state. */\n @Prop() sort: TableSortState | null = null;\n\n /** Required accessible table name, rendered as a native caption. */\n @Prop() caption!: string;\n @Prop() captionVisibility: TableCaptionVisibility = 'hidden';\n @Prop() stickyHeader: boolean = false;\n /** Maximum scroll-region height. Numbers resolve to CSS pixels. */\n @Prop() maxHeight: string | number | undefined;\n /** Optional explicit label for the horizontal/vertical scroll region. */\n @Prop() scrollLabel: string | undefined;\n\n @Prop() selectionMode: TableSelectionMode = 'none';\n /** Controlled selected row identities. IDs outside the loaded rows are preserved. */\n @Prop() selectedRowIds: string[] = [];\n\n /** Initial loading state. Existing rows stay visible; incremental loading uses loadingMore. */\n @Prop() loading: boolean = false;\n @Prop() skeletonRows: number = 5;\n @Prop() emptyHeading: string = 'No results';\n @Prop() emptyBody: string = 'No data is available.';\n /** Initial error state. Existing rows stay visible; incremental failures use loadMoreError. */\n @Prop() error: boolean = false;\n @Prop() errorHeading: string = 'Unable to load data';\n @Prop() errorBody: string = 'The data could not be loaded.';\n @Prop() emptyCellLabel: string = 'Not available';\n\n /** Enable application-owned incremental loading without pagination. */\n @Prop() lazyLoading: boolean = false;\n @Prop() loadMoreMode: TableLoadMoreMode = 'auto';\n @Prop() hasMore: boolean = false;\n @Prop() loadingMore: boolean = false;\n @Prop() loadMoreError: string | undefined;\n /** Reset key for a new query/group/sort dataset. */\n @Prop() loadIdentity: string | number = 'default';\n /** IntersectionObserver root margin in CSS pixels. */\n @Prop() loadMoreThreshold: number = 0;\n @Prop() loadMoreLabel: string = 'Load more';\n @Prop() retryLabel: string = 'Retry';\n @Prop() loadingMoreLabel: string = 'Loading more results';\n @Prop() endOfResultsLabel: string = 'All results loaded';\n /** Supports {count} and {total} placeholders. */\n @Prop() rowsLoadedLabel: string = '{count} more rows loaded. {total} rows loaded.';\n\n @Event() dsSortChange!: EventEmitter<TableSortChangeDetail>;\n @Event() dsGroupingChange!: EventEmitter<TableGroupingChangeDetail>;\n @Event() dsSelectionChange!: EventEmitter<TableSelectionChangeDetail>;\n @Event() dsLoadMore!: EventEmitter<TableLoadMoreDetail>;\n @Event() dsCellAction!: EventEmitter<TableCellActionDetail>;\n @Event() dsRowActivate!: EventEmitter<TableRowActivateDetail>;\n\n @State() private overflowStart = false;\n @State() private overflowEnd = false;\n @State() private scrollable = false;\n @State() private intersectionSupported = true;\n @State() private announcement = '';\n\n private viewportEl: HTMLElement | null = null;\n private stickyHeaderTableEl: HTMLTableElement | null = null;\n private tableEl: HTMLTableElement | null = null;\n private sentinelEl: HTMLElement | null = null;\n private resizeObserver: ResizeObserver | null = null;\n private intersectionObserver: IntersectionObserver | null = null;\n private observedSentinel: HTMLElement | null = null;\n private requestPending = false;\n private requestedRowCount = 0;\n private previousLoadedRowCount = 0;\n private previousModelWarning = '';\n private hasLoaded = false;\n private reconnectFrame: number | null = null;\n\n componentWillLoad(): void {\n this.previousLoadedRowCount = this.loadedRows.length;\n this.intersectionSupported = typeof IntersectionObserver !== 'undefined';\n this.warnModelIssues();\n }\n\n componentDidLoad(): void {\n this.hasLoaded = true;\n this.connectViewportObserver();\n this.syncOverflow();\n this.connectIntersectionObserver();\n }\n\n componentDidRender(): void {\n this.syncStickyHeaderPosition();\n this.connectIntersectionObserver();\n }\n\n connectedCallback(): void {\n if (!this.hasLoaded) return;\n if (this.reconnectFrame !== null) cancelAnimationFrame(this.reconnectFrame);\n this.reconnectFrame = requestAnimationFrame(() => {\n this.reconnectFrame = null;\n this.connectViewportObserver();\n this.syncOverflow();\n this.connectIntersectionObserver();\n });\n }\n\n disconnectedCallback(): void {\n if (this.reconnectFrame !== null) cancelAnimationFrame(this.reconnectFrame);\n this.reconnectFrame = null;\n this.viewportEl?.removeEventListener('scroll', this.syncOverflow);\n this.resizeObserver?.disconnect();\n this.resizeObserver = null;\n this.disconnectIntersectionObserver();\n }\n\n @Watch('columns')\n @Watch('grouping')\n handleStructureChange(): void {\n this.warnModelIssues();\n this.resetLoadRequest();\n }\n\n @Watch('rows')\n @Watch('groups')\n handleDataChange(): void {\n const nextCount = this.loadedRows.length;\n if (nextCount > this.previousLoadedRowCount) {\n const added = nextCount - this.previousLoadedRowCount;\n this.announcement = this.rowsLoadedLabel\n .replace('{count}', String(added))\n .replace('{total}', String(nextCount));\n }\n if (nextCount > this.requestedRowCount) this.requestPending = false;\n this.previousLoadedRowCount = nextCount;\n this.warnModelIssues();\n this.disconnectIntersectionObserver();\n }\n\n @Watch('loadIdentity')\n handleLoadIdentityChange(): void {\n this.previousLoadedRowCount = this.loadedRows.length;\n this.resetLoadRequest();\n }\n\n @Watch('lazyLoading')\n @Watch('loadMoreMode')\n @Watch('loadMoreThreshold')\n handleLazyConfigurationChange(): void {\n this.disconnectIntersectionObserver();\n }\n\n @Watch('loadingMore')\n handleLoadingMoreChange(loading: boolean): void {\n if (loading) {\n this.requestPending = true;\n this.announcement = this.loadingMoreLabel;\n } else if (this.loadMoreMode === 'manual') {\n // A completed manual request may legitimately append no rows. Permit a\n // deliberate follow-up activation; automatic mode stays guarded until\n // the data shape or query identity changes.\n this.requestPending = false;\n }\n this.disconnectIntersectionObserver();\n }\n\n @Watch('loadMoreError')\n handleLoadMoreErrorChange(error: string | undefined): void {\n if (error?.trim()) {\n this.requestPending = false;\n this.announcement = error;\n }\n this.disconnectIntersectionObserver();\n }\n\n @Watch('hasMore')\n handleHasMoreChange(hasMore: boolean, hadMore: boolean): void {\n if (!hasMore) {\n this.requestPending = false;\n if (hadMore) this.announcement = this.endOfResultsLabel;\n }\n this.disconnectIntersectionObserver();\n }\n\n private get grouped(): boolean {\n return this.grouping !== null;\n }\n\n private get selectable(): boolean {\n return this.selectionMode === 'multiple';\n }\n\n private get documentStickyHeader(): boolean {\n return this.stickyHeader && this.resolvedMaxHeight === undefined;\n }\n\n private get loadedRows(): TableRow[] {\n return tableRows(this.rows, this.groups, this.grouped);\n }\n\n private get hasData(): boolean {\n return this.loadedRows.length > 0;\n }\n\n private get totalColumns(): number {\n return this.columns.length + (this.selectable ? 1 : 0);\n }\n\n private get selectedSet(): Set<string> {\n return new Set(this.selectedRowIds);\n }\n\n private get selectionState() {\n return deriveTableSelectionState(this.loadedRows, this.selectedRowIds);\n }\n\n private get resolvedMaxHeight(): string | undefined {\n if (this.maxHeight == null || this.maxHeight === '') return undefined;\n return typeof this.maxHeight === 'number' ? `${Math.max(0, this.maxHeight)}px` : this.maxHeight;\n }\n\n private get tableStyle(): Record<string, string> | undefined {\n const width = tableExplicitMinWidth(this.columns);\n return width == null ? undefined : { '--ds-table-explicit-min-inline-size': width };\n }\n\n private warnModelIssues(): void {\n const issues = tableModelIssues(this.columns, this.rows, this.groups, this.grouped);\n if (!this.caption?.trim()) issues.unshift('A non-empty caption is required.');\n if (this.grouping && !this.columns.some(column => column.id === this.grouping!.columnId)) {\n issues.push(`Grouping references unknown column id: ${this.grouping.columnId}`);\n }\n if (\n this.sort &&\n !this.columns.some(column =>\n column.id === this.sort!.columnId ||\n column.headerSegments?.some(segment => segment.sortKey === this.sort!.columnId),\n )\n ) {\n issues.push(`Sorting references unknown column id: ${this.sort.columnId}`);\n }\n const stickyStart = this.columns.filter(column => column.sticky === 'start');\n const stickyEnd = this.columns.filter(column => column.sticky === 'end');\n if (stickyStart.length > 1 || (this.selectable && stickyStart.length > 0)) {\n issues.push('Only one sticky start column is supported, and row selection already owns that lane.');\n }\n if (stickyEnd.length > 1) issues.push('Only one sticky end column is supported.');\n for (const column of [...stickyStart, ...stickyEnd]) {\n if (!tableColumnSize(column)) issues.push(`Sticky column ${column.id} requires an explicit size.`);\n }\n const message = issues.join(' ');\n if (!message || message === this.previousModelWarning) return;\n this.previousModelWarning = message;\n console.warn(`[ds-table] ${message}`);\n }\n\n private connectViewportObserver(): void {\n if (!this.viewportEl) return;\n this.viewportEl.removeEventListener('scroll', this.syncOverflow);\n this.viewportEl.addEventListener('scroll', this.syncOverflow, { passive: true });\n\n this.resizeObserver?.disconnect();\n if (typeof ResizeObserver === 'undefined') return;\n this.resizeObserver = new ResizeObserver(this.syncOverflow);\n this.resizeObserver.observe(this.viewportEl);\n if (this.tableEl) this.resizeObserver.observe(this.tableEl);\n }\n\n private syncOverflow = (): void => {\n const viewport = this.viewportEl;\n if (!viewport) return;\n const overflows = viewport.scrollWidth - viewport.clientWidth > 1;\n const scrollable = overflows || viewport.scrollHeight - viewport.clientHeight > 1;\n const nextStart = overflows && viewport.scrollLeft > 1;\n const nextEnd = overflows && viewport.scrollLeft + viewport.clientWidth < viewport.scrollWidth - 1;\n if (nextStart !== this.overflowStart) this.overflowStart = nextStart;\n if (nextEnd !== this.overflowEnd) this.overflowEnd = nextEnd;\n if (scrollable !== this.scrollable) this.scrollable = scrollable;\n this.syncStickyHeaderPosition();\n };\n\n private syncStickyHeaderPosition(): void {\n if (!this.viewportEl || !this.stickyHeaderTableEl) return;\n const offset = this.viewportEl.scrollLeft;\n const maxOffset = Math.max(0, this.viewportEl.scrollWidth - this.viewportEl.clientWidth);\n this.stickyHeaderTableEl.style.setProperty('--ds-table-inline-scroll-offset', `${offset}px`);\n this.stickyHeaderTableEl.style.setProperty('--ds-table-inline-scroll-max-offset', `${maxOffset}px`);\n }\n\n private resetLoadRequest(): void {\n this.requestPending = false;\n this.requestedRowCount = this.loadedRows.length;\n this.disconnectIntersectionObserver();\n }\n\n private disconnectIntersectionObserver(): void {\n this.intersectionObserver?.disconnect();\n this.intersectionObserver = null;\n this.observedSentinel = null;\n }\n\n private connectIntersectionObserver(): void {\n if (\n !this.lazyLoading ||\n this.loadMoreMode !== 'auto' ||\n !this.hasMore ||\n !!this.loadMoreError?.trim() ||\n !this.intersectionSupported ||\n !this.viewportEl ||\n !this.sentinelEl\n ) {\n this.disconnectIntersectionObserver();\n return;\n }\n\n if (this.intersectionObserver && this.observedSentinel === this.sentinelEl) return;\n this.disconnectIntersectionObserver();\n\n const threshold = Number.isFinite(this.loadMoreThreshold)\n ? Math.max(0, this.loadMoreThreshold)\n : 0;\n this.intersectionObserver = new IntersectionObserver(\n entries => {\n if (entries.some(entry => entry.isIntersecting)) this.requestLoadMore('auto');\n },\n {\n root: this.resolvedMaxHeight === undefined ? null : this.viewportEl,\n rootMargin: `0px 0px ${threshold}px 0px`,\n },\n );\n this.observedSentinel = this.sentinelEl;\n this.intersectionObserver.observe(this.sentinelEl);\n }\n\n private requestLoadMore(reason: TableLoadMoreReason): void {\n if (\n !this.lazyLoading ||\n !this.hasMore ||\n this.loadingMore ||\n (reason !== 'retry' && !!this.loadMoreError?.trim()) ||\n this.requestPending\n ) return;\n\n this.requestPending = true;\n this.requestedRowCount = this.loadedRows.length;\n this.announcement = this.loadingMoreLabel;\n this.dsLoadMore.emit({\n reason,\n loadIdentity: this.loadIdentity,\n loadedRowCount: this.requestedRowCount,\n });\n }\n\n private emitSort(column: TableColumn, sortKey = column.id): void {\n if (this.grouping?.columnId === column.id && sortKey === column.id) {\n this.dsGroupingChange.emit({ grouping: nextTableGroupOrder(this.grouping) });\n return;\n }\n if (!column.sortable) return;\n this.dsSortChange.emit({ sort: nextTableSortState(this.sort, sortKey) });\n }\n\n private sortButtonLabel(column: TableColumn, sortKey = column.id, label = column.header): string {\n if (this.grouping?.columnId === column.id && sortKey === column.id) {\n const next = this.grouping.direction === 'asc' ? 'descending' : 'ascending';\n return `Sort ${column.header} groups ${next}. Currently grouped ${this.grouping.direction === 'asc' ? 'ascending' : 'descending'}.`;\n }\n\n if (this.sort?.columnId !== sortKey) return `Sort ${label} ascending`;\n if (this.sort.direction === 'asc') return `Sort ${label} descending. Currently ascending.`;\n return `Sort ${label} ascending. Currently descending.`;\n }\n\n private emitRowSelection(row: TableRow): void {\n const next = toggleTableRowSelection(this.selectedRowIds, row);\n const selected = next.includes(row.id);\n this.dsSelectionChange.emit({\n selectedRowIds: next,\n scope: 'row',\n changedRowId: row.id,\n selected,\n });\n }\n\n private emitAllSelection(): void {\n this.dsSelectionChange.emit({\n selectedRowIds: toggleAllLoadedTableRows(this.selectedRowIds, this.loadedRows),\n scope: 'all-loaded',\n selected: !this.selectionState.allSelected,\n });\n }\n\n private rowEventOwnsActivation(event: Event): boolean {\n const currentTarget = event.currentTarget;\n return !event.composedPath().some(target => {\n if (!(target instanceof HTMLElement) || target === currentTarget) return false;\n return target.matches(\n 'button, a, input, select, textarea, [role=\"button\"], [role=\"checkbox\"], ds-button-unfilled',\n );\n });\n }\n\n private emitRowActivation(row: TableRow, event: Event): void {\n if (!row.interactive || row.disabled || !this.rowEventOwnsActivation(event)) return;\n this.dsRowActivate.emit({ rowId: row.id });\n }\n\n private handleRowKeydown(row: TableRow, event: KeyboardEvent): void {\n if (event.target !== event.currentTarget || (event.key !== 'Enter' && event.key !== ' ')) return;\n event.preventDefault();\n this.emitRowActivation(row, event);\n }\n\n private renderSelectionControl(\n label: string,\n checked: boolean,\n indeterminate: boolean,\n disabled: boolean,\n onActivate: () => void,\n ) {\n return (\n <button\n class=\"ds-table__selection-control ds-interaction-fill__content\"\n type=\"button\"\n role=\"checkbox\"\n aria-label={label}\n aria-checked={indeterminate ? 'mixed' : String(checked)}\n disabled={disabled}\n onClick={onActivate}\n >\n <ds-checkbox\n label=\"\"\n size=\"md\"\n checked={checked}\n indeterminate={indeterminate}\n presentation={true}\n />\n </button>\n );\n }\n\n private renderStickyEdge(sticky: TableColumn['sticky']) {\n if (!sticky) return null;\n return (\n <span\n class={`ds-table__sticky-edge ds-table__sticky-edge--${sticky}`}\n aria-hidden=\"true\"\n />\n );\n }\n\n private renderColumnHeader(\n column: TableColumn,\n interactive = true,\n presentational = false,\n ) {\n const groupedColumn = this.grouping?.columnId === column.id;\n const headerSegments = column.headerSegments?.length\n ? column.headerSegments\n : [{ label: column.header, sortKey: column.id }];\n const activeMemberSegment = !groupedColumn\n ? headerSegments.find(segment => segment.sortKey === this.sort?.columnId)\n : undefined;\n const activeMemberSort = !!activeMemberSegment;\n const activeSort = groupedColumn || activeMemberSort;\n const align = column.align ?? 'start';\n const direction = groupedColumn\n ? this.grouping!.direction\n : activeMemberSort\n ? this.sort!.direction\n : undefined;\n const memberAriaSort = activeMemberSort\n ? (this.sort!.direction === 'asc' ? 'ascending' : 'descending')\n : undefined;\n\n const labelControl = (\n <span class=\"ds-table__header-labels\">\n {headerSegments.map((segment, index) => {\n const segmentActive = groupedColumn\n ? headerSegments.length === 1\n : activeMemberSegment?.sortKey === segment.sortKey;\n const segmentInteractive = interactive && (\n !!column.sortable || (groupedColumn && segment.sortKey === column.id)\n );\n const label = (\n <ds-text\n class=\"ds-table__header-label-box ds-control-label-box\"\n as=\"span\"\n variant=\"text-caption\"\n emphasis={segmentActive}\n color=\"inherit\"\n lineTruncation={1}\n >\n {segment.label}\n </ds-text>\n );\n const control = segmentInteractive ? (\n <button\n class=\"ds-table__header-label ds-table__header-label--interactive\"\n type=\"button\"\n aria-label={this.sortButtonLabel(column, segment.sortKey, segment.label)}\n data-sort-control=\"label\"\n data-sort-key={segment.sortKey}\n data-sort-active={segmentActive ? 'true' : undefined}\n onClick={() => this.emitSort(column, segment.sortKey)}\n >\n {label}\n </button>\n ) : (\n <span class=\"ds-table__header-label ds-table__header-static\">{label}</span>\n );\n\n return (\n <span class=\"ds-table__header-segment\" key={segment.sortKey}>\n {control}\n {index < headerSegments.length - 1 && (\n <ds-text\n class=\"ds-table__header-separator\"\n as=\"span\"\n variant=\"text-caption\"\n color=\"tertiary\"\n aria-hidden=\"true\"\n >\n {segment.separator ?? '/'}\n </ds-text>\n )}\n </span>\n );\n })}\n </span>\n );\n const sortControl = interactive ? (\n <span class=\"ds-table__sort-slot\">\n {activeSort && (\n <ds-button-unfilled\n class=\"ds-table__sort-direction\"\n variant=\"icon\"\n icon={direction === 'asc' ? 'ArrowUp' : 'ArrowDown'}\n size=\"xs\"\n aria-label={this.sortButtonLabel(\n column,\n activeMemberSegment?.sortKey ?? column.id,\n activeMemberSegment?.label ?? column.header,\n )}\n hasBorder={false}\n activeFill={false}\n isActive={true}\n pressScale={false}\n data-sort-control=\"direction\"\n onDsClick={() => this.emitSort(column, activeMemberSegment?.sortKey ?? column.id)}\n />\n )}\n </span>\n ) : null;\n\n return (\n <th\n key={column.id}\n class={{\n 'ds-table__header-cell': true,\n [`ds-table__cell--align-${align}`]: true,\n 'ds-table__cell--sticky-start': column.sticky === 'start',\n 'ds-table__cell--sticky-end': column.sticky === 'end',\n }}\n scope={presentational ? undefined : 'col'}\n aria-sort={presentational ? undefined : memberAriaSort}\n data-column-id={column.id}\n data-grouped={groupedColumn ? 'true' : undefined}\n data-sort-active={activeSort ? 'true' : undefined}\n >\n {!column.header.trim() && column.headerLabel?.trim() && (\n <span class=\"ds-visually-hidden\">{column.headerLabel}</span>\n )}\n <span class=\"ds-table__header-content\">\n {align === 'end' && sortControl}\n {align === 'center' && (\n <span\n class=\"ds-table__sort-slot ds-table__sort-slot--balance\"\n data-sort-balance=\"true\"\n aria-hidden=\"true\"\n />\n )}\n {labelControl}\n {align !== 'end' && sortControl}\n </span>\n {this.renderStickyEdge(column.sticky)}\n </th>\n );\n }\n\n private renderColgroup() {\n return (\n <colgroup>\n {this.selectable && <col class=\"ds-table__selection-column\" />}\n {this.columns.map(column => {\n const width = tableColumnSize(column);\n return <col key={column.id} style={width ? { width } : undefined} />;\n })}\n </colgroup>\n );\n }\n\n private renderHeader(interactive = true, presentational = false) {\n const selection = this.selectionState;\n return (\n <thead class={{\n 'ds-table__head': true,\n 'ds-table__head--semantic-copy': !interactive,\n }}>\n <tr class=\"ds-table__header-row\">\n {this.selectable && (\n <th\n class=\"ds-table__header-cell ds-table__selection-cell ds-table__cell--sticky-start\"\n scope={presentational ? undefined : 'col'}\n >\n {interactive ? this.renderSelectionControl(\n selection.allSelected ? 'Deselect all loaded rows' : 'Select all loaded rows',\n selection.allSelected,\n selection.indeterminate,\n selection.selectableRowIds.length === 0,\n () => this.emitAllSelection(),\n ) : (\n <span class=\"ds-visually-hidden\">Select rows</span>\n )}\n {this.renderStickyEdge('start')}\n </th>\n )}\n {this.columns.map(column => (\n this.renderColumnHeader(column, interactive, presentational)\n ))}\n </tr>\n </thead>\n );\n }\n\n private renderCellValue(value: TableCellValue, column: TableColumn, row: TableRow) {\n if (isTableCellBlank(value)) return null;\n\n if (value == null || isTableCellEmpty(value)) {\n return (\n <ds-text\n class=\"ds-table__cell-track ds-table__cell-track--text\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"secondary\"\n >\n <span aria-hidden=\"true\">—</span>\n <span class=\"ds-visually-hidden\">{this.emptyCellLabel}</span>\n </ds-text>\n );\n }\n\n if (isTableCellIcon(value)) {\n return (\n <ds-icon\n name={value.icon}\n size=\"md\"\n color={value.color ?? 'secondary'}\n label={value.label}\n />\n );\n }\n\n if (isTableCellImage(value)) {\n return (\n <span class=\"ds-table__cell-image\">\n {value.src ? (\n <img class=\"ds-table__cell-image-content\" src={value.src} alt={value.alt} loading=\"lazy\" />\n ) : (\n <span\n class=\"ds-table__cell-image-placeholder\"\n role=\"img\"\n aria-label={value.alt}\n />\n )}\n </span>\n );\n }\n\n if (isTableCellAction(value)) {\n return (\n <ds-button-unfilled\n variant={value.variant ?? 'label'}\n size=\"md\"\n isInset={true}\n label={value.label ?? ''}\n icon={value.icon ?? ''}\n aria-label={value.ariaLabel ?? null}\n hasBorder={value.hasBorder ?? false}\n isInactive={value.isInactive ?? false}\n isLoading={value.isLoading ?? false}\n onDsClick={event => {\n event.stopPropagation();\n this.dsCellAction.emit({\n actionId: value.actionId,\n rowId: row.id,\n columnId: column.id,\n });\n }}\n />\n );\n }\n\n if (isTableCellTag(value)) {\n const variant = value.variant ?? 'tag-only';\n const tag = (\n <ds-tag\n label={value.label}\n intent={value.intent ?? 'neutral'}\n contrast={value.contrast ?? 'faint'}\n size={variant === 'text-with-tag' ? 'sm' : 'md'}\n icon={value.icon ?? ''}\n rounded={value.rounded ?? false}\n isInset\n />\n );\n\n if (variant === 'tag-only') return tag;\n\n return (\n <span class={`ds-table__cell-tag-stack ds-table__cell-tag-stack--${variant}`}>\n {variant === 'tag-with-text' && tag}\n <ds-text\n class=\"ds-table__cell-tag-text ds-table__cell-track\"\n as=\"span\"\n variant={variant === 'tag-with-text' ? 'text-body-small' : 'text-body-medium'}\n color=\"secondary\"\n lineTruncation={1}\n >\n {value.text}\n </ds-text>\n {variant === 'text-with-tag' && (\n <span class=\"ds-table__cell-tag-control-track\">{tag}</span>\n )}\n </span>\n );\n }\n\n const primaryText = isTableCellPrimaryText(value);\n const text = isTableCellText(value) || primaryText\n ? value\n : { primary: value, fontFeature: typeof value === 'number' ? 'tabular-nums' as const : 'normal' as const };\n const wraps = text.wrap ?? column.wrap ?? false;\n\n return (\n <span class={{ 'ds-table__cell-copy': true, 'ds-table__cell-copy--wrap': wraps }}>\n <ds-text\n class=\"ds-table__cell-primary ds-table__cell-track ds-table__cell-track--text\"\n as=\"span\"\n variant=\"text-body-medium\"\n color=\"primary\"\n lineTruncation={wraps ? 'none' : 1}\n wrap={wraps ? 'wrap' : 'nowrap'}\n fontFeature={text.fontFeature ?? 'normal'}\n >\n {text.primary}\n </ds-text>\n {text.secondary !== undefined && text.secondary !== '' && (\n <ds-text\n class=\"ds-table__cell-secondary ds-table__cell-track ds-table__cell-track--text\"\n as=\"span\"\n variant={primaryText ? 'text-body-medium' : 'text-body-small'}\n color={primaryText ? 'primary' : 'secondary'}\n lineTruncation={wraps ? 'none' : 1}\n wrap={wraps ? 'wrap' : 'nowrap'}\n >\n {text.secondary}\n </ds-text>\n )}\n </span>\n );\n }\n\n private renderRow(row: TableRow) {\n const selected = this.selectedSet.has(row.id);\n const rowSelectable = row.selectable !== false && !row.disabled;\n return (\n <tr\n key={row.id}\n class={{\n 'ds-table__row': true,\n 'ds-table__row--selected': selected,\n 'ds-table__row--disabled': !!row.disabled,\n 'ds-table__row--interactive': !!row.interactive && !row.disabled,\n }}\n data-row-id={row.id}\n data-selected={selected ? 'true' : undefined}\n aria-disabled={row.disabled ? 'true' : undefined}\n tabIndex={row.interactive && !row.disabled ? 0 : undefined}\n onClick={event => this.emitRowActivation(row, event)}\n onKeyDown={event => this.handleRowKeydown(row, event)}\n >\n {this.selectable && (\n <td class={{\n 'ds-table__cell': true,\n 'ds-table__selection-cell': true,\n 'ds-table__cell--sticky-start': true,\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--grouped': true,\n 'ds-interaction-fill--selected': selected,\n }}>\n {this.renderSelectionControl(\n `${selected ? 'Deselect' : 'Select'} ${tableRowSelectionLabel(row, this.columns)}`,\n selected,\n false,\n !rowSelectable,\n () => this.emitRowSelection(row),\n )}\n {this.renderStickyEdge('start')}\n </td>\n )}\n {this.columns.map(column => {\n const align = column.align ?? 'start';\n const value = row.cells[column.id];\n const tagCell = isTableCellTag(value);\n const iconCell = isTableCellIcon(value);\n const imageCell = isTableCellImage(value);\n const actionCell = isTableCellAction(value);\n const primaryTextCell = isTableCellPrimaryText(value);\n const emptyCell = value == null || isTableCellEmpty(value);\n const blankCell = isTableCellBlank(value);\n const textCell = primaryTextCell || (!tagCell && !iconCell && !imageCell && !actionCell && !emptyCell && !blankCell);\n const singleTextCell = textCell && !primaryTextCell && (!isTableCellText(value) || !value.secondary);\n const tagVariant = tagCell ? value.variant ?? 'tag-only' : undefined;\n const textVariant = textCell\n ? primaryTextCell\n ? 'primary-pair'\n : singleTextCell\n ? 'single'\n : 'multi'\n : undefined;\n const cellType = tagCell\n ? 'tag'\n : iconCell\n ? 'icon'\n : imageCell\n ? 'image'\n : actionCell\n ? 'action'\n : primaryTextCell\n ? 'primary-text'\n : emptyCell\n ? 'empty'\n : blankCell\n ? 'blank'\n : 'text';\n return (\n <td\n key={`${row.id}:${column.id}`}\n class={{\n 'ds-table__cell': true,\n [`ds-table__cell--align-${align}`]: true,\n 'ds-table__cell--tag': tagCell,\n [`ds-table__cell--tag-${tagVariant}`]: tagCell,\n 'ds-table__cell--icon': iconCell,\n 'ds-table__cell--image': imageCell,\n 'ds-table__cell--action': actionCell,\n 'ds-table__cell--primary-text': primaryTextCell,\n 'ds-table__cell--text-single': singleTextCell,\n 'ds-table__cell--text-multi': textCell && !singleTextCell,\n 'ds-table__cell--empty': emptyCell,\n 'ds-table__cell--blank': blankCell,\n 'ds-table__cell--sticky-start': column.sticky === 'start',\n 'ds-table__cell--sticky-end': column.sticky === 'end',\n 'ds-interaction-fill': true,\n 'ds-interaction-fill--grouped': true,\n 'ds-interaction-fill--selected': selected,\n }}\n data-column-id={column.id}\n data-cell-type={cellType}\n data-cell-variant={tagVariant ?? textVariant}\n >\n <span class=\"ds-table__cell-content ds-interaction-fill__content\">\n {this.renderCellValue(value, column, row)}\n </span>\n {this.renderStickyEdge(column.sticky)}\n </td>\n );\n })}\n </tr>\n );\n }\n\n private renderDataBodies() {\n if (!this.grouped) {\n return <tbody class=\"ds-table__body\">{this.rows.map(row => this.renderRow(row))}</tbody>;\n }\n\n return this.groups.map(group => {\n const count = resolvedTableGroupCount(group);\n const countLabel = group.countLabel ?? `${count} ${count === 1 ? 'item' : 'items'}`;\n return (\n <tbody class=\"ds-table__body ds-table__group\" data-group-id={group.id} key={group.id}>\n <tr class=\"ds-table__group-row\">\n <th class=\"ds-table__group-cell\" scope=\"rowgroup\" colSpan={this.totalColumns}>\n <span class=\"ds-table__group-content\">\n <ds-icon name=\"GroupBy\" size=\"sm\" color=\"secondary\" aria-hidden=\"true\" />\n <ds-text as=\"span\" variant=\"text-body-small\" emphasis={true} color=\"primary\">\n {group.label}\n </ds-text>\n <ds-text as=\"span\" variant=\"text-body-small\" color=\"secondary\">\n {countLabel}\n </ds-text>\n </span>\n </th>\n </tr>\n {group.rows.map(row => this.renderRow(row))}\n </tbody>\n );\n });\n }\n\n private renderSkeletonBody() {\n const count = Math.min(20, Math.max(1, Math.round(this.skeletonRows) || 1));\n return (\n <tbody class=\"ds-table__body ds-table__skeleton-body\">\n {Array.from({ length: count }, (_, index) => (\n <tr class=\"ds-table__row ds-table__skeleton-row\" key={`skeleton-${index}`}>\n {this.selectable && (\n <td class=\"ds-table__cell ds-table__selection-cell ds-table__cell--sticky-start\">\n <ds-skeleton variant=\"icon\" iconSize=\"md\" />\n {this.renderStickyEdge('start')}\n </td>\n )}\n {this.columns.map(column => (\n <td\n class={{\n 'ds-table__cell': true,\n 'ds-table__cell--sticky-start': column.sticky === 'start',\n 'ds-table__cell--sticky-end': column.sticky === 'end',\n }}\n key={`skeleton-${index}:${column.id}`}\n >\n <span class=\"ds-table__cell-content\">\n <ds-skeleton variant=\"text\" textVariant=\"text-body-medium\" width=\"100%\" />\n </span>\n {this.renderStickyEdge(column.sticky)}\n </td>\n ))}\n </tr>\n ))}\n </tbody>\n );\n }\n\n private renderStateBody(kind: 'empty' | 'error') {\n const error = kind === 'error';\n return (\n <tbody class=\"ds-table__body ds-table__state-body\">\n <tr class=\"ds-table__state-row\">\n <td class=\"ds-table__state-cell\" colSpan={this.totalColumns}>\n <ds-empty-state\n icon={error ? 'ErrorTriangle' : 'Inbox'}\n heading={error ? this.errorHeading : this.emptyHeading}\n body={error ? this.errorBody : this.emptyBody}\n />\n </td>\n </tr>\n </tbody>\n );\n }\n\n private renderLazyBody() {\n if (!this.lazyLoading || !this.hasData) return null;\n const error = this.loadMoreError?.trim();\n const manualFallback = this.loadMoreMode === 'manual' || !this.intersectionSupported;\n\n return (\n <tbody class=\"ds-table__body ds-table__load-body\">\n <tr\n class=\"ds-table__load-row\"\n ref={element => {\n this.sentinelEl = element ?? null;\n }}\n >\n <td class=\"ds-table__load-cell\" colSpan={this.totalColumns}>\n {error ? (\n <span class=\"ds-table__load-content ds-table__load-content--error\">\n <ds-icon name=\"ErrorTriangle\" size=\"sm\" color=\"negative\" aria-hidden=\"true\" />\n <ds-text as=\"span\" variant=\"text-body-small\" color=\"secondary\">{error}</ds-text>\n <ds-button-unfilled\n label={this.retryLabel}\n size=\"sm\"\n onDsClick={() => this.requestLoadMore('retry')}\n />\n </span>\n ) : this.loadingMore ? (\n <span class=\"ds-table__load-content\">\n <ds-loader size=\"sm\" color=\"secondary\" />\n <ds-text as=\"span\" variant=\"text-body-small\" color=\"secondary\">\n {this.loadingMoreLabel}\n </ds-text>\n </span>\n ) : this.hasMore && manualFallback ? (\n <ds-button-unfilled\n label={this.loadMoreLabel}\n size=\"sm\"\n onDsClick={() => this.requestLoadMore('manual')}\n />\n ) : this.hasMore ? (\n <span class=\"ds-table__auto-sentinel\" aria-hidden=\"true\" />\n ) : (\n <ds-text as=\"span\" variant=\"text-body-small\" color=\"secondary\">\n {this.endOfResultsLabel}\n </ds-text>\n )}\n </td>\n </tr>\n </tbody>\n );\n }\n\n private renderDocumentStickyHeader() {\n if (!this.documentStickyHeader) return null;\n return (\n <div\n class=\"ds-table__document-sticky-header\"\n >\n <table\n class={{\n 'ds-table__table': true,\n 'ds-table__sticky-header-table': true,\n 'ds-table__table--selectable': this.selectable,\n }}\n style={this.tableStyle}\n role=\"presentation\"\n ref={element => {\n this.stickyHeaderTableEl = element ?? null;\n }}\n >\n {this.renderColgroup()}\n {this.renderHeader(true, true)}\n </table>\n </div>\n );\n }\n\n render() {\n const regionLabel = this.scrollLabel?.trim() || `${this.caption} scroll area`;\n const initialLoading = this.loading && !this.hasData;\n const initialError = !this.hasData && this.error;\n const viewportStyle = this.resolvedMaxHeight\n ? { '--ds-table-max-block-size': this.resolvedMaxHeight }\n : undefined;\n\n return (\n <Host class=\"table-host\">\n <div class={{\n 'ds-table': true,\n 'ds-table--sticky-header': this.stickyHeader,\n 'ds-table--document-sticky-header': this.documentStickyHeader,\n 'ds-table--contained-sticky-header': this.stickyHeader && !this.documentStickyHeader,\n }}>\n <div\n class={{\n 'ds-table__frame': true,\n 'ds-table__frame--overflow-start': this.overflowStart,\n 'ds-table__frame--overflow-end': this.overflowEnd,\n }}\n >\n {this.renderDocumentStickyHeader()}\n <div\n class=\"ds-table__viewport\"\n style={viewportStyle}\n ref={element => {\n this.viewportEl = element ?? null;\n }}\n role={this.scrollable ? 'region' : undefined}\n aria-label={this.scrollable ? regionLabel : undefined}\n tabIndex={this.scrollable ? 0 : undefined}\n >\n <table\n class={{\n 'ds-table__table': true,\n 'ds-table__table--selectable': this.selectable,\n }}\n style={this.tableStyle}\n aria-busy={initialLoading || this.loadingMore ? 'true' : undefined}\n ref={element => {\n this.tableEl = element ?? null;\n }}\n >\n <caption\n class={{\n 'ds-table__caption': true,\n 'ds-visually-hidden': this.captionVisibility === 'hidden',\n }}\n >\n <ds-text as=\"span\" variant=\"text-title-small\" emphasis={true} color=\"primary\">\n {this.caption}\n </ds-text>\n </caption>\n {this.renderColgroup()}\n {this.renderHeader(!this.documentStickyHeader)}\n {initialLoading\n ? this.renderSkeletonBody()\n : initialError\n ? this.renderStateBody('error')\n : this.hasData\n ? this.renderDataBodies()\n : this.renderStateBody('empty')}\n {!initialLoading && !initialError && this.renderLazyBody()}\n </table>\n </div>\n </div>\n <div class=\"ds-visually-hidden\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\">\n {this.announcement}\n </div>\n </div>\n </Host>\n );\n }\n}\n",
745
764
  "type": "registry:ui"
746
765
  },
747
766
  {
748
767
  "path": "src/wc/components/Table/table-model.ts",
749
- "content": "import type {\n TableCellBlank,\n TableCellAction,\n TableCellEmpty,\n TableCellIcon,\n TableCellImage,\n TableCellPrimaryText,\n TableCellText,\n TableCellTag,\n TableCellValue,\n TableColumn,\n TableGroup,\n TableGroupingState,\n TableRow,\n TableSortState,\n} from './table-types';\n\nexport interface TableSelectionState {\n selectableRowIds: string[];\n selectedLoadedCount: number;\n allSelected: boolean;\n indeterminate: boolean;\n}\n\nconst TABLE_COLUMN_WIDTH_TOKENS = {\n xs: '--dimension-table-column-width-xs',\n sm: '--dimension-table-column-width-sm',\n md: '--dimension-table-column-width-md',\n lg: '--dimension-table-column-width-lg',\n xl: '--dimension-table-column-width-xl',\n} as const;\n\nexport function isTableCellText(value: TableCellValue): value is TableCellText {\n return typeof value === 'object' && value !== null && 'primary' in value && !('kind' in value);\n}\n\nexport function isTableCellPrimaryText(value: TableCellValue): value is TableCellPrimaryText {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'primary-text';\n}\n\nexport function isTableCellTag(value: TableCellValue): value is TableCellTag {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'tag';\n}\n\nexport function isTableCellIcon(value: TableCellValue): value is TableCellIcon {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'icon';\n}\n\nexport function isTableCellImage(value: TableCellValue): value is TableCellImage {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'image';\n}\n\nexport function isTableCellAction(value: TableCellValue): value is TableCellAction {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'action';\n}\n\nexport function isTableCellEmpty(value: TableCellValue): value is TableCellEmpty {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'empty';\n}\n\nexport function isTableCellBlank(value: TableCellValue): value is TableCellBlank {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'blank';\n}\n\nexport function tableCellPrimary(value: TableCellValue): string | number | null {\n if (value == null) return null;\n if (isTableCellTag(value)) return value.label;\n if (isTableCellIcon(value)) return null;\n if (isTableCellImage(value)) return value.alt;\n if (isTableCellAction(value)) return value.label ?? value.ariaLabel;\n if (isTableCellPrimaryText(value)) return value.primary;\n if (isTableCellEmpty(value) || isTableCellBlank(value)) return null;\n return isTableCellText(value) ? value.primary : value;\n}\n\nexport function tableRowSelectionLabel(row: TableRow, columns: TableColumn[]): string {\n const explicit = row.selectionLabel?.trim();\n if (explicit) return explicit;\n\n for (const column of columns) {\n const primary = tableCellPrimary(row.cells[column.id]);\n if (primary != null && String(primary).trim()) return String(primary);\n }\n\n return row.id;\n}\n\nexport function nextTableSortState(\n current: TableSortState | null | undefined,\n columnId: string,\n): TableSortState {\n if (current?.columnId !== columnId) return { columnId, direction: 'asc' };\n if (current.direction === 'asc') return { columnId, direction: 'desc' };\n return { columnId, direction: 'asc' };\n}\n\nexport function nextTableGroupOrder(grouping: TableGroupingState): TableGroupingState {\n return {\n ...grouping,\n direction: grouping.direction === 'asc' ? 'desc' : 'asc',\n };\n}\n\nexport function clampTableColumnSize(column: TableColumn): number | undefined {\n if (typeof column.size !== 'number' || !Number.isFinite(column.size) || column.size <= 0) {\n return undefined;\n }\n\n const minimum = Number.isFinite(column.minSize) && (column.minSize ?? 0) > 0\n ? column.minSize!\n : 0;\n const maximum = Number.isFinite(column.maxSize) && (column.maxSize ?? 0) > 0\n ? Math.max(column.maxSize!, minimum)\n : Number.POSITIVE_INFINITY;\n\n return Math.min(Math.max(column.size!, minimum), maximum);\n}\n\nexport function tableColumnSize(column: TableColumn): string | undefined {\n if (typeof column.size === 'string') {\n return `var(${TABLE_COLUMN_WIDTH_TOKENS[column.size]})`;\n }\n const width = clampTableColumnSize(column);\n return width == null ? undefined : `${width}px`;\n}\n\nexport function tableExplicitMinWidth(columns: TableColumn[]): string | undefined {\n const widths = columns.map(tableColumnSize).filter((width): width is string => !!width);\n if (widths.length === 0) return undefined;\n return widths.length === 1 ? widths[0] : `calc(${widths.join(' + ')})`;\n}\n\nexport function tableRows(rows: TableRow[], groups: TableGroup[], grouped: boolean): TableRow[] {\n return grouped ? groups.flatMap(group => group.rows) : rows;\n}\n\nfunction canSelectRow(row: TableRow): boolean {\n return row.selectable !== false && !row.disabled;\n}\n\nexport function deriveTableSelectionState(\n rows: TableRow[],\n selectedRowIds: readonly string[],\n): TableSelectionState {\n const selected = new Set(selectedRowIds);\n const selectableRowIds = rows.filter(canSelectRow).map(row => row.id);\n const selectedLoadedCount = selectableRowIds.reduce(\n (count, id) => count + (selected.has(id) ? 1 : 0),\n 0,\n );\n\n return {\n selectableRowIds,\n selectedLoadedCount,\n allSelected: selectableRowIds.length > 0 && selectedLoadedCount === selectableRowIds.length,\n indeterminate: selectedLoadedCount > 0 && selectedLoadedCount < selectableRowIds.length,\n };\n}\n\nexport function toggleTableRowSelection(\n selectedRowIds: readonly string[],\n row: TableRow,\n): string[] {\n if (!canSelectRow(row)) return [...selectedRowIds];\n const selected = new Set(selectedRowIds);\n if (selected.has(row.id)) selected.delete(row.id);\n else selected.add(row.id);\n return [...selected];\n}\n\nexport function toggleAllLoadedTableRows(\n selectedRowIds: readonly string[],\n loadedRows: TableRow[],\n): string[] {\n const selected = new Set(selectedRowIds);\n const state = deriveTableSelectionState(loadedRows, selectedRowIds);\n\n for (const id of state.selectableRowIds) {\n if (state.allSelected) selected.delete(id);\n else selected.add(id);\n }\n\n return [...selected];\n}\n\nexport function resolvedTableGroupCount(group: TableGroup): number {\n const total = Number.isFinite(group.totalCount) ? Math.max(0, group.totalCount!) : 0;\n return Math.max(total, group.rows.length);\n}\n\nexport function tableModelIssues(\n columns: TableColumn[],\n rows: TableRow[],\n groups: TableGroup[],\n grouped: boolean,\n): string[] {\n const issues: string[] = [];\n const columnIds = new Set<string>();\n for (const column of columns) {\n if (!column.id.trim()) issues.push('Every column requires a non-empty id.');\n else if (columnIds.has(column.id)) issues.push(`Duplicate column id: ${column.id}`);\n columnIds.add(column.id);\n }\n\n const groupIds = new Set<string>();\n if (grouped) {\n for (const group of groups) {\n if (!group.id.trim()) issues.push('Every group requires a non-empty id.');\n else if (groupIds.has(group.id)) issues.push(`Duplicate group id: ${group.id}`);\n groupIds.add(group.id);\n if (group.totalCount != null && group.totalCount < group.rows.length) {\n issues.push(`Group ${group.id} totalCount is smaller than its loaded row count.`);\n }\n }\n }\n\n const rowIds = new Set<string>();\n for (const row of tableRows(rows, groups, grouped)) {\n if (!row.id.trim()) issues.push('Every row requires a non-empty id.');\n else if (rowIds.has(row.id)) issues.push(`Duplicate row id: ${row.id}`);\n rowIds.add(row.id);\n }\n\n return [...new Set(issues)];\n}\n",
768
+ "content": "import type {\n TableCellBlank,\n TableCellAction,\n TableCellEmpty,\n TableCellIcon,\n TableCellImage,\n TableCellPrimaryText,\n TableCellText,\n TableCellTag,\n TableCellValue,\n TableColumn,\n TableGroup,\n TableGroupingState,\n TableRow,\n TableSortState,\n} from './table-types';\n\nexport interface TableSelectionState {\n selectableRowIds: string[];\n selectedLoadedCount: number;\n allSelected: boolean;\n indeterminate: boolean;\n}\n\nconst TABLE_COLUMN_WIDTH_TOKENS = {\n xs: '--dimension-table-column-width-xs',\n sm: '--dimension-table-column-width-sm',\n md: '--dimension-table-column-width-md',\n lg: '--dimension-table-column-width-lg',\n xl: '--dimension-table-column-width-xl',\n} as const;\n\nexport function isTableCellText(value: TableCellValue): value is TableCellText {\n return typeof value === 'object' && value !== null && 'primary' in value && !('kind' in value);\n}\n\nexport function isTableCellPrimaryText(value: TableCellValue): value is TableCellPrimaryText {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'primary-text';\n}\n\nexport function isTableCellTag(value: TableCellValue): value is TableCellTag {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'tag';\n}\n\nexport function isTableCellIcon(value: TableCellValue): value is TableCellIcon {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'icon';\n}\n\nexport function isTableCellImage(value: TableCellValue): value is TableCellImage {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'image';\n}\n\nexport function isTableCellAction(value: TableCellValue): value is TableCellAction {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'action';\n}\n\nexport function isTableCellEmpty(value: TableCellValue): value is TableCellEmpty {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'empty';\n}\n\nexport function isTableCellBlank(value: TableCellValue): value is TableCellBlank {\n return typeof value === 'object' && value !== null && 'kind' in value && value.kind === 'blank';\n}\n\nexport function tableCellPrimary(value: TableCellValue): string | number | null {\n if (value == null) return null;\n if (isTableCellTag(value)) return value.label;\n if (isTableCellIcon(value)) return null;\n if (isTableCellImage(value)) return value.alt;\n if (isTableCellAction(value)) return value.label ?? value.ariaLabel;\n if (isTableCellPrimaryText(value)) return value.primary;\n if (isTableCellEmpty(value) || isTableCellBlank(value)) return null;\n return isTableCellText(value) ? value.primary : value;\n}\n\nexport function tableRowSelectionLabel(row: TableRow, columns: TableColumn[]): string {\n const explicit = row.selectionLabel?.trim();\n if (explicit) return explicit;\n\n for (const column of columns) {\n const primary = tableCellPrimary(row.cells[column.id]);\n if (primary != null && String(primary).trim()) return String(primary);\n }\n\n return row.id;\n}\n\nexport function nextTableSortState(\n current: TableSortState | null | undefined,\n columnId: string,\n): TableSortState {\n if (current?.columnId !== columnId) return { columnId, direction: 'asc' };\n if (current.direction === 'asc') return { columnId, direction: 'desc' };\n return { columnId, direction: 'asc' };\n}\n\nexport function nextTableGroupOrder(grouping: TableGroupingState): TableGroupingState {\n return {\n ...grouping,\n direction: grouping.direction === 'asc' ? 'desc' : 'asc',\n };\n}\n\nexport function clampTableColumnSize(column: TableColumn): number | undefined {\n if (typeof column.size !== 'number' || !Number.isFinite(column.size) || column.size <= 0) {\n return undefined;\n }\n\n const minimum = Number.isFinite(column.minSize) && (column.minSize ?? 0) > 0\n ? column.minSize!\n : 0;\n const maximum = Number.isFinite(column.maxSize) && (column.maxSize ?? 0) > 0\n ? Math.max(column.maxSize!, minimum)\n : Number.POSITIVE_INFINITY;\n\n return Math.min(Math.max(column.size!, minimum), maximum);\n}\n\nexport function tableColumnSize(column: TableColumn): string | undefined {\n if (typeof column.size === 'string') {\n return `var(${TABLE_COLUMN_WIDTH_TOKENS[column.size]})`;\n }\n const width = clampTableColumnSize(column);\n return width == null ? undefined : `${width}px`;\n}\n\nexport function tableExplicitMinWidth(columns: TableColumn[]): string | undefined {\n const widths = columns.map(tableColumnSize).filter((width): width is string => !!width);\n if (widths.length === 0) return undefined;\n return widths.length === 1 ? widths[0] : `calc(${widths.join(' + ')})`;\n}\n\nexport function tableRows(rows: TableRow[], groups: TableGroup[], grouped: boolean): TableRow[] {\n return grouped ? groups.flatMap(group => group.rows) : rows;\n}\n\nfunction canSelectRow(row: TableRow): boolean {\n return row.selectable !== false && !row.disabled;\n}\n\nexport function deriveTableSelectionState(\n rows: TableRow[],\n selectedRowIds: readonly string[],\n): TableSelectionState {\n const selected = new Set(selectedRowIds);\n const selectableRowIds = rows.filter(canSelectRow).map(row => row.id);\n const selectedLoadedCount = selectableRowIds.reduce(\n (count, id) => count + (selected.has(id) ? 1 : 0),\n 0,\n );\n\n return {\n selectableRowIds,\n selectedLoadedCount,\n allSelected: selectableRowIds.length > 0 && selectedLoadedCount === selectableRowIds.length,\n indeterminate: selectedLoadedCount > 0 && selectedLoadedCount < selectableRowIds.length,\n };\n}\n\nexport function toggleTableRowSelection(\n selectedRowIds: readonly string[],\n row: TableRow,\n): string[] {\n if (!canSelectRow(row)) return [...selectedRowIds];\n const selected = new Set(selectedRowIds);\n if (selected.has(row.id)) selected.delete(row.id);\n else selected.add(row.id);\n return [...selected];\n}\n\nexport function toggleAllLoadedTableRows(\n selectedRowIds: readonly string[],\n loadedRows: TableRow[],\n): string[] {\n const selected = new Set(selectedRowIds);\n const state = deriveTableSelectionState(loadedRows, selectedRowIds);\n\n for (const id of state.selectableRowIds) {\n if (state.allSelected) selected.delete(id);\n else selected.add(id);\n }\n\n return [...selected];\n}\n\nexport function resolvedTableGroupCount(group: TableGroup): number {\n const total = Number.isFinite(group.totalCount) ? Math.max(0, group.totalCount!) : 0;\n return Math.max(total, group.rows.length);\n}\n\nexport function tableModelIssues(\n columns: TableColumn[],\n rows: TableRow[],\n groups: TableGroup[],\n grouped: boolean,\n): string[] {\n const issues: string[] = [];\n const columnIds = new Set<string>();\n for (const column of columns) {\n if (!column.id.trim()) issues.push('Every column requires a non-empty id.');\n else if (columnIds.has(column.id)) issues.push(`Duplicate column id: ${column.id}`);\n if (!column.header.trim() && !column.headerLabel?.trim()) {\n issues.push(`Column ${column.id || '(missing id)'} requires a visible header or headerLabel.`);\n }\n columnIds.add(column.id);\n }\n\n const groupIds = new Set<string>();\n if (grouped) {\n for (const group of groups) {\n if (!group.id.trim()) issues.push('Every group requires a non-empty id.');\n else if (groupIds.has(group.id)) issues.push(`Duplicate group id: ${group.id}`);\n groupIds.add(group.id);\n if (group.totalCount != null && group.totalCount < group.rows.length) {\n issues.push(`Group ${group.id} totalCount is smaller than its loaded row count.`);\n }\n }\n }\n\n const rowIds = new Set<string>();\n for (const row of tableRows(rows, groups, grouped)) {\n if (!row.id.trim()) issues.push('Every row requires a non-empty id.');\n else if (rowIds.has(row.id)) issues.push(`Duplicate row id: ${row.id}`);\n rowIds.add(row.id);\n }\n\n return [...new Set(issues)];\n}\n",
750
769
  "type": "registry:ui"
751
770
  },
752
771
  {
753
772
  "path": "src/wc/components/Table/table-types.ts",
754
- "content": "import type { TagContrast, TagIntent } from '../Tag/Tag';\nimport type { IconColor } from '../Icon/Icon';\n\nexport type TableSortDirection = 'asc' | 'desc';\nexport type TableSelectionMode = 'none' | 'multiple';\nexport type TableCellAlign = 'start' | 'center' | 'end';\nexport type TableCaptionVisibility = 'visible' | 'hidden';\nexport type TableLoadMoreMode = 'auto' | 'manual';\nexport type TableLoadMoreReason = 'auto' | 'manual' | 'retry';\nexport type TableColumnWidth = 'xs' | 'sm' | 'md' | 'lg' | 'xl';\n\n/** Independently sortable label within a compound column header. */\nexport interface TableHeaderSegment {\n label: string;\n /** Stable key emitted through TableSortState.columnId. */\n sortKey: string;\n /** Visible separator rendered after this label when another segment follows. */\n separator?: string;\n}\n\n/** Controlled member-row sort state. Group order is controlled separately. */\nexport interface TableSortState {\n columnId: string;\n direction: TableSortDirection;\n}\n\n/** One controlled grouping level plus the supplied group-order direction. */\nexport interface TableGroupingState {\n columnId: string;\n direction: TableSortDirection;\n}\n\n/** Standard two-line cell content owned by the table renderer. */\nexport interface TableCellText {\n primary: string | number;\n secondary?: string;\n /** Allow this cell to wrap even when its column truncates by default. */\n wrap?: boolean;\n fontFeature?: 'normal' | 'tabular-nums';\n}\n\n/** Two-line text content where both tracks use primary body text. */\nexport interface TableCellPrimaryText {\n kind: 'primary-text';\n primary: string | number;\n secondary: string | number;\n /** Allow this cell to wrap even when its column truncates by default. */\n wrap?: boolean;\n fontFeature?: 'normal' | 'tabular-nums';\n}\n\n/** Applicable data point whose value is currently unavailable. */\nexport interface TableCellEmpty {\n kind: 'empty';\n}\n\n/** Data point that is intentionally not applicable to this row. */\nexport interface TableCellBlank {\n kind: 'blank';\n}\n\n/** Declarative icon-only content rendered by the table's standard cell primitive. */\nexport interface TableCellIcon {\n kind: 'icon';\n /** Exact canonical IcoMo export name. */\n icon: string;\n color?: IconColor;\n /** Accessible label for informative icons. Omit only when the icon is decorative. */\n label?: string;\n /** Application-owned value used when this icon column participates in sorting. */\n sortValue?: string | number | boolean;\n}\n\n/** Declarative 16:9 image preview rendered by the table's standard cell primitive. */\nexport interface TableCellImage {\n kind: 'image';\n /** Optional image source. Omit it to render the standard bordered placeholder. */\n src?: string;\n /** Accessible description for either the image or its placeholder. */\n alt: string;\n}\n\ninterface TableCellActionBase {\n kind: 'action';\n /** Stable application-owned action identity emitted with dsCellAction. */\n actionId: string;\n /** Add the ButtonUnfilled resting border. Action cells are unbordered by default. */\n hasBorder?: boolean;\n isInactive?: boolean;\n isLoading?: boolean;\n}\n\n/** Declarative ButtonUnfilled content rendered by the table's standard action cell. */\nexport type TableCellAction = TableCellActionBase & (\n | {\n variant?: 'label';\n label: string;\n icon?: never;\n ariaLabel?: string;\n }\n | {\n variant: 'icon-label';\n label: string;\n icon: string;\n ariaLabel?: string;\n }\n | {\n variant: 'icon';\n label?: never;\n icon: string;\n ariaLabel: string;\n }\n);\n\n/** Declarative Tag content rendered by the table's standard cell primitive. */\nexport type TableCellTagVariant = 'tag-only' | 'tag-with-text' | 'text-with-tag';\n\ninterface TableCellTagBase {\n kind: 'tag';\n label: string;\n intent?: TagIntent;\n contrast?: TagContrast;\n icon?: string;\n rounded?: boolean;\n}\n\nexport type TableCellTag = TableCellTagBase & (\n | {\n variant?: 'tag-only';\n text?: never;\n }\n | {\n variant: Exclude<TableCellTagVariant, 'tag-only'>;\n text: string;\n }\n);\n\nexport type TableCellValue =\n | string\n | number\n | null\n | undefined\n | TableCellText\n | TableCellPrimaryText\n | TableCellTag\n | TableCellIcon\n | TableCellImage\n | TableCellAction\n | TableCellEmpty\n | TableCellBlank;\n\nexport interface TableColumn {\n /** Stable column identity. */\n id: string;\n header: string;\n /** Optional labels for columns that present and sort multiple related data points. */\n headerSegments?: TableHeaderSegment[];\n align?: TableCellAlign;\n sortable?: boolean;\n /** Preferred TokoMo table-column width. Numbers remain available for exceptional custom pixel widths. */\n size?: TableColumnWidth | number;\n /** Minimum accepted preferred width in CSS pixels. */\n minSize?: number;\n /** Maximum accepted preferred width in CSS pixels. */\n maxSize?: number;\n /** Wrap cell text instead of truncating it to one line. */\n wrap?: boolean;\n}\n\nexport interface TableRow {\n /** Stable identity, unique across all groups. */\n id: string;\n cells: Record<string, TableCellValue>;\n /** Accessible row name used by its selection control. Falls back to the first cell. */\n selectionLabel?: string;\n /** Exclude this row from selection without muting its content. */\n selectable?: boolean;\n /** Keep the row visible while disabling its selection control and styling it as inactive. */\n disabled?: boolean;\n}\n\nexport interface TableGroup {\n /** Stable group identity. */\n id: string;\n label: string;\n value?: string | number;\n /** Total server-side member count; may exceed the currently loaded row count. */\n totalCount?: number;\n /** Optional localized, display-ready count such as “18 vehicles”. */\n countLabel?: string;\n rows: TableRow[];\n}\n\nexport interface TableSortChangeDetail {\n sort: TableSortState | null;\n}\n\nexport interface TableGroupingChangeDetail {\n grouping: TableGroupingState;\n}\n\nexport interface TableSelectionChangeDetail {\n selectedRowIds: string[];\n scope: 'row' | 'all-loaded';\n changedRowId?: string;\n selected?: boolean;\n}\n\nexport interface TableLoadMoreDetail {\n reason: TableLoadMoreReason;\n loadIdentity: string | number;\n loadedRowCount: number;\n}\n\nexport interface TableCellActionDetail {\n actionId: string;\n rowId: string;\n columnId: string;\n}\n",
773
+ "content": "import type { TagContrast, TagIntent } from '../Tag/Tag';\nimport type { IconColor } from '../Icon/Icon';\n\nexport type TableSortDirection = 'asc' | 'desc';\nexport type TableSelectionMode = 'none' | 'multiple';\nexport type TableCellAlign = 'start' | 'center' | 'end';\nexport type TableColumnSticky = 'start' | 'end';\nexport type TableCaptionVisibility = 'visible' | 'hidden';\nexport type TableLoadMoreMode = 'auto' | 'manual';\nexport type TableLoadMoreReason = 'auto' | 'manual' | 'retry';\nexport type TableColumnWidth = 'xs' | 'sm' | 'md' | 'lg' | 'xl';\n\n/** Independently sortable label within a compound column header. */\nexport interface TableHeaderSegment {\n label: string;\n /** Stable key emitted through TableSortState.columnId. */\n sortKey: string;\n /** Visible separator rendered after this label when another segment follows. */\n separator?: string;\n}\n\n/** Controlled member-row sort state. Group order is controlled separately. */\nexport interface TableSortState {\n columnId: string;\n direction: TableSortDirection;\n}\n\n/** One controlled grouping level plus the supplied group-order direction. */\nexport interface TableGroupingState {\n columnId: string;\n direction: TableSortDirection;\n}\n\n/** Standard two-line cell content owned by the table renderer. */\nexport interface TableCellText {\n primary: string | number;\n secondary?: string;\n /** Allow this cell to wrap even when its column truncates by default. */\n wrap?: boolean;\n fontFeature?: 'normal' | 'tabular-nums';\n}\n\n/** Two-line text content where both tracks use primary body text. */\nexport interface TableCellPrimaryText {\n kind: 'primary-text';\n primary: string | number;\n secondary: string | number;\n /** Allow this cell to wrap even when its column truncates by default. */\n wrap?: boolean;\n fontFeature?: 'normal' | 'tabular-nums';\n}\n\n/** Applicable data point whose value is currently unavailable. */\nexport interface TableCellEmpty {\n kind: 'empty';\n}\n\n/** Data point that is intentionally not applicable to this row. */\nexport interface TableCellBlank {\n kind: 'blank';\n}\n\n/** Declarative icon-only content rendered by the table's standard cell primitive. */\nexport interface TableCellIcon {\n kind: 'icon';\n /** Exact canonical IcoMo export name. */\n icon: string;\n color?: IconColor;\n /** Accessible label for informative icons. Omit only when the icon is decorative. */\n label?: string;\n /** Application-owned value used when this icon column participates in sorting. */\n sortValue?: string | number | boolean;\n}\n\n/** Declarative 16:9 image preview rendered by the table's standard cell primitive. */\nexport interface TableCellImage {\n kind: 'image';\n /** Optional image source. Omit it to render the standard bordered placeholder. */\n src?: string;\n /** Accessible description for either the image or its placeholder. */\n alt: string;\n}\n\ninterface TableCellActionBase {\n kind: 'action';\n /** Stable application-owned action identity emitted with dsCellAction. */\n actionId: string;\n /** Add the ButtonUnfilled resting border. Action cells are unbordered by default. */\n hasBorder?: boolean;\n isInactive?: boolean;\n isLoading?: boolean;\n}\n\n/** Declarative ButtonUnfilled content rendered by the table's standard action cell. */\nexport type TableCellAction = TableCellActionBase & (\n | {\n variant?: 'label';\n label: string;\n icon?: never;\n ariaLabel?: string;\n }\n | {\n variant: 'icon-label';\n label: string;\n icon: string;\n ariaLabel?: string;\n }\n | {\n variant: 'icon';\n label?: never;\n icon: string;\n ariaLabel: string;\n }\n);\n\n/** Declarative Tag content rendered by the table's standard cell primitive. */\nexport type TableCellTagVariant = 'tag-only' | 'tag-with-text' | 'text-with-tag';\n\ninterface TableCellTagBase {\n kind: 'tag';\n label: string;\n intent?: TagIntent;\n contrast?: TagContrast;\n icon?: string;\n rounded?: boolean;\n}\n\nexport type TableCellTag = TableCellTagBase & (\n | {\n variant?: 'tag-only';\n text?: never;\n }\n | {\n variant: Exclude<TableCellTagVariant, 'tag-only'>;\n text: string;\n }\n);\n\nexport type TableCellValue =\n | string\n | number\n | null\n | undefined\n | TableCellText\n | TableCellPrimaryText\n | TableCellTag\n | TableCellIcon\n | TableCellImage\n | TableCellAction\n | TableCellEmpty\n | TableCellBlank;\n\nexport interface TableColumn {\n /** Stable column identity. */\n id: string;\n /** Visible column label. May be empty when headerLabel supplies a non-visual name. */\n header: string;\n /** Screen-reader-only column name for an intentionally blank visual header. */\n headerLabel?: string;\n /** Optional labels for columns that present and sort multiple related data points. */\n headerSegments?: TableHeaderSegment[];\n align?: TableCellAlign;\n sortable?: boolean;\n /** Preferred TokoMo table-column width. Numbers remain available for exceptional custom pixel widths. */\n size?: TableColumnWidth | number;\n /** Minimum accepted preferred width in CSS pixels. */\n minSize?: number;\n /** Maximum accepted preferred width in CSS pixels. */\n maxSize?: number;\n /** Wrap cell text instead of truncating it to one line. */\n wrap?: boolean;\n /** Pin one application column to either inline edge during horizontal scrolling. */\n sticky?: TableColumnSticky;\n}\n\nexport interface TableRow {\n /** Stable identity, unique across all groups. */\n id: string;\n cells: Record<string, TableCellValue>;\n /** Accessible row name used by its selection control. Falls back to the first cell. */\n selectionLabel?: string;\n /** Exclude this row from selection without muting its content. */\n selectable?: boolean;\n /** Keep the row visible while disabling its selection control and styling it as inactive. */\n disabled?: boolean;\n /** Make the complete row a keyboard- and pointer-activatable application target. */\n interactive?: boolean;\n}\n\nexport interface TableGroup {\n /** Stable group identity. */\n id: string;\n label: string;\n value?: string | number;\n /** Total server-side member count; may exceed the currently loaded row count. */\n totalCount?: number;\n /** Optional localized, display-ready count such as “18 vehicles”. */\n countLabel?: string;\n rows: TableRow[];\n}\n\nexport interface TableSortChangeDetail {\n sort: TableSortState | null;\n}\n\nexport interface TableGroupingChangeDetail {\n grouping: TableGroupingState;\n}\n\nexport interface TableSelectionChangeDetail {\n selectedRowIds: string[];\n scope: 'row' | 'all-loaded';\n changedRowId?: string;\n selected?: boolean;\n}\n\nexport interface TableLoadMoreDetail {\n reason: TableLoadMoreReason;\n loadIdentity: string | number;\n loadedRowCount: number;\n}\n\nexport interface TableCellActionDetail {\n actionId: string;\n rowId: string;\n columnId: string;\n}\n\nexport interface TableRowActivateDetail {\n rowId: string;\n}\n",
755
774
  "type": "registry:ui"
756
775
  }
757
776
  ]
@@ -3,7 +3,7 @@
3
3
  * Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
4
4
  */
5
5
  import type { EventName, StencilReactComponent } from './react-runtime.js';
6
- import { type DsTableCustomEvent, type TableCellActionDetail, type TableGroupingChangeDetail, type TableLoadMoreDetail, type TableSelectionChangeDetail, type TableSortChangeDetail } from "@ds-mo/ui";
6
+ import { type DsTableCustomEvent, type TableCellActionDetail, type TableGroupingChangeDetail, type TableLoadMoreDetail, type TableRowActivateDetail, type TableSelectionChangeDetail, type TableSortChangeDetail } from "@ds-mo/ui";
7
7
  import type { Components } from "@ds-mo/ui/dist/components";
8
8
  import { DsTable as DsTableElement } from "@ds-mo/ui/dist/components/ds-table.js";
9
9
  export type DsTableEvents = {
@@ -12,5 +12,6 @@ export type DsTableEvents = {
12
12
  onDsSelectionChange: EventName<DsTableCustomEvent<TableSelectionChangeDetail>>;
13
13
  onDsLoadMore: EventName<DsTableCustomEvent<TableLoadMoreDetail>>;
14
14
  onDsCellAction: EventName<DsTableCustomEvent<TableCellActionDetail>>;
15
+ onDsRowActivate: EventName<DsTableCustomEvent<TableRowActivateDetail>>;
15
16
  };
16
17
  export declare const DsTable: StencilReactComponent<DsTableElement, DsTableEvents, Components.DsTable, 'caption'>;
@@ -12,7 +12,8 @@ export const DsTable = /*@__PURE__*/ createComponent({
12
12
  onDsGroupingChange: 'dsGroupingChange',
13
13
  onDsSelectionChange: 'dsSelectionChange',
14
14
  onDsLoadMore: 'dsLoadMore',
15
- onDsCellAction: 'dsCellAction'
15
+ onDsCellAction: 'dsCellAction',
16
+ onDsRowActivate: 'dsRowActivate'
16
17
  },
17
18
  defineCustomElement: defineDsTable
18
19
  });