@adia-ai/web-components 0.8.43 → 0.8.45

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 (113) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/components/avatar/avatar.class.js +12 -1
  3. package/components/button/button.d.ts +4 -0
  4. package/components/chart/chart.a2ui.json +26 -0
  5. package/components/chart/chart.class.js +1304 -222
  6. package/components/chart/chart.css +151 -1
  7. package/components/chart/chart.d.ts +6 -0
  8. package/components/chart/chart.yaml +57 -0
  9. package/components/chart-legend/chart-legend.a2ui.json +17 -2
  10. package/components/chart-legend/chart-legend.class.js +69 -4
  11. package/components/chart-legend/chart-legend.css +65 -1
  12. package/components/chart-legend/chart-legend.d.ts +4 -2
  13. package/components/chart-legend/chart-legend.examples.md +4 -4
  14. package/components/chart-legend/chart-legend.yaml +43 -3
  15. package/components/combobox/combobox.a2ui.json +3 -0
  16. package/components/combobox/combobox.class.js +9 -8
  17. package/components/combobox/combobox.css +25 -4
  18. package/components/combobox/combobox.yaml +6 -0
  19. package/components/command/command.css +17 -1
  20. package/components/context-menu/context-menu.a2ui.json +8 -3
  21. package/components/context-menu/context-menu.class.js +46 -5
  22. package/components/context-menu/context-menu.d.ts +8 -3
  23. package/components/context-menu/context-menu.examples.md +2 -2
  24. package/components/context-menu/context-menu.yaml +22 -5
  25. package/components/heatmap/heatmap.a2ui.json +5 -0
  26. package/components/heatmap/heatmap.d.ts +2 -0
  27. package/components/heatmap/heatmap.yaml +8 -0
  28. package/components/input/input.a2ui.json +2 -2
  29. package/components/input/input.class.js +12 -7
  30. package/components/input/input.yaml +6 -5
  31. package/components/integration-card/integration-card.d.ts +2 -0
  32. package/components/nav/nav.a2ui.json +2 -2
  33. package/components/nav/nav.css +1 -1
  34. package/components/nav/nav.d.ts +1 -1
  35. package/components/nav/nav.yaml +14 -3
  36. package/components/nav-group/nav-group.css +37 -3
  37. package/components/noodles/noodles.a2ui.json +0 -15
  38. package/components/noodles/noodles.yaml +0 -12
  39. package/components/number-format/number-format.class.js +10 -0
  40. package/components/pagination/pagination.class.js +140 -26
  41. package/components/popover/popover.d.ts +2 -0
  42. package/components/progress/progress.class.js +11 -6
  43. package/components/range/range.class.js +9 -1
  44. package/components/search/search.class.js +39 -5
  45. package/components/select/select.a2ui.json +10 -1
  46. package/components/select/select.class.js +34 -14
  47. package/components/select/select.css +39 -1
  48. package/components/select/select.d.ts +2 -0
  49. package/components/select/select.yaml +19 -1
  50. package/components/stream/stream.d.ts +2 -0
  51. package/components/table/cell-types.js +9 -0
  52. package/components/table/table.a2ui.json +6 -1
  53. package/components/table/table.class.js +265 -34
  54. package/components/table/table.css +38 -10
  55. package/components/table/table.d.ts +3 -1
  56. package/components/table/table.examples.md +2 -2
  57. package/components/table/table.yaml +15 -2
  58. package/components/table-toolbar/table-toolbar.a2ui.json +43 -4
  59. package/components/table-toolbar/table-toolbar.class.js +407 -28
  60. package/components/table-toolbar/table-toolbar.css +299 -0
  61. package/components/table-toolbar/table-toolbar.d.ts +15 -1
  62. package/components/table-toolbar/table-toolbar.yaml +162 -15
  63. package/components/tag/tag.class.js +13 -1
  64. package/components/text/text.a2ui.json +3 -3
  65. package/components/text/text.d.ts +5 -3
  66. package/components/text/text.yaml +3 -3
  67. package/components/textarea/textarea.class.js +9 -1
  68. package/components/tour/tour.d.ts +10 -0
  69. package/core/a11y.d.ts +22 -0
  70. package/core/a11y.js +64 -0
  71. package/core/anchor.js +9 -5
  72. package/core/data-stream.js +37 -2
  73. package/core/element.js +1 -1
  74. package/core/index.d.ts +1 -0
  75. package/core/index.js +1 -0
  76. package/core/provider.d.ts +9 -13
  77. package/core/provider.js +9 -113
  78. package/core/store.d.ts +46 -0
  79. package/core/store.js +89 -0
  80. package/custom-elements.json +127 -33
  81. package/dist/host.min.css +1 -1
  82. package/dist/host.sheet.js +1 -1
  83. package/dist/theme-provider.min.js +1 -1
  84. package/dist/web-components.min.css +1 -1
  85. package/dist/web-components.min.js +119 -119
  86. package/dist/web-components.sheet.js +1 -1
  87. package/package.json +1 -1
  88. package/patterns/access-requests/access-requests.examples.html +1 -1
  89. package/patterns/admin-keys-and-export/admin-keys-and-export.examples.html +7 -7
  90. package/patterns/agent-cost/agent-cost.examples.html +6 -6
  91. package/patterns/agent-memory/agent-memory.examples.html +3 -3
  92. package/patterns/agent-prompt-library/agent-prompt-library.examples.html +8 -8
  93. package/patterns/agent-tool-call/agent-tool-call.examples.html +1 -1
  94. package/patterns/approvals/approvals.examples.html +6 -6
  95. package/patterns/audit-log/audit-log.examples.html +1 -1
  96. package/patterns/bulk-action-toolbar/bulk-action-toolbar.examples.html +4 -4
  97. package/patterns/chart-in-card/chart-in-card.examples.html +114 -0
  98. package/patterns/chart-in-card/chart-in-card.examples.js +37 -0
  99. package/patterns/comments-and-collaboration/comments-and-collaboration.examples.html +5 -5
  100. package/patterns/data-tables-inline-edit-and-tree/data-tables-inline-edit-and-tree.examples.html +2 -2
  101. package/patterns/diff-review/diff-review.examples.html +6 -6
  102. package/patterns/export-flow/export-flow.examples.html +2 -2
  103. package/patterns/filter-bar/filter-bar.examples.html +2 -2
  104. package/patterns/forms-conditional-and-autocomplete/forms-conditional-and-autocomplete.examples.html +2 -2
  105. package/patterns/inline-dialog/inline-dialog.examples.html +2 -2
  106. package/patterns/marketing-engagement/marketing-engagement.examples.html +4 -4
  107. package/patterns/notifications-bell-and-digest/notifications-bell-and-digest.examples.html +1 -1
  108. package/patterns/permissions-matrix/permissions-matrix.examples.html +1 -1
  109. package/patterns/permissions-sharing/permissions-sharing.examples.html +2 -2
  110. package/patterns/profile-public-and-verification/profile-public-and-verification.examples.html +4 -4
  111. package/patterns/search-discovery/search-discovery.examples.html +2 -2
  112. package/styles/api/sizing.css +46 -0
  113. package/styles/type/roles.css +29 -9
@@ -45,13 +45,22 @@
45
45
  * .sortState → [{key, dir}] (read-only)
46
46
  * .exportCSV(filename?)
47
47
  *
48
- * Events:
48
+ * Events (fired):
49
49
  * sort — { detail: { key, dir, sortState } }
50
50
  * select — { detail: { selected: [...indices] } }
51
51
  * page — { detail: { page } }
52
52
  * resize — { detail: { key, width } }
53
53
  * cell-click — { detail: { key, row, value, dataIndex } }
54
54
  * row-click — { detail: { row, dataIndex } } — row-level companion to cell-click
55
+ *
56
+ * Events (listened) — gh#1764/#1780, ADR-0079. table-toolbar-ui dispatches
57
+ * these directly at its [for]-resolved table-ui target instead of writing
58
+ * a property or calling a method; any other consumer may dispatch them too:
59
+ * toolbar-search — { detail: { value } } → this.search =
60
+ * toolbar-filter-set — { detail: { key, value, op } } → this.setFilter(key, value, op)
61
+ * toolbar-filter-clear — { detail: {} } → this.clearFilters()
62
+ * toolbar-columns-set — { detail: { columns } } → this.columns =
63
+ * toolbar-paginate — { detail: { pageSize } } → this.paginate =
55
64
  */
56
65
 
57
66
  import { UIElement } from '../../core/element.js';
@@ -94,6 +103,117 @@ function csvEscape(val) {
94
103
  return str;
95
104
  }
96
105
 
106
+ // ── SSR adopt-in-place (gh#1678) ─────────────────────────────────────────────
107
+
108
+ /**
109
+ * Idempotent attribute set — skip the DOM write entirely when the value
110
+ * already matches. `setAttribute` mutates (and queues a MutationObserver
111
+ * record) even when the new value is byte-identical to the old one, so a
112
+ * host/row-level attribute this render path sets on EVERY call (`role`,
113
+ * `tabindex`, `data-index`, `aria-selected`) must compare-before-write to
114
+ * let a byte-identical SSR-rendered subtree survive upgrade with zero
115
+ * subtree mutations (AC-004a-equivalent probe) — bare `setAttribute` cannot
116
+ * give that guarantee even when the value never actually changes.
117
+ */
118
+ function setAttrIfChanged(el, name, value) {
119
+ if (el.getAttribute(name) !== value) el.setAttribute(name, value);
120
+ }
121
+
122
+ /** Idempotent attribute removal — same rationale as setAttrIfChanged. */
123
+ function removeAttrIfPresent(el, name) {
124
+ if (el.hasAttribute(name)) el.removeAttribute(name);
125
+ }
126
+
127
+ /**
128
+ * Marks a freshly-built candidate cell as renderer-owned — its content came
129
+ * from something free to attach an event listener or otherwise stash
130
+ * runtime state on the node it returns, where `Node.isEqualNode()` (the
131
+ * adopt-or-diff structural check below) has no way to see the difference
132
+ * (it compares tag/attributes/text/descendants only). Two, and only two,
133
+ * sources are marked:
134
+ *
135
+ * 1. `col.render()` — arbitrary consumer code. It can do anything to the
136
+ * node it hands back, so it's always marked; there's no way to inspect
137
+ * an opaque function for safety.
138
+ * 2. A built-in cell-type renderer (`typeDef.render`) that declares
139
+ * `attachesListeners: true` (currently only `cellTypes.actions`, which
140
+ * calls `addEventListener()` directly — see cell-types.js).
141
+ *
142
+ * Deliberately NOT marked: `col.format()`, the plain-text fallback, and
143
+ * every OTHER built-in cell-type renderer (text/number/currency/percent/
144
+ * date/datetime/boolean/badge/avatar/link/markdown/progress) — each of
145
+ * those only sets attributes on already-declarative custom elements
146
+ * (badge-ui, avatar-ui, check-ui, progress-ui, …) or plain nodes with no
147
+ * listeners; their own runtime behavior lives in their OWN
148
+ * `connectedCallback`, which fires independent of whether table.class.js
149
+ * discards or adopts the cell that contains them. Marking every
150
+ * `typeDef.render` cell unconditionally (an earlier draft of this fix) was
151
+ * tried and reverted — it broke the AC-004a zero-mutation acceptance
152
+ * criterion for the overwhelmingly common case (plain text/number/date
153
+ * cells) for no correctness benefit, since none of those renderers ever
154
+ * attach a listener. A cell that discards without ever being marked here
155
+ * (and turns out later to have needed it) is exactly the gap
156
+ * `guard-patterns.md` §4.1 asks a new interactive cell type to close by
157
+ * declaring `attachesListeners: true` on itself.
158
+ *
159
+ * A renderer-owned cell that happens to be structurally identical to the
160
+ * existing DOM would, without this guard, silently keep the OLD node (and
161
+ * its stale/missing listener) instead of the fresh one the renderer just
162
+ * built (gh#1678 CodeRabbit finding, table.class.js `adoptOrDiffChildren`
163
+ * L141-150 as originally shipped).
164
+ */
165
+ const RENDERER_OWNED = new WeakSet();
166
+
167
+ /**
168
+ * Adopt-or-diff child reconciliation — the core of table-ui's SSR
169
+ * adopt-in-place fix. `freshChildren` are already-built, detached candidate
170
+ * nodes (header cells, row cells, …) computed exactly as this file always
171
+ * computed them. Rather than unconditionally `replaceChild`-ing every
172
+ * position (the old, SSR-hostile behavior — a byte-identical server-
173
+ * rendered subtree got fully rebuilt at upgrade regardless), each position
174
+ * is compared against its EXISTING child with `Node.isEqualNode()` — a
175
+ * standard, structural + attribute + text deep-equality check present in
176
+ * every environment this framework runs under (real browsers, happy-dom,
177
+ * and linkedom under SSR — confirmed directly; unlike the browser-only APIs
178
+ * `ssr-compatibility`'s guard-patterns.md §1 guards, `isEqualNode` is core
179
+ * DOM and never absent). A match discards the freshly-built candidate and
180
+ * leaves the live DOM completely untouched — the "adopt" half. A mismatch
181
+ * replaces the position wholesale exactly as before — the "fall back to
182
+ * full rebuild" half. Never a partial/half-adopt: a position either
183
+ * survives byte-for-byte or gets fully rebuilt, matching the shape
184
+ * pagination-ui's own first-connect adoption fix (gh#1687) established for
185
+ * its flat, keyed item list — generalized here to an arbitrary positional
186
+ * child (header cells, row cells) via a value check instead of a
187
+ * shape/key check, since these cells have no stable identity key of their
188
+ * own the way a pagination item does.
189
+ *
190
+ * **Renderer-owned exception (gh#1678 CodeRabbit follow-up):** structural
191
+ * equality is a necessary but not sufficient adoption test — it says
192
+ * nothing about listeners or other runtime state a renderer attached to the
193
+ * node it returned. A cell in `RENDERER_OWNED` is therefore ALWAYS replaced
194
+ * with its fresh candidate, never adopted via the structural-equality path,
195
+ * even when `isEqualNode()` would have reported a match — correctness over
196
+ * the zero-mutation optimization for exactly the cells where structural
197
+ * equality can't prove the node is safe to keep. See
198
+ * `ssr-compatibility`'s `references/failure-shapes.md` §5.1 and
199
+ * `references/guard-patterns.md` §4.1 for the general shape.
200
+ */
201
+ function adoptOrDiffChildren(container, freshChildren) {
202
+ while (container.children.length > freshChildren.length) container.lastChild.remove();
203
+ for (let i = 0; i < freshChildren.length; i++) {
204
+ const existing = container.children[i];
205
+ const fresh = freshChildren[i];
206
+ if (!existing) {
207
+ container.appendChild(fresh);
208
+ } else if (RENDERER_OWNED.has(fresh) || !existing.isEqualNode(fresh)) {
209
+ container.replaceChild(fresh, existing);
210
+ }
211
+ // else: existing already matches fresh byte-for-byte AND fresh carries
212
+ // no renderer-owned runtime state — adopt in place, discard the
213
+ // candidate, touch nothing.
214
+ }
215
+ }
216
+
97
217
  // ── Component ────────────────────────────────────────────────────────────────
98
218
 
99
219
  export class UITable extends UIElement {
@@ -125,6 +245,11 @@ export class UITable extends UIElement {
125
245
  paginate: { type: Number, default: 0, reflect: true },
126
246
  loading: { type: Boolean, default: false, reflect: true },
127
247
  search: { type: String, default: '', reflect: true },
248
+ // adiav2 zero-overrides purge (gh#1680) deleted an app-side [data-clickable]
249
+ // cursor rule with no kit-side replacement — row-click is always-emitted
250
+ // (fires whether or not a listener cares), so cursor can't be inferred
251
+ // from listener presence; this is an explicit opt-in affordance instead.
252
+ clickableRows: { type: Boolean, default: false, reflect: true, attribute: 'clickable-rows' },
128
253
  };
129
254
 
130
255
  static template = () => null;
@@ -245,6 +370,52 @@ export class UITable extends UIElement {
245
370
 
246
371
  get filters() { return Object.fromEntries(this.#filters); }
247
372
 
373
+ // ── table-toolbar-ui command-event listeners (gh#1764/#1780, ADR-0079) ──
374
+ // Each handler applies the exact call table-toolbar.class.js used to make
375
+ // directly on this element before the events-only normalization — no
376
+ // behavior change, only the interaction shape.
377
+ //
378
+ // `e.target !== this` guard (code-checker finding, 2026-08-20): these
379
+ // events are dispatched `bubbles: true` (mirroring the ratified
380
+ // chart-legend-ui↔chart-ui model), so one fired at an INNER table-ui
381
+ // (e.g. inside an expanded row — table-ui's own row-expansion API makes
382
+ // a nested table a real composition) would otherwise also reach and
383
+ // apply to every ANCESTOR table-ui's listener. The old direct-write code
384
+ // had no such hazard — it targeted the resolved element precisely. The
385
+ // ratified model guards this exact case (`chart.class.js`'s
386
+ // `#onLegendToggle` returns unless the bubbled event's own [for] matches
387
+ // `this.id`); here the equivalent guard is simpler since these events are
388
+ // always dispatched directly at their intended table-ui, never through a
389
+ // [for] filter — `e.target !== this` is the correct, minimal check.
390
+
391
+ #onToolbarSearch = (e) => {
392
+ if (e.target !== this) return;
393
+ this.search = e.detail?.value ?? '';
394
+ };
395
+
396
+ #onToolbarFilterSet = (e) => {
397
+ if (e.target !== this) return;
398
+ const { key, value, op } = e.detail || {};
399
+ if (!key) return;
400
+ this.setFilter(key, value, op);
401
+ };
402
+
403
+ #onToolbarFilterClear = (e) => {
404
+ if (e.target !== this) return;
405
+ this.clearFilters();
406
+ };
407
+
408
+ #onToolbarColumnsSet = (e) => {
409
+ if (e.target !== this) return;
410
+ const columns = e.detail?.columns;
411
+ if (Array.isArray(columns)) this.columns = columns;
412
+ };
413
+
414
+ #onToolbarPaginate = (e) => {
415
+ if (e.target !== this) return;
416
+ this.paginate = Number(e.detail?.pageSize) || 0;
417
+ };
418
+
248
419
  // ── Public API: expansion ──
249
420
 
250
421
  toggleExpand(index) {
@@ -364,8 +535,12 @@ export class UITable extends UIElement {
364
535
  } catch (_) { /* malformed JSON — leave empty, render() shows empty state */ }
365
536
  }
366
537
 
367
- this.setAttribute('role', 'grid');
368
- this.setAttribute('tabindex', '0');
538
+ // gh#1678 — idempotent: a byte-identical SSR-rendered host already
539
+ // carries both attributes, and an unconditional setAttribute() here
540
+ // would still queue a mutation record even though the value never
541
+ // actually changes.
542
+ setAttrIfChanged(this, 'role', 'grid');
543
+ setAttrIfChanged(this, 'tabindex', '0');
369
544
 
370
545
  // Restore persisted state
371
546
  this.#restoreState();
@@ -381,6 +556,18 @@ export class UITable extends UIElement {
381
556
  // pen automatically; Playwright's page.mouse.* synthesizes pointer
382
557
  // events (not mouse events), so a mousedown-only handler tests dead.
383
558
  this.addEventListener('pointerdown', this.#onPointerdown);
559
+ // gh#1764/#1780 (events-only interaction contract, ADR-0079) — the
560
+ // `toolbar-*` command events table-toolbar-ui dispatches AT this
561
+ // element in place of the direct property writes / method calls it
562
+ // used to make. Each handler applies the exact same public-API call
563
+ // the toolbar previously made directly, so observable behavior is
564
+ // unchanged; only the interaction shape moved to events (mirrors
565
+ // chart-legend-ui↔chart-ui's own bubbling-CustomEvent model).
566
+ this.addEventListener('toolbar-search', this.#onToolbarSearch);
567
+ this.addEventListener('toolbar-filter-set', this.#onToolbarFilterSet);
568
+ this.addEventListener('toolbar-filter-clear', this.#onToolbarFilterClear);
569
+ this.addEventListener('toolbar-columns-set', this.#onToolbarColumnsSet);
570
+ this.addEventListener('toolbar-paginate', this.#onToolbarPaginate);
384
571
  }
385
572
  }
386
573
 
@@ -388,6 +575,11 @@ export class UITable extends UIElement {
388
575
  this.removeEventListener('click', this.#onClick);
389
576
  this.removeEventListener('keydown', this.#onKeydown);
390
577
  this.removeEventListener('pointerdown', this.#onPointerdown);
578
+ this.removeEventListener('toolbar-search', this.#onToolbarSearch);
579
+ this.removeEventListener('toolbar-filter-set', this.#onToolbarFilterSet);
580
+ this.removeEventListener('toolbar-filter-clear', this.#onToolbarFilterClear);
581
+ this.removeEventListener('toolbar-columns-set', this.#onToolbarColumnsSet);
582
+ this.removeEventListener('toolbar-paginate', this.#onToolbarPaginate);
391
583
  this.#bound = false;
392
584
  this.#cleanupResize();
393
585
  if (this.#renderRaf) {
@@ -624,8 +816,13 @@ export class UITable extends UIElement {
624
816
 
625
817
  const visCols = this.#visibleColumns;
626
818
 
627
- // Set grid template
628
- this.style.gridTemplateColumns = this.#buildGridTemplate();
819
+ // Set grid template — gh#1678: guard the write, same rationale as the
820
+ // host attribute sets in connected(). Assigning the identical string
821
+ // again is still a real style-attribute mutation, not a no-op.
822
+ const gridTemplate = this.#buildGridTemplate();
823
+ if (this.style.gridTemplateColumns !== gridTemplate) {
824
+ this.style.gridTemplateColumns = gridTemplate;
825
+ }
629
826
 
630
827
  // ── Header row ──
631
828
 
@@ -664,13 +861,12 @@ export class UITable extends UIElement {
664
861
  const row = existing || this.#createRow(idx, visCols);
665
862
  if (existing) this.#updateRow(existing, idx, visCols);
666
863
 
667
- // Expand toggle in first cell
668
- if (this.#isRowExpandable(idx)) {
669
- const isExpanded = this.#expanded.has(idx);
670
- if (isExpanded) row.setAttribute('data-expanded', '');
671
- else row.removeAttribute('data-expanded');
864
+ // Expand toggle in first cell — gh#1678: idempotent (see the
865
+ // module-level helpers' own rationale).
866
+ if (this.#isRowExpandable(idx) && this.#expanded.has(idx)) {
867
+ setAttrIfChanged(row, 'data-expanded', '');
672
868
  } else {
673
- row.removeAttribute('data-expanded');
869
+ removeAttrIfPresent(row, 'data-expanded');
674
870
  }
675
871
 
676
872
  bodyChildren.push(row);
@@ -735,6 +931,18 @@ export class UITable extends UIElement {
735
931
  const allSelected = this.#data.length > 0 && this.#selected.size === this.#data.length;
736
932
  const cells = [];
737
933
 
934
+ // Expand column header — an empty placeholder for the reserved
935
+ // expand-toggle track. Must precede the checkbox header, matching the
936
+ // grid template and body-row cell order (gh#1654: with no placeholder,
937
+ // every header label sat one column left of its data).
938
+ if (this.expandable) {
939
+ const cell = document.createElement('div');
940
+ cell.setAttribute('role', 'columnheader');
941
+ cell.setAttribute('data-expand-col', '');
942
+ cell.setAttribute('aria-label', 'Expand');
943
+ cells.push(cell);
944
+ }
945
+
738
946
  // Checkbox column header
739
947
  if (this.selectable) {
740
948
  const cell = document.createElement('div');
@@ -813,12 +1021,10 @@ export class UITable extends UIElement {
813
1021
  cells.push(cell);
814
1022
  }
815
1023
 
816
- // Reconcile header cells
817
- while (header.children.length > cells.length) header.lastChild.remove();
818
- for (let i = 0; i < cells.length; i++) {
819
- if (header.children[i]) header.replaceChild(cells[i], header.children[i]);
820
- else header.appendChild(cells[i]);
821
- }
1024
+ // Reconcile header cells — gh#1678: adopt-or-diff instead of an
1025
+ // unconditional replaceChild per position (see the module-level
1026
+ // adoptOrDiffChildren() doc comment for why this is safe under SSR).
1027
+ adoptOrDiffChildren(header, cells);
822
1028
  }
823
1029
 
824
1030
  // ── Row Builders ───────────────────────────────────────────────────────────
@@ -833,12 +1039,16 @@ export class UITable extends UIElement {
833
1039
  #updateRow(row, dataIndex, visCols) {
834
1040
  const data = this.#data[dataIndex];
835
1041
  const isSelected = this.#selected.has(dataIndex);
836
- row.dataset.index = dataIndex;
1042
+ // gh#1678 — idempotent: `#updateRow` runs on an ADOPTED (SSR-matched)
1043
+ // row exactly as often as on a freshly-created one, and a plain
1044
+ // `row.dataset.index = …` / `setAttribute` always mutates regardless of
1045
+ // whether the value already matches.
1046
+ setAttrIfChanged(row, 'data-index', String(dataIndex));
837
1047
 
838
1048
  if (isSelected) {
839
- row.setAttribute('aria-selected', 'true');
1049
+ setAttrIfChanged(row, 'aria-selected', 'true');
840
1050
  } else {
841
- row.removeAttribute('aria-selected');
1051
+ removeAttrIfPresent(row, 'aria-selected');
842
1052
  }
843
1053
 
844
1054
  const cells = [];
@@ -884,6 +1094,12 @@ export class UITable extends UIElement {
884
1094
  const value = getCellValue(data, col);
885
1095
 
886
1096
  // Render priority: column.render > column.format > cellType.render > text
1097
+ // gh#1678 CodeRabbit follow-up: `col.render()` and a cell-type renderer
1098
+ // are free to attach listeners / runtime state to the node they hand
1099
+ // back — mark the cell RENDERER_OWNED so adoptOrDiffChildren() never
1100
+ // adopts it on structural equality alone (see that function's own doc
1101
+ // comment). `col.format()` and the plain-text fallback below build
1102
+ // nothing but a static <span>, so they're never marked.
887
1103
  if (typeof col.render === 'function') {
888
1104
  const result = col.render(value, data, cell, dataIndex);
889
1105
  if (isNode(result)) {
@@ -891,6 +1107,7 @@ export class UITable extends UIElement {
891
1107
  } else if (typeof result === 'string') {
892
1108
  cell.innerHTML = result;
893
1109
  }
1110
+ RENDERER_OWNED.add(cell);
894
1111
  } else if (typeof col.format === 'function') {
895
1112
  const t = col.format(value, data);
896
1113
  cell.appendChild(Object.assign(document.createElement('span'), { textContent: t }));
@@ -899,6 +1116,16 @@ export class UITable extends UIElement {
899
1116
  const typeDef = cellTypes[col.type || 'text'];
900
1117
  if (typeDef?.render) {
901
1118
  typeDef.render(value, data, cell, col.meta);
1119
+ // Most built-in cell-type renderers only set attributes on
1120
+ // custom elements (badge-ui, avatar-ui, check-ui, …) — safe to
1121
+ // adopt structurally, since their own runtime behavior lives in
1122
+ // their own connectedCallback, independent of node identity. Only
1123
+ // a type that declares `attachesListeners` (currently: 'actions',
1124
+ // which calls addEventListener() directly) is marked renderer-
1125
+ // owned; marking every typeDef.render() cell would defeat the
1126
+ // zero-mutation adoption for the overwhelming common case (plain
1127
+ // text/number/date/badge/... cells) for no correctness benefit.
1128
+ if (typeDef.attachesListeners) RENDERER_OWNED.add(cell);
902
1129
  } else {
903
1130
  const t = value != null ? String(value) : '';
904
1131
  cell.appendChild(Object.assign(document.createElement('span'), { textContent: t }));
@@ -932,12 +1159,13 @@ export class UITable extends UIElement {
932
1159
  cells.push(cell);
933
1160
  }
934
1161
 
935
- // Reconcile cells in row
936
- while (row.children.length > cells.length) row.lastChild.remove();
937
- for (let i = 0; i < cells.length; i++) {
938
- if (row.children[i]) row.replaceChild(cells[i], row.children[i]);
939
- else row.appendChild(cells[i]);
940
- }
1162
+ // Reconcile cells in row — gh#1678: adopt-or-diff (see
1163
+ // adoptOrDiffChildren()'s doc comment). This is the fix's main hot
1164
+ // path: `#updateRow` is called on every already-adopted row found by
1165
+ // `data-index` at connect, so this is what stops a byte-identical
1166
+ // SSR-rendered row from having every one of its cells rebuilt at
1167
+ // upgrade.
1168
+ adoptOrDiffChildren(row, cells);
941
1169
  }
942
1170
 
943
1171
  // ── Overlays ───────────────────────────────────────────────────────────────
@@ -1067,10 +1295,14 @@ export class UITable extends UIElement {
1067
1295
  default: result = '';
1068
1296
  }
1069
1297
 
1070
- // Format using cell type if available
1298
+ // Format using cell type if available. gh#1678 CodeRabbit follow-up:
1299
+ // same attachesListeners-only scoping as the body-row cell path
1300
+ // above — only a cell type that declares it attaches listeners
1301
+ // needs the always-replace guard.
1071
1302
  const typeDef = cellTypes[col.type || 'text'];
1072
1303
  if (typeof result === 'number' && typeDef?.render) {
1073
1304
  typeDef.render(result, {}, cell, col.meta);
1305
+ if (typeDef.attachesListeners) RENDERER_OWNED.add(cell);
1074
1306
  } else {
1075
1307
  cell.textContent = typeof result === 'number' ? result.toLocaleString() : result;
1076
1308
  }
@@ -1079,12 +1311,11 @@ export class UITable extends UIElement {
1079
1311
  cells.push(cell);
1080
1312
  }
1081
1313
 
1082
- // Reconcile cells
1083
- while (aggRow.children.length > cells.length) aggRow.lastChild.remove();
1084
- for (let i = 0; i < cells.length; i++) {
1085
- if (aggRow.children[i]) aggRow.replaceChild(cells[i], aggRow.children[i]);
1086
- else aggRow.appendChild(cells[i]);
1087
- }
1314
+ // Reconcile cells — gh#1678: adopt-or-diff, same helper as the header
1315
+ // and body-row cell reconciliation above (consistency; the aggregation
1316
+ // row is built from the exact same per-cell full-rebuild shape those
1317
+ // had).
1318
+ adoptOrDiffChildren(aggRow, cells);
1088
1319
  }
1089
1320
 
1090
1321
  // ── Pagination ─────────────────────────────────────────────────────────────
@@ -1505,7 +1736,7 @@ export class UITable extends UIElement {
1505
1736
 
1506
1737
  #onKeydown = (e) => {
1507
1738
  const visCols = this.#visibleColumns;
1508
- const totalCols = visCols.length + (this.selectable ? 1 : 0);
1739
+ const totalCols = visCols.length + (this.expandable ? 1 : 0) + (this.selectable ? 1 : 0);
1509
1740
  const body = this.querySelector(':scope > [data-body]');
1510
1741
  const totalRows = body ? body.children.length : 0;
1511
1742
 
@@ -303,9 +303,18 @@
303
303
 
304
304
  /* text-overflow on a flex container doesn't reach text nodes (anonymous
305
305
  flex items). Built-in cell types now wrap text in <span>; link type
306
- creates <a>; avatar/progress composite types use row-ui > span. */
306
+ creates <a>; avatar/progress composite types use row-ui > span.
307
+
308
+ `[data-truncate]` (gh#1681) extends the identical contract to
309
+ consumer-rendered `column.render` cells, whose output shape isn't
310
+ guaranteed to be a bare <span>/<a> — a render() function sets it on
311
+ whichever element (any tag, including a custom element) should
312
+ single-line-ellipsis, the same way a column author sets [data-wrap] to
313
+ opt a built-in cell INTO wrapping. Undocumented before this: `render`
314
+ cells clipped with no ellipsis and no opt-in path. */
307
315
  [data-body] [role="gridcell"] > span,
308
- [data-body] [role="gridcell"] > a {
316
+ [data-body] [role="gridcell"] > a,
317
+ [data-body] [role="gridcell"] > [data-truncate] {
309
318
  min-width: 0;
310
319
  flex: 1;
311
320
  overflow: hidden;
@@ -351,8 +360,10 @@
351
360
 
352
361
  :scope[wrap] [data-body] [role="gridcell"] > span,
353
362
  :scope[wrap] [data-body] [role="gridcell"] > a,
363
+ :scope[wrap] [data-body] [role="gridcell"] > [data-truncate],
354
364
  [data-body] [role="gridcell"][data-wrap] > span,
355
- [data-body] [role="gridcell"][data-wrap] > a {
365
+ [data-body] [role="gridcell"][data-wrap] > a,
366
+ [data-body] [role="gridcell"][data-wrap] > [data-truncate] {
356
367
  white-space: normal;
357
368
  overflow: visible;
358
369
  text-overflow: clip;
@@ -417,6 +428,15 @@
417
428
  color: var(--table-fg-hover);
418
429
  }
419
430
 
431
+ /* Clickable-row affordance (gh#1680) — adiav2's zero-overrides purge
432
+ deleted an app-side [data-clickable] cursor rule with no kit-side
433
+ replacement. `row-click` fires unconditionally regardless of whether a
434
+ listener cares, so cursor can't be inferred from listener presence;
435
+ `clickable-rows` is an explicit opt-in instead. */
436
+ :scope[clickable-rows] [data-body] > [role="row"] {
437
+ cursor: pointer;
438
+ }
439
+
420
440
  /* Selected row — must out-specify the base row rule `[data-body] >
421
441
  [role="row"]` (0,2,0) and the hover rule (0,2,1), or the highlight never
422
442
  paints. Scoping to `[data-body] > [role="row"][aria-selected="true"]` lifts
@@ -466,10 +486,13 @@
466
486
  /* ═══════ Density ═══════ */
467
487
 
468
488
  /* No local override (ADR-0054 convergence) — --table-py/-px above already
469
- derive from --a-space-* steps, which the ambient global
470
- [density="compact"|"spacious"] attribute already scales via
471
- --a-density. Do not reintroduce a component-local :scope[density=…]
472
- block; that would re-shadow the global grammar (ADR-0053). */
489
+ derive from --a-space-* steps, which the global [density="compact"|
490
+ "spacious"] attribute scales via --a-density on ANY host, root or
491
+ scoped (gh#1771 — api/sizing.css re-declares the --a-space-* formulas
492
+ at the [density] boundary itself, the gh#570 cure; previously this
493
+ comment's "already scales" claim only held for root-level [density]).
494
+ Do not reintroduce a component-local :scope[density=…] block; that
495
+ would re-shadow the global grammar (ADR-0053). */
473
496
 
474
497
  /* ═══════ Pinned cells ═══════ */
475
498
 
@@ -534,9 +557,14 @@
534
557
 
535
558
  /* ═══════ Overlays (empty / loading / error) ═══════ */
536
559
 
537
- [data-overlay],
538
- [data-empty],
539
- [data-loading] {
560
+ /* Scoped to the host's direct children — gh#1700, hygiene pass matching
561
+ the command-ui (gh#1697/#1698) and combobox-ui (gh#1700) fix shape.
562
+ Single-consumer today (only table.class.js's own `:scope >
563
+ [data-empty]` / `:scope > [data-loading]` overlay markers), but a bare
564
+ selector here is one nested consumer away from the same leak. */
565
+ :scope > [data-overlay],
566
+ :scope > [data-empty],
567
+ :scope > [data-loading] {
540
568
  grid-column: 1 / -1;
541
569
  display: flex;
542
570
  flex-direction: column;
@@ -86,7 +86,9 @@ export interface TableSortEventDetail {
86
86
  export type TableSortEvent = CustomEvent<TableSortEventDetail>;
87
87
 
88
88
  export class UITable extends UIElement {
89
- /** Column definitions. Array of {key, label, type?, width?, minWidth?, maxWidth?, flex?, sortable?, resizable?, filterable?, pinned?, hidden?, wrap?, valign?, accessor?, format?, render?, sortFn?, filterType?, meta?}. `valign` ('top'|'center') overrides the host [valign] for that column's body cells only header cells always stay centered. Alternative to declarative <col-def> children (<col-def valign="top">); col-def also accepts `meta` as a JSON attribute (gh#925) — e.g. <col-def type="badge" meta='{"variants":{"Online":"success"}}'> — so cell-type meta (badge variant maps, currency codes) is expressible in static HTML. */
89
+ /** Renders body rows with `cursor: pointer`. `row-click` fires unconditionally whether or not a listener is attached, so the cursor can't be inferred from listener presenceset this explicitly when row-click behavior is wired up (master-detail, open-flyout, navigation) so rows read as actionable. */
90
+ clickableRows: boolean;
91
+ /** Column definitions. Array of {key, label, type?, width?, minWidth?, maxWidth?, flex?, sortable?, resizable?, filterable?, pinned?, hidden?, wrap?, valign?, accessor?, format?, render?, sortFn?, filterType?, meta?}. `valign` ('top'|'center') overrides the host [valign] for that column's body cells only — header cells always stay centered. Alternative to declarative <col-def> children (<col-def valign="top">); col-def also accepts `meta` as a JSON attribute (gh#925) — e.g. <col-def type="badge" meta='{"variants":{"Online":"success"}}'> — so cell-type meta (badge variant maps, currency codes) is expressible in static HTML. Truncation contract for `render` cells (gh#1681) — a `render(value, row, cell, dataIndex)` function's returned markup isn't guaranteed to be a bare <span>/<a> the way built-in cell types are, so single-line ellipsis truncation isn't automatic; set `data-truncate` on whichever rendered element (any tag, including a custom element) should single-line-ellipsis to opt it into the same contract built-in cells get for free. `[wrap]` (host) / `[data-wrap]` (per-column) override it back to multi-line, matching built-in cells. */
90
92
  columns: string;
91
93
  /** JS property (set programmatically — `el.data = [...]`) — row records, an array of plain objects keyed to columns[].key. May also be supplied declaratively as a JSON-array `data="[…]"` attribute, hydrated once at connect (gh#288) — the same pattern chart-ui's `data` attribute uses. Custom accessor on the element class, not a reflected attribute. */
92
94
  data: string;
@@ -12,8 +12,8 @@
12
12
  <table-ui id="api-selectable" selectable></table-ui>
13
13
  ```
14
14
 
15
- ## expandable
15
+ ## render cell truncation (data-truncate)
16
16
 
17
17
  ```html
18
- <table-ui id="api-expandable" expandable></table-ui>
18
+ <table-ui id="api-render-truncate"></table-ui>
19
19
  ```
@@ -20,7 +20,7 @@ composes:
20
20
  - badge-ui
21
21
  props:
22
22
  columns:
23
- description: Column definitions. Array of {key, label, type?, width?, minWidth?, maxWidth?, flex?, sortable?, resizable?, filterable?, pinned?, hidden?, wrap?, valign?, accessor?, format?, render?, sortFn?, filterType?, meta?}. `valign` ('top'|'center') overrides the host [valign] for that column's body cells only — header cells always stay centered. Alternative to declarative <col-def> children (<col-def valign="top">); col-def also accepts `meta` as a JSON attribute (gh#925) — e.g. <col-def type="badge" meta='{"variants":{"Online":"success"}}'> — so cell-type meta (badge variant maps, currency codes) is expressible in static HTML.
23
+ description: Column definitions. Array of {key, label, type?, width?, minWidth?, maxWidth?, flex?, sortable?, resizable?, filterable?, pinned?, hidden?, wrap?, valign?, accessor?, format?, render?, sortFn?, filterType?, meta?}. `valign` ('top'|'center') overrides the host [valign] for that column's body cells only — header cells always stay centered. Alternative to declarative <col-def> children (<col-def valign="top">); col-def also accepts `meta` as a JSON attribute (gh#925) — e.g. <col-def type="badge" meta='{"variants":{"Online":"success"}}'> — so cell-type meta (badge variant maps, currency codes) is expressible in static HTML. Truncation contract for `render` cells (gh#1681) — a `render(value, row, cell, dataIndex)` function's returned markup isn't guaranteed to be a bare <span>/<a> the way built-in cell types are, so single-line ellipsis truncation isn't automatic; set `data-truncate` on whichever rendered element (any tag, including a custom element) should single-line-ellipsis to opt it into the same contract built-in cells get for free. `[wrap]` (host) / `[data-wrap]` (per-column) override it back to multi-line, matching built-in cells.
24
24
  type: array
25
25
  default: []
26
26
  dynamic: true # JS-set collection prop with a custom setter — deliberately not in static properties
@@ -85,6 +85,14 @@ props:
85
85
  range select.
86
86
  type: boolean
87
87
  default: false
88
+ clickable-rows:
89
+ description: >-
90
+ Renders body rows with `cursor: pointer`. `row-click` fires unconditionally
91
+ whether or not a listener is attached, so the cursor can't be inferred from
92
+ listener presence — set this explicitly when row-click behavior is wired up
93
+ (master-detail, open-flyout, navigation) so rows read as actionable.
94
+ type: boolean
95
+ default: false
88
96
  sortable:
89
97
  description: Enable click-to-sort on column headers. Supports multi-sort via Shift+click.
90
98
  type: boolean
@@ -294,7 +302,12 @@ a2ui:
294
302
  needs search / filter / sort / columns visibility. Do NOT
295
303
  re-implement those affordances in the card header — the toolbar
296
304
  auto-wires search/filter/sort/columns changes into the bound
297
- table.
305
+ table via `toolbar-*` CustomEvents (gh#1764/#1780, ADR-0079 —
306
+ events-only interaction contract; table-ui listens for
307
+ toolbar-search/toolbar-filter-set/toolbar-filter-clear/
308
+ toolbar-columns-set/toolbar-paginate on itself). Any consumer, not
309
+ only table-toolbar-ui, may dispatch these events directly at a
310
+ table-ui instance.
298
311
  - >-
299
312
  Cells truncate single-line by default (v0.6.21 §403
300
313
  truncate-default). Opt out per-table with [wrap] for whole-table