@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
@@ -18,7 +18,9 @@
18
18
  * • title + optional count badge
19
19
  * • filter / sort / columns popover buttons
20
20
  * • search input
21
- * • optional [slot="actions"] trailing region
21
+ * • optional [slot="actions-leading"] region, BEFORE the page-size select (gh#1649)
22
+ * • optional page-size select
23
+ * • optional [slot="actions"] trailing region, AFTER the page-size select
22
24
  *
23
25
  * [for] resolution mirrors chart-legend-ui — the toolbar mounts an element by
24
26
  * id, then dispatches state changes against it. When [for] is absent, falls
@@ -27,18 +29,43 @@
27
29
  * Popovers use the platform Popover API + core/anchor.js, the same primitives
28
30
  * that menu-ui / popover-ui / toolbar-ui already use in this package.
29
31
  *
30
- * State flow:
31
- * search → table.search (string property)
32
- * filters → table.setFilter() (per-key)
33
- * sort → simulated click on table's [data-sort-key] header
34
- * column hidden table.columns = (clone with hidden flag flipped)
32
+ * State flow — events-only interaction contract (gh#1764/#1780, ADR-0079;
33
+ * mirrors chart-legend-ui↔chart-ui's own bubbling-CustomEvent model). The
34
+ * toolbar never writes a property or calls a method on the bound table
35
+ * directly every command is a `toolbar-*` CustomEvent dispatched AT the
36
+ * resolved target, which table.class.js listens for in its own
37
+ * connected()/disconnected() and applies via its existing public surface:
38
+ * search → `toolbar-search` { value } → table.search =
39
+ * filters → `toolbar-filter-set` { key, value, op } → table.setFilter()
40
+ * filters clear → `toolbar-filter-clear` {} → table.clearFilters()
41
+ * sort → simulated click on table's [data-sort-key] header (already
42
+ * event-shaped — a real DOM event dispatched at the target,
43
+ * not a property write; left as-is, see gh#1780's PR notes)
44
+ * column hidden → `toolbar-columns-set` { columns } → table.columns =
45
+ * page size → `toolbar-paginate` { pageSize } → table.paginate =
46
+ * The table→toolbar direction (`sort`, `filter-change` listened on #target)
47
+ * was already events-only and is unchanged.
35
48
  */
36
49
 
37
50
  import { UIElement } from '../../core/element.js';
38
- import { anchorPopover } from '../../core/anchor.js';
51
+ import { anchorPopover, resolveLengthPx } from '../../core/anchor.js';
39
52
 
40
53
  const SEARCH_DEBOUNCE = 200;
41
54
 
55
+ // ADR-0076 REQ-M-006 — the three breakpoints' NORMATIVE default pixel
56
+ // widths (56rem/40rem/26rem at the standard 16px root), duplicated here
57
+ // ONLY as a fallback for #classifyStage() below when the live
58
+ // `--table-toolbar-bp-*` custom property can't be read (no table-toolbar.css
59
+ // stylesheet attached yet, an SSR shim, or a unit-test harness with no real
60
+ // cascade) — resolveLengthPx() returns NaN in that case, `||` falls
61
+ // through to these. Table-toolbar.css's own `:where(:scope)` declarations
62
+ // are the actual source of truth whenever a real stylesheet IS present;
63
+ // this fallback exists so the reflection degrades to the DOCUMENTED
64
+ // default instead of silently misclassifying against NaN.
65
+ const DEFAULT_BP_SEARCH_TIGHT_PX = 56 * 16;
66
+ const DEFAULT_BP_ICON_ONLY_PX = 40 * 16;
67
+ const DEFAULT_BP_OVERFLOW_PX = 26 * 16;
68
+
42
69
  function emptyHint(text) {
43
70
  const el = document.createElement('text-ui');
44
71
  el.setAttribute('data-popover-empty', '');
@@ -136,14 +163,28 @@ export class UITableToolbar extends UIElement {
136
163
  noSort: { type: Boolean, default: false, reflect: true, attribute: 'no-sort' },
137
164
  noColumns: { type: Boolean, default: false, reflect: true, attribute: 'no-columns' },
138
165
  noSearch: { type: Boolean, default: false, reflect: true, attribute: 'no-search' },
166
+ // ADR-0076 (ADIA2-9123) — additive sugar over the four `no-*` attributes
167
+ // above. Pure, absolute OR: see #updateControlVisibility(), which is the
168
+ // single place that combines chromeOnly with each granular no-* value.
169
+ chromeOnly: { type: Boolean, default: false, reflect: true, attribute: 'chrome-only' },
139
170
  placeholder: { type: String, default: 'Search...', reflect: false },
140
171
  variant: { type: String, default: 'default', reflect: true },
172
+ // ADR-0076 REQ-O-001 — explicit compaction-stage override. Unset (the
173
+ // default, empty string) means auto-snap via the @container query
174
+ // (table-toolbar.css's :scope container-type/container-name). All four
175
+ // stages' CSS is now live: search-tight steps the search field down
176
+ // (step 2); icon-only drops control labels and compacts/hides the
177
+ // range summary (steps 3+4); overflow collapses Filter/Sort/Columns
178
+ // into one "More" trigger (step 5).
179
+ stage: { type: String, default: '', reflect: true },
141
180
  };
142
181
 
143
182
  // §205 (v0.5.7): dynamic sort-indicator icons (table-toolbar.class.js:576 — nested ternary
144
183
  // `dir === 'asc' ? 'arrow-up' : dir === 'desc' ? 'arrow-down' : 'caret-up-down'`).
145
184
  // Per FEEDBACK-16 §1 + §209 slot-11 ternary-walker discovery.
146
- static requiredIcons = ['arrow-up', 'arrow-down', 'caret-up-down'];
185
+ // 'dots-three-vertical' — the overflow-stage "More" trigger icon
186
+ // (ADR-0076 REQ-S-004, PLAN step 5, #mkOverflowTrigger()).
187
+ static requiredIcons = ['arrow-up', 'arrow-down', 'caret-up-down', 'dots-three-vertical'];
147
188
 
148
189
  static template = () => null;
149
190
 
@@ -154,6 +195,17 @@ export class UITableToolbar extends UIElement {
154
195
  #docListenerRaf = null;
155
196
  #sortIndicatorRafs = new Set();
156
197
  #pageSizeOptions = [];
198
+ // ADR-0076 REQ-M-005 (ADIA2-9123 S4-ii), PLAN step 7 — data-stage-resolved
199
+ // reflection. #resizeObs classifies live width against the SAME three
200
+ // breakpoints table-toolbar.css's @container rules use (read from the
201
+ // custom properties themselves, never a second hardcoded copy of
202
+ // 56/40/26 — see #classifyStage()). #lastWidthPx caches the last
203
+ // measured inline-size so a `stage` attribute change alone (no resize)
204
+ // can still re-classify without waiting on the next ResizeObserver
205
+ // callback (REQ-M-005's dual-trigger requirement).
206
+ #resizeObs = null;
207
+ #lastWidthPx = null;
208
+ #stageRaf = null;
157
209
 
158
210
  // ── Public API: pageSizeOptions ──────────────────────────────────────────
159
211
  // Dynamic array prop (JS-set collection, custom setter) — same shape as
@@ -195,6 +247,34 @@ export class UITableToolbar extends UIElement {
195
247
  this.#stamp();
196
248
  this.#resolveTarget();
197
249
  this.#syncFromTarget();
250
+
251
+ // ADR-0076 REQ-M-005, PLAN step 7 — classify synchronously off
252
+ // `clientWidth` at connect time too (0 pre-layout treated as "no
253
+ // measurement yet", the same `null`-width fallback #reflectResolvedStage
254
+ // uses) rather than waiting on the ResizeObserver's own first
255
+ // (asynchronous) callback — avoids a brief connect-time gap with no
256
+ // `data-stage-resolved` reflected at all, mirroring how chart.class.js's
257
+ // own `data-ratio-resolved` is set synchronously from `#dims()` at
258
+ // first render(), not deferred to its ResizeObserver's first tick.
259
+ this.#lastWidthPx = this.clientWidth || null;
260
+ this.#reflectResolvedStage();
261
+
262
+ // gh#285 SSR DOM shims (linkedom) have no ResizeObserver global, same
263
+ // guard chart.class.js's own ResizeObserver setup uses.
264
+ if (typeof ResizeObserver !== 'undefined') {
265
+ this.#resizeObs = new ResizeObserver((entries) => {
266
+ const { inlineSize } = entries[0].contentBoxSize[0];
267
+ this.#lastWidthPx = inlineSize;
268
+ // rAF-batch, mirroring chart.class.js's own ResizeObserver setup —
269
+ // avoids a same-tick reflect→layout→observer-refire loop.
270
+ if (this.#stageRaf) return;
271
+ this.#stageRaf = requestAnimationFrame(() => {
272
+ this.#stageRaf = null;
273
+ this.#reflectResolvedStage();
274
+ });
275
+ });
276
+ this.#resizeObs.observe(this);
277
+ }
198
278
  }
199
279
 
200
280
  // gh#1620 — this used to live in a declared `attributeChanged(name)` hook
@@ -204,7 +284,8 @@ export class UITableToolbar extends UIElement {
204
284
  // hook, keyed by the matching property names.
205
285
  updated(changed) {
206
286
  if (changed.has('noFilter') || changed.has('noSort')
207
- || changed.has('noColumns') || changed.has('noSearch')) {
287
+ || changed.has('noColumns') || changed.has('noSearch')
288
+ || changed.has('chromeOnly')) {
208
289
  this.#updateControlVisibility();
209
290
  }
210
291
  if (changed.has('placeholder')) {
@@ -213,6 +294,30 @@ export class UITableToolbar extends UIElement {
213
294
  }
214
295
  }
215
296
 
297
+ // ADR-0076 REQ-E-001 — `updated(changed)` alone MISSES a live loading→
298
+ // confirmed-zero transition: `rangeTotal`'s default is already 0, so
299
+ // setting `range-total="0"` from an unset start parses to the SAME
300
+ // Number(0), the base class's property setter's `Object.is` short-circuit
301
+ // swallows the signal write (core/element.js `installProps()`), and
302
+ // `changed` never gets a `rangeTotal` entry — `#updateSummary()`'s
303
+ // hasAttribute-based branch never re-runs. Verified live in a browser
304
+ // (not just the vitest suite, which happened to only exercise the
305
+ // positive→zero transition, where the signal DOES change). REQ-E-001's
306
+ // check is fundamentally an ATTRIBUTE-presence concern, not a coerced-
307
+ // value one, so it needs the raw attribute callback, not the signal diff.
308
+ attributeChangedCallback(name, oldVal, newVal) {
309
+ super.attributeChangedCallback(name, oldVal, newVal);
310
+ if (name === 'range-total') this.#updateSummary();
311
+ // ADR-0076 REQ-M-005 — the SECOND of the two triggers `data-stage-
312
+ // resolved` must react to: a live resize (the ResizeObserver callback
313
+ // above) AND a `stage` attribute change alone, with no resize needed.
314
+ // Re-classifies against the cached last-measured width — a pin set
315
+ // before the first resize observation (#lastWidthPx still null) simply
316
+ // reports the pin; classifyStage() below treats a null width as "no
317
+ // live width to compare against yet", never a false 0-width match.
318
+ if (name === 'stage') this.#reflectResolvedStage();
319
+ }
320
+
216
321
  disconnected() {
217
322
  if (this.#docListenerRaf != null) {
218
323
  cancelAnimationFrame(this.#docListenerRaf);
@@ -222,6 +327,68 @@ export class UITableToolbar extends UIElement {
222
327
  this.#sortIndicatorRafs.clear();
223
328
  this.#closePopover();
224
329
  this.#detachTarget();
330
+ if (this.#stageRaf != null) {
331
+ cancelAnimationFrame(this.#stageRaf);
332
+ this.#stageRaf = null;
333
+ }
334
+ this.#resizeObs?.disconnect();
335
+ this.#resizeObs = null;
336
+ }
337
+
338
+ // ── data-stage-resolved reflection (ADR-0076 REQ-M-005, ADIA2-9123 ─────────
339
+ // S4-ii, PLAN step 7) ────────────────────────────────────────────────────
340
+ //
341
+ // Purely informational — mirrors the CSS-decided stage for a consumer's
342
+ // own [slot="actions-leading"] content, never drives table-toolbar's own
343
+ // rendering (REQ-S stays 100% CSS `@container`-driven, REQ-M-002). The
344
+ // three breakpoints are read live off the SAME custom properties the CSS
345
+ // `@container` rules consume (--table-toolbar-bp-search-tight/-icon-only/
346
+ // -overflow) via `resolveLengthPx` (core/anchor.js, shared with its
347
+ // --popover-max-height use) — one source of truth, never a second
348
+ // hardcoded copy of 56/40/26 here.
349
+
350
+ #classifyStage(widthPx) {
351
+ const cs = getComputedStyle(this);
352
+ // resolveLengthPx() returns NaN when the custom property is unreadable
353
+ // — no table-toolbar.css stylesheet attached yet, an SSR shim, or a
354
+ // headless unit-test harness with no real cascade — so an explicit
355
+ // Number.isFinite() check falls through to each breakpoint's own
356
+ // documented REQ-M-006 default. This must be a real NaN check, not a
357
+ // `|| DEFAULT` fallback: `0` is a legitimate consumer override (e.g.
358
+ // `--table-toolbar-bp-overflow: 0rem` to disable the overflow stage
359
+ // entirely) and `||` would silently discard it as falsy, substituting
360
+ // the default instead — reviewer finding on PR #1676, fixed here.
361
+ // Whenever a real stylesheet IS present, the live custom property
362
+ // (overridable per-consumer, same posture as every other
363
+ // --table-toolbar-* token) wins, zero included.
364
+ const resolveBp = (raw, fallback) => {
365
+ const v = resolveLengthPx(raw);
366
+ return Number.isFinite(v) ? v : fallback;
367
+ };
368
+ const searchTightPx = resolveBp(cs.getPropertyValue('--table-toolbar-bp-search-tight'), DEFAULT_BP_SEARCH_TIGHT_PX);
369
+ const iconOnlyPx = resolveBp(cs.getPropertyValue('--table-toolbar-bp-icon-only'), DEFAULT_BP_ICON_ONLY_PX);
370
+ const overflowPx = resolveBp(cs.getPropertyValue('--table-toolbar-bp-overflow'), DEFAULT_BP_OVERFLOW_PX);
371
+
372
+ // Inclusive-on-the-narrower-side (max-width semantics, REQ-M-006) — a
373
+ // width exactly AT a boundary classifies to the narrower stage, same
374
+ // rule the CSS `@container (max-width: …)` conditions already encode.
375
+ if (widthPx <= overflowPx) return 'overflow';
376
+ if (widthPx <= iconOnlyPx) return 'icon-only';
377
+ if (widthPx <= searchTightPx) return 'search-tight';
378
+ return 'full';
379
+ }
380
+
381
+ #reflectResolvedStage() {
382
+ // REQ-O-001/REQ-M-005 — the pin wins for the reflection exactly as it
383
+ // wins for rendering: an explicit `stage` reports itself, never the
384
+ // width-derived classification, even inside a container sized for a
385
+ // different stage.
386
+ const resolved = this.stage || (this.#lastWidthPx == null ? 'full' : this.#classifyStage(this.#lastWidthPx));
387
+ // Is-different guard — same shape as chart.class.js's own
388
+ // data-ratio-resolved reflection (chart.class.js:533).
389
+ if (this.getAttribute('data-stage-resolved') !== resolved) {
390
+ this.setAttribute('data-stage-resolved', resolved);
391
+ }
225
392
  }
226
393
 
227
394
  render() {
@@ -357,12 +524,51 @@ export class UITableToolbar extends UIElement {
357
524
  summaryText.setAttribute('data-summary-text', '');
358
525
  summary.appendChild(summaryText);
359
526
 
527
+ // ADR-0076 REQ-S-003 — short-form compact text ("1–25/320"), a sibling
528
+ // of [data-summary-text] holding the SAME range values in a terser
529
+ // shape. Which of the two is visible is decided entirely by CSS
530
+ // (table-toolbar.css's icon-only @container rule / [stage] pin) —
531
+ // both nodes are always populated together in #updateSummary(), never
532
+ // a JS branch keyed on a resolved-stage signal (REQ-M-002 stays true:
533
+ // no JS decides rendering here, only which text each stage's CSS shows).
534
+ const summaryCompact = document.createElement('span');
535
+ summaryCompact.setAttribute('data-summary-compact', '');
536
+ summaryCompact.hidden = true;
537
+ summary.appendChild(summaryCompact);
538
+
539
+ // Empty-state slot (ADR-0076 REQ-E, adiav2 ADIA2-9123) — renders in
540
+ // [data-summary]'s own position, alongside [data-summary-text], never a
541
+ // separate region: only one of the two is ever visible at a time (see
542
+ // #updateSummary()). Same real-insertion-point pattern as [slot="scope"]
543
+ // / [slot="actions"] / [slot="actions-leading"] above — move any
544
+ // pre-existing [slot="empty"] children into this stamped wrapper rather
545
+ // than declaring a template-owned `parts:` entry (ADR-0067).
546
+ const summaryEmpty = document.createElement('div');
547
+ summaryEmpty.setAttribute('data-summary-empty', '');
548
+ summaryEmpty.hidden = true;
549
+ for (const node of [...this.children]) {
550
+ if (node === root) continue;
551
+ if (node.getAttribute?.('slot') === 'empty') {
552
+ summaryEmpty.appendChild(node);
553
+ }
554
+ }
555
+ summary.appendChild(summaryEmpty);
556
+
360
557
  // Controls cluster
361
558
  const controls = document.createElement('div');
362
559
  controls.setAttribute('data-controls', '');
363
560
  controls.appendChild(this.#mkButton('filter', 'Filter', 'funnel-simple'));
364
561
  controls.appendChild(this.#mkButton('sort', 'Sort', 'arrows-down-up'));
365
562
  controls.appendChild(this.#mkButton('columns', 'Columns', 'columns'));
563
+ // ADR-0076 REQ-S-004/REQ-X-003, PLAN step 5 — overflow-stage "More"
564
+ // trigger. Stamped unconditionally alongside the three granular
565
+ // buttons above (never removed/re-created), CSS-hidden except at the
566
+ // overflow breakpoint/pin (REQ-M-002 stays true: which cluster paints
567
+ // is a pure CSS decision, not a JS branch). Keeping all four buttons
568
+ // permanently in the DOM is the simplest way to honor REQ-X-004's
569
+ // tab-order guarantee — nothing is ever created/destroyed across a
570
+ // stage transition, only shown/hidden.
571
+ controls.appendChild(this.#mkOverflowTrigger());
366
572
 
367
573
  // Search — compose <search-ui>, which already stamps input-ui with
368
574
  // the magnifying-glass prefix + clear suffix and debounces a `search`
@@ -389,6 +595,25 @@ export class UITableToolbar extends UIElement {
389
595
  pageSize.hidden = true;
390
596
  pageSize.addEventListener('change', this.#onPageSizeChange);
391
597
 
598
+ // Actions-leading slot passthrough (gh#1649) — a real insertion point
599
+ // (ADR-0067), same pattern as [slot="scope"]/[slot="actions"] above,
600
+ // rendered BEFORE the page-size select rather than after it: app-owned
601
+ // trigger buttons (e.g. custom Filter/Columns triggers that open the
602
+ // consumer's own drawer instead of the native filter/columns popovers)
603
+ // need to land ahead of page-size, which [slot="actions"] alone can't
604
+ // reach since it renders trailing (after page-size).
605
+ const actionsLeadingSlot = document.createElement('div');
606
+ actionsLeadingSlot.setAttribute('data-actions-leading', '');
607
+ // Move any pre-existing [slot="actions-leading"] children into this
608
+ // stamped wrapper — same real-insertion-point pattern as [slot="scope"]
609
+ // / [slot="actions"] above.
610
+ for (const node of [...this.children]) {
611
+ if (node === root) continue;
612
+ if (node.getAttribute?.('slot') === 'actions-leading') {
613
+ actionsLeadingSlot.appendChild(node);
614
+ }
615
+ }
616
+
392
617
  // Actions slot passthrough — we move any pre-existing [slot="actions"] children here
393
618
  const actionsSlot = document.createElement('div');
394
619
  actionsSlot.setAttribute('data-actions', '');
@@ -404,6 +629,7 @@ export class UITableToolbar extends UIElement {
404
629
  root.appendChild(summary);
405
630
  root.appendChild(controls);
406
631
  root.appendChild(search);
632
+ root.appendChild(actionsLeadingSlot);
407
633
  root.appendChild(pageSize);
408
634
  root.appendChild(actionsSlot);
409
635
 
@@ -433,6 +659,14 @@ export class UITableToolbar extends UIElement {
433
659
  btn.setAttribute('variant', 'outline');
434
660
  btn.setAttribute('size', 'sm');
435
661
  btn.setAttribute('aria-haspopup', 'menu');
662
+ // ADR-0076 REQ-X-002 — unconditional (harmless at full-text stages,
663
+ // becomes the hover-tooltip affordance at the icon-only stage once
664
+ // the rendered label is CSS-hidden, REQ-M-003). button-ui's own
665
+ // title→aria-label mirror (button.class.js) only fires when no
666
+ // aria-label is already present — button-ui stamps aria-label from
667
+ // [text] unconditionally in its own render(), so this never conflicts
668
+ // with REQ-X-001's already-set accessible name.
669
+ btn.setAttribute('title', label);
436
670
  btn.addEventListener('click', (e) => {
437
671
  e.stopPropagation();
438
672
  this.#togglePopover(kind, btn);
@@ -440,6 +674,55 @@ export class UITableToolbar extends UIElement {
440
674
  return btn;
441
675
  }
442
676
 
677
+ // ADR-0076 REQ-S-004/REQ-X-003, PLAN step 5 — the overflow-stage "More"
678
+ // trigger. Reuses menu-ui/menu-item-ui unchanged (table-toolbar already
679
+ // `composes: menu-item-ui`; menu-ui is added to the yaml's composes list
680
+ // alongside it — no new runtime dependency, both already ship in this
681
+ // package) and routes every item straight through the EXISTING
682
+ // #togglePopover(kind, btn) — the same method the three granular buttons
683
+ // above call, never a reimplementation of filter/sort/columns logic.
684
+ #mkOverflowTrigger() {
685
+ const menu = document.createElement('menu-ui');
686
+ menu.setAttribute('data-toolbar-overflow', '');
687
+
688
+ const trigger = document.createElement('button-ui');
689
+ trigger.setAttribute('slot', 'trigger');
690
+ trigger.setAttribute('icon', 'dots-three-vertical');
691
+ trigger.setAttribute('variant', 'outline');
692
+ trigger.setAttribute('size', 'sm');
693
+ // REQ-X-003 — a genuinely new interactive element (not a repurposed
694
+ // existing button), so it needs its own static accessible name rather
695
+ // than an inherited one. No [text] attribute — always icon-only,
696
+ // button-ui's own render() reflects [data-icon-only] geometry from
697
+ // that (no CSS override needed here, unlike REQ-M-004's granular
698
+ // buttons which carry a real text label at wider stages).
699
+ trigger.setAttribute('aria-label', 'More table actions');
700
+ trigger.setAttribute('aria-haspopup', 'menu');
701
+ menu.appendChild(trigger);
702
+
703
+ for (const [kind, label, icon] of [
704
+ ['filter', 'Filter', 'funnel-simple'],
705
+ ['sort', 'Sort', 'arrows-down-up'],
706
+ ['columns', 'Columns', 'columns'],
707
+ ]) {
708
+ const item = document.createElement('menu-item-ui');
709
+ item.setAttribute('icon', icon);
710
+ item.setAttribute('text', label);
711
+ item.setAttribute('value', kind);
712
+ menu.appendChild(item);
713
+ }
714
+
715
+ // menu-ui fires `action` with { value, text } (menu.class.js's own
716
+ // #onItemClick) — anchor the resulting popover on the "More" trigger
717
+ // itself (not any of the three hidden granular buttons), same
718
+ // #togglePopover(kind, btn) every other entry point already calls.
719
+ menu.addEventListener('action', (e) => {
720
+ this.#togglePopover(e.detail.value, trigger);
721
+ });
722
+
723
+ return menu;
724
+ }
725
+
443
726
  #updateTitle() {
444
727
  const heading = this.querySelector(':scope [data-heading]');
445
728
  if (heading) {
@@ -488,19 +771,56 @@ export class UITableToolbar extends UIElement {
488
771
  // Independent of #updateTitle's [data-title] cluster by design — coexists
489
772
  // with [text]/[count] rather than sharing their hide-with-children logic.
490
773
 
774
+ // ADR-0076 REQ-E — the `empty` slot renders ONLY on an explicit
775
+ // range-total="0" (checked via hasAttribute, never the computed number
776
+ // alone — REQ-E-001/AC-013: an unset range-total, adiav2's loading state,
777
+ // must never flash the empty-state message). A positive range-total
778
+ // always wins over empty-slot content (REQ-E-002's additive guarantee /
779
+ // AC-014's mutual-exclusion check) — the two states can never render
780
+ // simultaneously.
491
781
  #updateSummary() {
492
782
  const summary = this.querySelector(':scope > [data-toolbar] > [data-summary]');
493
783
  if (!summary) return;
784
+ const text = summary.querySelector(':scope > [data-summary-text]');
785
+ const compact = summary.querySelector(':scope > [data-summary-compact]');
786
+ const empty = summary.querySelector(':scope > [data-summary-empty]');
494
787
  const start = Number(this.rangeStart) || 0;
495
788
  const end = Number(this.rangeEnd) || 0;
496
789
  const total = Number(this.rangeTotal) || 0;
790
+
497
791
  if (start > 0 && end > 0 && total > 0) {
498
- const text = summary.querySelector(':scope [data-summary-text]');
792
+ // Normal range text always wins over the empty slot (AC-014).
793
+ // Both the full ("Showing X–Y of N") and short-form ("X–Y/N", ADR-0076
794
+ // REQ-S-003) strings are populated together — which one paints is a
795
+ // pure CSS decision (icon-only @container rule / [stage] pin), never
796
+ // a JS branch (REQ-M-002).
499
797
  if (text) text.textContent = `Showing ${start}–${end} of ${total}`;
798
+ if (text) text.hidden = false;
799
+ if (compact) compact.textContent = `${start}–${end}/${total}`;
800
+ if (compact) compact.hidden = false;
801
+ if (empty) empty.hidden = true;
500
802
  summary.hidden = false;
501
- } else {
502
- summary.hidden = true;
803
+ return;
503
804
  }
805
+
806
+ const confirmedZero = this.hasAttribute('range-total') && this.getAttribute('range-total') === '0';
807
+ const hasEmptyContent = !!empty && empty.childNodes.length > 0;
808
+
809
+ if (confirmedZero && hasEmptyContent) {
810
+ // Explicit range-total="0" + author-supplied empty content (AC-011).
811
+ if (text) text.hidden = true;
812
+ if (compact) compact.hidden = true;
813
+ empty.hidden = false;
814
+ summary.hidden = false;
815
+ return;
816
+ }
817
+
818
+ // Loading (range-total unset, AC-013) or confirmed-zero-with-no-slot-
819
+ // content (AC-012) — hide [data-summary] exactly as today (REQ-E-002).
820
+ if (text) text.hidden = true;
821
+ if (compact) compact.hidden = true;
822
+ if (empty) empty.hidden = true;
823
+ summary.hidden = true;
504
824
  }
505
825
 
506
826
  // ── Page-size select (gh#1615) ───────────────────────────────────────────
@@ -537,37 +857,76 @@ export class UITableToolbar extends UIElement {
537
857
  #onPageSizeChange = (e) => {
538
858
  const pageSize = Number(e.detail?.value ?? this.querySelector(':scope [data-page-size]')?.value) || 0;
539
859
  this.pageSize = pageSize;
540
- // Applied directly to the bound table's own [paginate] (rows-per-page)
541
- // prop the same "mirror to URL/analytics via the event, but the
542
- // toolbar already wires the change into the target" pattern
543
- // columns-change/sort-change already use.
544
- if (this.#target) this.#target.paginate = pageSize;
860
+ // gh#1764/#1780 (events-only interaction contract, ADR-0079) the
861
+ // bound table applies its own [paginate] through the `toolbar-paginate`
862
+ // command event rather than a direct property write; table.class.js
863
+ // listens for it in connected() and sets `this.paginate` itself.
864
+ if (this.#target) {
865
+ this.#target.dispatchEvent(new CustomEvent('toolbar-paginate', {
866
+ bubbles: true,
867
+ detail: { pageSize },
868
+ }));
869
+ }
545
870
  this.dispatchEvent(new CustomEvent('page-size-change', {
546
871
  bubbles: true,
547
872
  detail: { pageSize },
548
873
  }));
549
874
  };
550
875
 
876
+ // ADR-0076 REQ-C-002 — `chromeOnly` is PURE, ABSOLUTE OR over the four
877
+ // granular `no-*` attributes: while set, all four stay off regardless of
878
+ // any individual `no-*` attribute's own value, with no partial re-enable
879
+ // path (AC-010). This is the ONLY place chromeOnly is combined with the
880
+ // granular attributes — never re-derive the OR elsewhere.
551
881
  #updateControlVisibility() {
552
882
  const root = this.querySelector(':scope > [data-toolbar]');
553
883
  if (!root) return;
554
884
 
885
+ const chromeOnly = this.chromeOnly;
555
886
  const setHidden = (sel, hidden) => {
556
887
  const el = root.querySelector(sel);
557
888
  if (el) el.hidden = hidden;
558
889
  };
559
890
 
560
- setHidden('[data-toolbar-btn="filter"]', this.noFilter);
561
- setHidden('[data-toolbar-btn="sort"]', this.noSort);
562
- setHidden('[data-toolbar-btn="columns"]', this.noColumns);
563
- setHidden('[data-search]', this.noSearch);
891
+ const filterHidden = chromeOnly || this.noFilter;
892
+ const sortHidden = chromeOnly || this.noSort;
893
+ const columnsHidden = chromeOnly || this.noColumns;
894
+
895
+ setHidden('[data-toolbar-btn="filter"]', filterHidden);
896
+ setHidden('[data-toolbar-btn="sort"]', sortHidden);
897
+ setHidden('[data-toolbar-btn="columns"]', columnsHidden);
898
+ setHidden('[data-search]', chromeOnly || this.noSearch);
899
+
900
+ // ADR-0076 REQ-S-004, PLAN step 5 — the overflow "More" trigger's own
901
+ // menu-item-ui children mirror the SAME granular/chrome-only
902
+ // suppression as the three native buttons above: a control suppressed
903
+ // via no-*/chrome-only stays suppressed inside the overflow menu too,
904
+ // never resurrected there. Hide the whole trigger only once every item
905
+ // it would offer is suppressed — an empty "More" menu is worse than no
906
+ // trigger at all.
907
+ const overflow = root.querySelector('[data-toolbar-overflow]');
908
+ if (overflow) {
909
+ const hiddenByKind = { filter: filterHidden, sort: sortHidden, columns: columnsHidden };
910
+ for (const item of overflow.querySelectorAll('menu-item-ui')) {
911
+ item.hidden = !!hiddenByKind[item.getAttribute('value')];
912
+ }
913
+ overflow.hidden = filterHidden && sortHidden && columnsHidden;
914
+ }
564
915
  }
565
916
 
566
917
  // ── Search ───────────────────────────────────────────────────────────────
567
918
 
568
919
  #onSearch = (e) => {
569
920
  const value = e.detail?.value ?? '';
570
- if (this.#target) this.#target.search = value;
921
+ // gh#1764/#1780 (events-only interaction contract, ADR-0079) —
922
+ // `toolbar-search` replaces the direct `.search =` write; table.class.js
923
+ // listens for it in connected() and sets `this.search` itself.
924
+ if (this.#target) {
925
+ this.#target.dispatchEvent(new CustomEvent('toolbar-search', {
926
+ bubbles: true,
927
+ detail: { value },
928
+ }));
929
+ }
571
930
  this.dispatchEvent(new CustomEvent('search', {
572
931
  bubbles: true,
573
932
  detail: { value },
@@ -709,8 +1068,14 @@ export class UITableToolbar extends UIElement {
709
1068
  }
710
1069
  sel.addEventListener('change', () => {
711
1070
  const v = sel.value || '';
712
- if (v) target.setFilter(col.key, v, 'select');
713
- else target.setFilter(col.key, null);
1071
+ // gh#1764/#1780 (events-only interaction contract, ADR-0079)
1072
+ // `toolbar-filter-set` replaces the direct target.setFilter()
1073
+ // call; table.class.js applies it synchronously in its own
1074
+ // listener, so `target.filters` below already reflects it.
1075
+ target.dispatchEvent(new CustomEvent('toolbar-filter-set', {
1076
+ bubbles: true,
1077
+ detail: { key: col.key, value: v || null, op: 'select' },
1078
+ }));
714
1079
  this.dispatchEvent(new CustomEvent('filter-change', {
715
1080
  bubbles: true,
716
1081
  detail: { filters: target.filters },
@@ -726,8 +1091,12 @@ export class UITableToolbar extends UIElement {
726
1091
  if (current?.op === 'contains') input.value = current.value ?? '';
727
1092
  input.addEventListener('input', () => {
728
1093
  const v = input.value;
729
- if (v) target.setFilter(col.key, v, 'contains');
730
- else target.setFilter(col.key, null);
1094
+ // gh#1764/#1780 (events-only interaction contract, ADR-0079)
1095
+ // see the select-filter handler above for the same normalization.
1096
+ target.dispatchEvent(new CustomEvent('toolbar-filter-set', {
1097
+ bubbles: true,
1098
+ detail: { key: col.key, value: v || null, op: 'contains' },
1099
+ }));
731
1100
  this.dispatchEvent(new CustomEvent('filter-change', {
732
1101
  bubbles: true,
733
1102
  detail: { filters: target.filters },
@@ -743,7 +1112,11 @@ export class UITableToolbar extends UIElement {
743
1112
 
744
1113
  if (Object.keys(filters).length) {
745
1114
  const clear = this.#mkPopoverAction('Clear all filters', () => {
746
- target.clearFilters();
1115
+ // gh#1764/#1780 (events-only interaction contract, ADR-0079)
1116
+ // `toolbar-filter-clear` replaces the direct target.clearFilters()
1117
+ // call; table.class.js applies it synchronously in its own
1118
+ // listener before #refreshFilterPanel() re-reads target.filters.
1119
+ target.dispatchEvent(new CustomEvent('toolbar-filter-clear', { bubbles: true }));
747
1120
  this.dispatchEvent(new CustomEvent('filter-change', {
748
1121
  bubbles: true,
749
1122
  detail: { filters: {} },
@@ -892,7 +1265,13 @@ export class UITableToolbar extends UIElement {
892
1265
  const next = target.columns.map((c) => (
893
1266
  c.key === col.key ? { ...c, hidden: !check.hasAttribute('checked') } : { ...c }
894
1267
  ));
895
- target.columns = next;
1268
+ // gh#1764/#1780 (events-only interaction contract, ADR-0079) —
1269
+ // `toolbar-columns-set` replaces the direct target.columns= write;
1270
+ // table.class.js applies it synchronously in its own listener.
1271
+ target.dispatchEvent(new CustomEvent('toolbar-columns-set', {
1272
+ bubbles: true,
1273
+ detail: { columns: next },
1274
+ }));
896
1275
  this.dispatchEvent(new CustomEvent('columns-change', {
897
1276
  bubbles: true,
898
1277
  detail: { hiddenColumns: next.filter((c) => c.hidden).map((c) => c.key) },