@adia-ai/web-components 0.8.43 → 0.8.44

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 (93) hide show
  1. package/CHANGELOG.md +30 -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.d.ts +2 -0
  21. package/components/heatmap/heatmap.a2ui.json +5 -0
  22. package/components/heatmap/heatmap.d.ts +2 -0
  23. package/components/heatmap/heatmap.yaml +8 -0
  24. package/components/input/input.a2ui.json +2 -2
  25. package/components/input/input.class.js +12 -7
  26. package/components/input/input.yaml +6 -5
  27. package/components/integration-card/integration-card.d.ts +2 -0
  28. package/components/noodles/noodles.a2ui.json +0 -15
  29. package/components/noodles/noodles.yaml +0 -12
  30. package/components/number-format/number-format.class.js +10 -0
  31. package/components/pagination/pagination.class.js +88 -4
  32. package/components/popover/popover.d.ts +2 -0
  33. package/components/progress/progress.class.js +11 -6
  34. package/components/range/range.class.js +9 -1
  35. package/components/select/select.a2ui.json +5 -1
  36. package/components/select/select.class.js +14 -14
  37. package/components/select/select.css +14 -1
  38. package/components/select/select.yaml +7 -1
  39. package/components/stream/stream.d.ts +2 -0
  40. package/components/table/table.a2ui.json +6 -1
  41. package/components/table/table.class.js +18 -1
  42. package/components/table/table.css +31 -6
  43. package/components/table/table.d.ts +3 -1
  44. package/components/table/table.examples.md +2 -2
  45. package/components/table/table.yaml +9 -1
  46. package/components/table-toolbar/table-toolbar.a2ui.json +28 -4
  47. package/components/table-toolbar/table-toolbar.class.js +346 -11
  48. package/components/table-toolbar/table-toolbar.css +265 -0
  49. package/components/table-toolbar/table-toolbar.d.ts +5 -1
  50. package/components/table-toolbar/table-toolbar.yaml +130 -15
  51. package/components/tag/tag.class.js +13 -1
  52. package/components/text/text.a2ui.json +3 -3
  53. package/components/text/text.d.ts +5 -3
  54. package/components/text/text.yaml +3 -3
  55. package/components/textarea/textarea.class.js +9 -1
  56. package/components/tour/tour.d.ts +10 -0
  57. package/core/a11y.d.ts +22 -0
  58. package/core/a11y.js +64 -0
  59. package/core/anchor.js +9 -5
  60. package/core/element.js +1 -1
  61. package/custom-elements.json +73 -29
  62. package/dist/host.min.css +1 -1
  63. package/dist/host.sheet.js +1 -1
  64. package/dist/theme-provider.min.js +1 -1
  65. package/dist/web-components.min.css +1 -1
  66. package/dist/web-components.min.js +119 -119
  67. package/dist/web-components.sheet.js +1 -1
  68. package/package.json +1 -1
  69. package/patterns/access-requests/access-requests.examples.html +1 -1
  70. package/patterns/admin-keys-and-export/admin-keys-and-export.examples.html +7 -7
  71. package/patterns/agent-cost/agent-cost.examples.html +6 -6
  72. package/patterns/agent-memory/agent-memory.examples.html +3 -3
  73. package/patterns/agent-prompt-library/agent-prompt-library.examples.html +8 -8
  74. package/patterns/agent-tool-call/agent-tool-call.examples.html +1 -1
  75. package/patterns/approvals/approvals.examples.html +6 -6
  76. package/patterns/audit-log/audit-log.examples.html +1 -1
  77. package/patterns/bulk-action-toolbar/bulk-action-toolbar.examples.html +4 -4
  78. package/patterns/chart-in-card/chart-in-card.examples.html +114 -0
  79. package/patterns/chart-in-card/chart-in-card.examples.js +37 -0
  80. package/patterns/comments-and-collaboration/comments-and-collaboration.examples.html +5 -5
  81. package/patterns/data-tables-inline-edit-and-tree/data-tables-inline-edit-and-tree.examples.html +2 -2
  82. package/patterns/diff-review/diff-review.examples.html +6 -6
  83. package/patterns/export-flow/export-flow.examples.html +2 -2
  84. package/patterns/filter-bar/filter-bar.examples.html +2 -2
  85. package/patterns/forms-conditional-and-autocomplete/forms-conditional-and-autocomplete.examples.html +2 -2
  86. package/patterns/inline-dialog/inline-dialog.examples.html +2 -2
  87. package/patterns/marketing-engagement/marketing-engagement.examples.html +4 -4
  88. package/patterns/notifications-bell-and-digest/notifications-bell-and-digest.examples.html +1 -1
  89. package/patterns/permissions-matrix/permissions-matrix.examples.html +1 -1
  90. package/patterns/permissions-sharing/permissions-sharing.examples.html +2 -2
  91. package/patterns/profile-public-and-verification/profile-public-and-verification.examples.html +4 -4
  92. package/patterns/search-discovery/search-discovery.examples.html +2 -2
  93. 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
@@ -35,10 +37,24 @@
35
37
  */
36
38
 
37
39
  import { UIElement } from '../../core/element.js';
38
- import { anchorPopover } from '../../core/anchor.js';
40
+ import { anchorPopover, resolveLengthPx } from '../../core/anchor.js';
39
41
 
40
42
  const SEARCH_DEBOUNCE = 200;
41
43
 
44
+ // ADR-0076 REQ-M-006 — the three breakpoints' NORMATIVE default pixel
45
+ // widths (56rem/40rem/26rem at the standard 16px root), duplicated here
46
+ // ONLY as a fallback for #classifyStage() below when the live
47
+ // `--table-toolbar-bp-*` custom property can't be read (no table-toolbar.css
48
+ // stylesheet attached yet, an SSR shim, or a unit-test harness with no real
49
+ // cascade) — resolveLengthPx() returns NaN in that case, `||` falls
50
+ // through to these. Table-toolbar.css's own `:where(:scope)` declarations
51
+ // are the actual source of truth whenever a real stylesheet IS present;
52
+ // this fallback exists so the reflection degrades to the DOCUMENTED
53
+ // default instead of silently misclassifying against NaN.
54
+ const DEFAULT_BP_SEARCH_TIGHT_PX = 56 * 16;
55
+ const DEFAULT_BP_ICON_ONLY_PX = 40 * 16;
56
+ const DEFAULT_BP_OVERFLOW_PX = 26 * 16;
57
+
42
58
  function emptyHint(text) {
43
59
  const el = document.createElement('text-ui');
44
60
  el.setAttribute('data-popover-empty', '');
@@ -136,14 +152,28 @@ export class UITableToolbar extends UIElement {
136
152
  noSort: { type: Boolean, default: false, reflect: true, attribute: 'no-sort' },
137
153
  noColumns: { type: Boolean, default: false, reflect: true, attribute: 'no-columns' },
138
154
  noSearch: { type: Boolean, default: false, reflect: true, attribute: 'no-search' },
155
+ // ADR-0076 (ADIA2-9123) — additive sugar over the four `no-*` attributes
156
+ // above. Pure, absolute OR: see #updateControlVisibility(), which is the
157
+ // single place that combines chromeOnly with each granular no-* value.
158
+ chromeOnly: { type: Boolean, default: false, reflect: true, attribute: 'chrome-only' },
139
159
  placeholder: { type: String, default: 'Search...', reflect: false },
140
160
  variant: { type: String, default: 'default', reflect: true },
161
+ // ADR-0076 REQ-O-001 — explicit compaction-stage override. Unset (the
162
+ // default, empty string) means auto-snap via the @container query
163
+ // (table-toolbar.css's :scope container-type/container-name). All four
164
+ // stages' CSS is now live: search-tight steps the search field down
165
+ // (step 2); icon-only drops control labels and compacts/hides the
166
+ // range summary (steps 3+4); overflow collapses Filter/Sort/Columns
167
+ // into one "More" trigger (step 5).
168
+ stage: { type: String, default: '', reflect: true },
141
169
  };
142
170
 
143
171
  // §205 (v0.5.7): dynamic sort-indicator icons (table-toolbar.class.js:576 — nested ternary
144
172
  // `dir === 'asc' ? 'arrow-up' : dir === 'desc' ? 'arrow-down' : 'caret-up-down'`).
145
173
  // Per FEEDBACK-16 §1 + §209 slot-11 ternary-walker discovery.
146
- static requiredIcons = ['arrow-up', 'arrow-down', 'caret-up-down'];
174
+ // 'dots-three-vertical' — the overflow-stage "More" trigger icon
175
+ // (ADR-0076 REQ-S-004, PLAN step 5, #mkOverflowTrigger()).
176
+ static requiredIcons = ['arrow-up', 'arrow-down', 'caret-up-down', 'dots-three-vertical'];
147
177
 
148
178
  static template = () => null;
149
179
 
@@ -154,6 +184,17 @@ export class UITableToolbar extends UIElement {
154
184
  #docListenerRaf = null;
155
185
  #sortIndicatorRafs = new Set();
156
186
  #pageSizeOptions = [];
187
+ // ADR-0076 REQ-M-005 (ADIA2-9123 S4-ii), PLAN step 7 — data-stage-resolved
188
+ // reflection. #resizeObs classifies live width against the SAME three
189
+ // breakpoints table-toolbar.css's @container rules use (read from the
190
+ // custom properties themselves, never a second hardcoded copy of
191
+ // 56/40/26 — see #classifyStage()). #lastWidthPx caches the last
192
+ // measured inline-size so a `stage` attribute change alone (no resize)
193
+ // can still re-classify without waiting on the next ResizeObserver
194
+ // callback (REQ-M-005's dual-trigger requirement).
195
+ #resizeObs = null;
196
+ #lastWidthPx = null;
197
+ #stageRaf = null;
157
198
 
158
199
  // ── Public API: pageSizeOptions ──────────────────────────────────────────
159
200
  // Dynamic array prop (JS-set collection, custom setter) — same shape as
@@ -195,6 +236,34 @@ export class UITableToolbar extends UIElement {
195
236
  this.#stamp();
196
237
  this.#resolveTarget();
197
238
  this.#syncFromTarget();
239
+
240
+ // ADR-0076 REQ-M-005, PLAN step 7 — classify synchronously off
241
+ // `clientWidth` at connect time too (0 pre-layout treated as "no
242
+ // measurement yet", the same `null`-width fallback #reflectResolvedStage
243
+ // uses) rather than waiting on the ResizeObserver's own first
244
+ // (asynchronous) callback — avoids a brief connect-time gap with no
245
+ // `data-stage-resolved` reflected at all, mirroring how chart.class.js's
246
+ // own `data-ratio-resolved` is set synchronously from `#dims()` at
247
+ // first render(), not deferred to its ResizeObserver's first tick.
248
+ this.#lastWidthPx = this.clientWidth || null;
249
+ this.#reflectResolvedStage();
250
+
251
+ // gh#285 SSR DOM shims (linkedom) have no ResizeObserver global, same
252
+ // guard chart.class.js's own ResizeObserver setup uses.
253
+ if (typeof ResizeObserver !== 'undefined') {
254
+ this.#resizeObs = new ResizeObserver((entries) => {
255
+ const { inlineSize } = entries[0].contentBoxSize[0];
256
+ this.#lastWidthPx = inlineSize;
257
+ // rAF-batch, mirroring chart.class.js's own ResizeObserver setup —
258
+ // avoids a same-tick reflect→layout→observer-refire loop.
259
+ if (this.#stageRaf) return;
260
+ this.#stageRaf = requestAnimationFrame(() => {
261
+ this.#stageRaf = null;
262
+ this.#reflectResolvedStage();
263
+ });
264
+ });
265
+ this.#resizeObs.observe(this);
266
+ }
198
267
  }
199
268
 
200
269
  // gh#1620 — this used to live in a declared `attributeChanged(name)` hook
@@ -204,7 +273,8 @@ export class UITableToolbar extends UIElement {
204
273
  // hook, keyed by the matching property names.
205
274
  updated(changed) {
206
275
  if (changed.has('noFilter') || changed.has('noSort')
207
- || changed.has('noColumns') || changed.has('noSearch')) {
276
+ || changed.has('noColumns') || changed.has('noSearch')
277
+ || changed.has('chromeOnly')) {
208
278
  this.#updateControlVisibility();
209
279
  }
210
280
  if (changed.has('placeholder')) {
@@ -213,6 +283,30 @@ export class UITableToolbar extends UIElement {
213
283
  }
214
284
  }
215
285
 
286
+ // ADR-0076 REQ-E-001 — `updated(changed)` alone MISSES a live loading→
287
+ // confirmed-zero transition: `rangeTotal`'s default is already 0, so
288
+ // setting `range-total="0"` from an unset start parses to the SAME
289
+ // Number(0), the base class's property setter's `Object.is` short-circuit
290
+ // swallows the signal write (core/element.js `installProps()`), and
291
+ // `changed` never gets a `rangeTotal` entry — `#updateSummary()`'s
292
+ // hasAttribute-based branch never re-runs. Verified live in a browser
293
+ // (not just the vitest suite, which happened to only exercise the
294
+ // positive→zero transition, where the signal DOES change). REQ-E-001's
295
+ // check is fundamentally an ATTRIBUTE-presence concern, not a coerced-
296
+ // value one, so it needs the raw attribute callback, not the signal diff.
297
+ attributeChangedCallback(name, oldVal, newVal) {
298
+ super.attributeChangedCallback(name, oldVal, newVal);
299
+ if (name === 'range-total') this.#updateSummary();
300
+ // ADR-0076 REQ-M-005 — the SECOND of the two triggers `data-stage-
301
+ // resolved` must react to: a live resize (the ResizeObserver callback
302
+ // above) AND a `stage` attribute change alone, with no resize needed.
303
+ // Re-classifies against the cached last-measured width — a pin set
304
+ // before the first resize observation (#lastWidthPx still null) simply
305
+ // reports the pin; classifyStage() below treats a null width as "no
306
+ // live width to compare against yet", never a false 0-width match.
307
+ if (name === 'stage') this.#reflectResolvedStage();
308
+ }
309
+
216
310
  disconnected() {
217
311
  if (this.#docListenerRaf != null) {
218
312
  cancelAnimationFrame(this.#docListenerRaf);
@@ -222,6 +316,68 @@ export class UITableToolbar extends UIElement {
222
316
  this.#sortIndicatorRafs.clear();
223
317
  this.#closePopover();
224
318
  this.#detachTarget();
319
+ if (this.#stageRaf != null) {
320
+ cancelAnimationFrame(this.#stageRaf);
321
+ this.#stageRaf = null;
322
+ }
323
+ this.#resizeObs?.disconnect();
324
+ this.#resizeObs = null;
325
+ }
326
+
327
+ // ── data-stage-resolved reflection (ADR-0076 REQ-M-005, ADIA2-9123 ─────────
328
+ // S4-ii, PLAN step 7) ────────────────────────────────────────────────────
329
+ //
330
+ // Purely informational — mirrors the CSS-decided stage for a consumer's
331
+ // own [slot="actions-leading"] content, never drives table-toolbar's own
332
+ // rendering (REQ-S stays 100% CSS `@container`-driven, REQ-M-002). The
333
+ // three breakpoints are read live off the SAME custom properties the CSS
334
+ // `@container` rules consume (--table-toolbar-bp-search-tight/-icon-only/
335
+ // -overflow) via `resolveLengthPx` (core/anchor.js, shared with its
336
+ // --popover-max-height use) — one source of truth, never a second
337
+ // hardcoded copy of 56/40/26 here.
338
+
339
+ #classifyStage(widthPx) {
340
+ const cs = getComputedStyle(this);
341
+ // resolveLengthPx() returns NaN when the custom property is unreadable
342
+ // — no table-toolbar.css stylesheet attached yet, an SSR shim, or a
343
+ // headless unit-test harness with no real cascade — so an explicit
344
+ // Number.isFinite() check falls through to each breakpoint's own
345
+ // documented REQ-M-006 default. This must be a real NaN check, not a
346
+ // `|| DEFAULT` fallback: `0` is a legitimate consumer override (e.g.
347
+ // `--table-toolbar-bp-overflow: 0rem` to disable the overflow stage
348
+ // entirely) and `||` would silently discard it as falsy, substituting
349
+ // the default instead — reviewer finding on PR #1676, fixed here.
350
+ // Whenever a real stylesheet IS present, the live custom property
351
+ // (overridable per-consumer, same posture as every other
352
+ // --table-toolbar-* token) wins, zero included.
353
+ const resolveBp = (raw, fallback) => {
354
+ const v = resolveLengthPx(raw);
355
+ return Number.isFinite(v) ? v : fallback;
356
+ };
357
+ const searchTightPx = resolveBp(cs.getPropertyValue('--table-toolbar-bp-search-tight'), DEFAULT_BP_SEARCH_TIGHT_PX);
358
+ const iconOnlyPx = resolveBp(cs.getPropertyValue('--table-toolbar-bp-icon-only'), DEFAULT_BP_ICON_ONLY_PX);
359
+ const overflowPx = resolveBp(cs.getPropertyValue('--table-toolbar-bp-overflow'), DEFAULT_BP_OVERFLOW_PX);
360
+
361
+ // Inclusive-on-the-narrower-side (max-width semantics, REQ-M-006) — a
362
+ // width exactly AT a boundary classifies to the narrower stage, same
363
+ // rule the CSS `@container (max-width: …)` conditions already encode.
364
+ if (widthPx <= overflowPx) return 'overflow';
365
+ if (widthPx <= iconOnlyPx) return 'icon-only';
366
+ if (widthPx <= searchTightPx) return 'search-tight';
367
+ return 'full';
368
+ }
369
+
370
+ #reflectResolvedStage() {
371
+ // REQ-O-001/REQ-M-005 — the pin wins for the reflection exactly as it
372
+ // wins for rendering: an explicit `stage` reports itself, never the
373
+ // width-derived classification, even inside a container sized for a
374
+ // different stage.
375
+ const resolved = this.stage || (this.#lastWidthPx == null ? 'full' : this.#classifyStage(this.#lastWidthPx));
376
+ // Is-different guard — same shape as chart.class.js's own
377
+ // data-ratio-resolved reflection (chart.class.js:533).
378
+ if (this.getAttribute('data-stage-resolved') !== resolved) {
379
+ this.setAttribute('data-stage-resolved', resolved);
380
+ }
225
381
  }
226
382
 
227
383
  render() {
@@ -357,12 +513,51 @@ export class UITableToolbar extends UIElement {
357
513
  summaryText.setAttribute('data-summary-text', '');
358
514
  summary.appendChild(summaryText);
359
515
 
516
+ // ADR-0076 REQ-S-003 — short-form compact text ("1–25/320"), a sibling
517
+ // of [data-summary-text] holding the SAME range values in a terser
518
+ // shape. Which of the two is visible is decided entirely by CSS
519
+ // (table-toolbar.css's icon-only @container rule / [stage] pin) —
520
+ // both nodes are always populated together in #updateSummary(), never
521
+ // a JS branch keyed on a resolved-stage signal (REQ-M-002 stays true:
522
+ // no JS decides rendering here, only which text each stage's CSS shows).
523
+ const summaryCompact = document.createElement('span');
524
+ summaryCompact.setAttribute('data-summary-compact', '');
525
+ summaryCompact.hidden = true;
526
+ summary.appendChild(summaryCompact);
527
+
528
+ // Empty-state slot (ADR-0076 REQ-E, adiav2 ADIA2-9123) — renders in
529
+ // [data-summary]'s own position, alongside [data-summary-text], never a
530
+ // separate region: only one of the two is ever visible at a time (see
531
+ // #updateSummary()). Same real-insertion-point pattern as [slot="scope"]
532
+ // / [slot="actions"] / [slot="actions-leading"] above — move any
533
+ // pre-existing [slot="empty"] children into this stamped wrapper rather
534
+ // than declaring a template-owned `parts:` entry (ADR-0067).
535
+ const summaryEmpty = document.createElement('div');
536
+ summaryEmpty.setAttribute('data-summary-empty', '');
537
+ summaryEmpty.hidden = true;
538
+ for (const node of [...this.children]) {
539
+ if (node === root) continue;
540
+ if (node.getAttribute?.('slot') === 'empty') {
541
+ summaryEmpty.appendChild(node);
542
+ }
543
+ }
544
+ summary.appendChild(summaryEmpty);
545
+
360
546
  // Controls cluster
361
547
  const controls = document.createElement('div');
362
548
  controls.setAttribute('data-controls', '');
363
549
  controls.appendChild(this.#mkButton('filter', 'Filter', 'funnel-simple'));
364
550
  controls.appendChild(this.#mkButton('sort', 'Sort', 'arrows-down-up'));
365
551
  controls.appendChild(this.#mkButton('columns', 'Columns', 'columns'));
552
+ // ADR-0076 REQ-S-004/REQ-X-003, PLAN step 5 — overflow-stage "More"
553
+ // trigger. Stamped unconditionally alongside the three granular
554
+ // buttons above (never removed/re-created), CSS-hidden except at the
555
+ // overflow breakpoint/pin (REQ-M-002 stays true: which cluster paints
556
+ // is a pure CSS decision, not a JS branch). Keeping all four buttons
557
+ // permanently in the DOM is the simplest way to honor REQ-X-004's
558
+ // tab-order guarantee — nothing is ever created/destroyed across a
559
+ // stage transition, only shown/hidden.
560
+ controls.appendChild(this.#mkOverflowTrigger());
366
561
 
367
562
  // Search — compose <search-ui>, which already stamps input-ui with
368
563
  // the magnifying-glass prefix + clear suffix and debounces a `search`
@@ -389,6 +584,25 @@ export class UITableToolbar extends UIElement {
389
584
  pageSize.hidden = true;
390
585
  pageSize.addEventListener('change', this.#onPageSizeChange);
391
586
 
587
+ // Actions-leading slot passthrough (gh#1649) — a real insertion point
588
+ // (ADR-0067), same pattern as [slot="scope"]/[slot="actions"] above,
589
+ // rendered BEFORE the page-size select rather than after it: app-owned
590
+ // trigger buttons (e.g. custom Filter/Columns triggers that open the
591
+ // consumer's own drawer instead of the native filter/columns popovers)
592
+ // need to land ahead of page-size, which [slot="actions"] alone can't
593
+ // reach since it renders trailing (after page-size).
594
+ const actionsLeadingSlot = document.createElement('div');
595
+ actionsLeadingSlot.setAttribute('data-actions-leading', '');
596
+ // Move any pre-existing [slot="actions-leading"] children into this
597
+ // stamped wrapper — same real-insertion-point pattern as [slot="scope"]
598
+ // / [slot="actions"] above.
599
+ for (const node of [...this.children]) {
600
+ if (node === root) continue;
601
+ if (node.getAttribute?.('slot') === 'actions-leading') {
602
+ actionsLeadingSlot.appendChild(node);
603
+ }
604
+ }
605
+
392
606
  // Actions slot passthrough — we move any pre-existing [slot="actions"] children here
393
607
  const actionsSlot = document.createElement('div');
394
608
  actionsSlot.setAttribute('data-actions', '');
@@ -404,6 +618,7 @@ export class UITableToolbar extends UIElement {
404
618
  root.appendChild(summary);
405
619
  root.appendChild(controls);
406
620
  root.appendChild(search);
621
+ root.appendChild(actionsLeadingSlot);
407
622
  root.appendChild(pageSize);
408
623
  root.appendChild(actionsSlot);
409
624
 
@@ -433,6 +648,14 @@ export class UITableToolbar extends UIElement {
433
648
  btn.setAttribute('variant', 'outline');
434
649
  btn.setAttribute('size', 'sm');
435
650
  btn.setAttribute('aria-haspopup', 'menu');
651
+ // ADR-0076 REQ-X-002 — unconditional (harmless at full-text stages,
652
+ // becomes the hover-tooltip affordance at the icon-only stage once
653
+ // the rendered label is CSS-hidden, REQ-M-003). button-ui's own
654
+ // title→aria-label mirror (button.class.js) only fires when no
655
+ // aria-label is already present — button-ui stamps aria-label from
656
+ // [text] unconditionally in its own render(), so this never conflicts
657
+ // with REQ-X-001's already-set accessible name.
658
+ btn.setAttribute('title', label);
436
659
  btn.addEventListener('click', (e) => {
437
660
  e.stopPropagation();
438
661
  this.#togglePopover(kind, btn);
@@ -440,6 +663,55 @@ export class UITableToolbar extends UIElement {
440
663
  return btn;
441
664
  }
442
665
 
666
+ // ADR-0076 REQ-S-004/REQ-X-003, PLAN step 5 — the overflow-stage "More"
667
+ // trigger. Reuses menu-ui/menu-item-ui unchanged (table-toolbar already
668
+ // `composes: menu-item-ui`; menu-ui is added to the yaml's composes list
669
+ // alongside it — no new runtime dependency, both already ship in this
670
+ // package) and routes every item straight through the EXISTING
671
+ // #togglePopover(kind, btn) — the same method the three granular buttons
672
+ // above call, never a reimplementation of filter/sort/columns logic.
673
+ #mkOverflowTrigger() {
674
+ const menu = document.createElement('menu-ui');
675
+ menu.setAttribute('data-toolbar-overflow', '');
676
+
677
+ const trigger = document.createElement('button-ui');
678
+ trigger.setAttribute('slot', 'trigger');
679
+ trigger.setAttribute('icon', 'dots-three-vertical');
680
+ trigger.setAttribute('variant', 'outline');
681
+ trigger.setAttribute('size', 'sm');
682
+ // REQ-X-003 — a genuinely new interactive element (not a repurposed
683
+ // existing button), so it needs its own static accessible name rather
684
+ // than an inherited one. No [text] attribute — always icon-only,
685
+ // button-ui's own render() reflects [data-icon-only] geometry from
686
+ // that (no CSS override needed here, unlike REQ-M-004's granular
687
+ // buttons which carry a real text label at wider stages).
688
+ trigger.setAttribute('aria-label', 'More table actions');
689
+ trigger.setAttribute('aria-haspopup', 'menu');
690
+ menu.appendChild(trigger);
691
+
692
+ for (const [kind, label, icon] of [
693
+ ['filter', 'Filter', 'funnel-simple'],
694
+ ['sort', 'Sort', 'arrows-down-up'],
695
+ ['columns', 'Columns', 'columns'],
696
+ ]) {
697
+ const item = document.createElement('menu-item-ui');
698
+ item.setAttribute('icon', icon);
699
+ item.setAttribute('text', label);
700
+ item.setAttribute('value', kind);
701
+ menu.appendChild(item);
702
+ }
703
+
704
+ // menu-ui fires `action` with { value, text } (menu.class.js's own
705
+ // #onItemClick) — anchor the resulting popover on the "More" trigger
706
+ // itself (not any of the three hidden granular buttons), same
707
+ // #togglePopover(kind, btn) every other entry point already calls.
708
+ menu.addEventListener('action', (e) => {
709
+ this.#togglePopover(e.detail.value, trigger);
710
+ });
711
+
712
+ return menu;
713
+ }
714
+
443
715
  #updateTitle() {
444
716
  const heading = this.querySelector(':scope [data-heading]');
445
717
  if (heading) {
@@ -488,19 +760,56 @@ export class UITableToolbar extends UIElement {
488
760
  // Independent of #updateTitle's [data-title] cluster by design — coexists
489
761
  // with [text]/[count] rather than sharing their hide-with-children logic.
490
762
 
763
+ // ADR-0076 REQ-E — the `empty` slot renders ONLY on an explicit
764
+ // range-total="0" (checked via hasAttribute, never the computed number
765
+ // alone — REQ-E-001/AC-013: an unset range-total, adiav2's loading state,
766
+ // must never flash the empty-state message). A positive range-total
767
+ // always wins over empty-slot content (REQ-E-002's additive guarantee /
768
+ // AC-014's mutual-exclusion check) — the two states can never render
769
+ // simultaneously.
491
770
  #updateSummary() {
492
771
  const summary = this.querySelector(':scope > [data-toolbar] > [data-summary]');
493
772
  if (!summary) return;
773
+ const text = summary.querySelector(':scope > [data-summary-text]');
774
+ const compact = summary.querySelector(':scope > [data-summary-compact]');
775
+ const empty = summary.querySelector(':scope > [data-summary-empty]');
494
776
  const start = Number(this.rangeStart) || 0;
495
777
  const end = Number(this.rangeEnd) || 0;
496
778
  const total = Number(this.rangeTotal) || 0;
779
+
497
780
  if (start > 0 && end > 0 && total > 0) {
498
- const text = summary.querySelector(':scope [data-summary-text]');
781
+ // Normal range text always wins over the empty slot (AC-014).
782
+ // Both the full ("Showing X–Y of N") and short-form ("X–Y/N", ADR-0076
783
+ // REQ-S-003) strings are populated together — which one paints is a
784
+ // pure CSS decision (icon-only @container rule / [stage] pin), never
785
+ // a JS branch (REQ-M-002).
499
786
  if (text) text.textContent = `Showing ${start}–${end} of ${total}`;
787
+ if (text) text.hidden = false;
788
+ if (compact) compact.textContent = `${start}–${end}/${total}`;
789
+ if (compact) compact.hidden = false;
790
+ if (empty) empty.hidden = true;
500
791
  summary.hidden = false;
501
- } else {
502
- summary.hidden = true;
792
+ return;
793
+ }
794
+
795
+ const confirmedZero = this.hasAttribute('range-total') && this.getAttribute('range-total') === '0';
796
+ const hasEmptyContent = !!empty && empty.childNodes.length > 0;
797
+
798
+ if (confirmedZero && hasEmptyContent) {
799
+ // Explicit range-total="0" + author-supplied empty content (AC-011).
800
+ if (text) text.hidden = true;
801
+ if (compact) compact.hidden = true;
802
+ empty.hidden = false;
803
+ summary.hidden = false;
804
+ return;
503
805
  }
806
+
807
+ // Loading (range-total unset, AC-013) or confirmed-zero-with-no-slot-
808
+ // content (AC-012) — hide [data-summary] exactly as today (REQ-E-002).
809
+ if (text) text.hidden = true;
810
+ if (compact) compact.hidden = true;
811
+ if (empty) empty.hidden = true;
812
+ summary.hidden = true;
504
813
  }
505
814
 
506
815
  // ── Page-size select (gh#1615) ───────────────────────────────────────────
@@ -548,19 +857,45 @@ export class UITableToolbar extends UIElement {
548
857
  }));
549
858
  };
550
859
 
860
+ // ADR-0076 REQ-C-002 — `chromeOnly` is PURE, ABSOLUTE OR over the four
861
+ // granular `no-*` attributes: while set, all four stay off regardless of
862
+ // any individual `no-*` attribute's own value, with no partial re-enable
863
+ // path (AC-010). This is the ONLY place chromeOnly is combined with the
864
+ // granular attributes — never re-derive the OR elsewhere.
551
865
  #updateControlVisibility() {
552
866
  const root = this.querySelector(':scope > [data-toolbar]');
553
867
  if (!root) return;
554
868
 
869
+ const chromeOnly = this.chromeOnly;
555
870
  const setHidden = (sel, hidden) => {
556
871
  const el = root.querySelector(sel);
557
872
  if (el) el.hidden = hidden;
558
873
  };
559
874
 
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);
875
+ const filterHidden = chromeOnly || this.noFilter;
876
+ const sortHidden = chromeOnly || this.noSort;
877
+ const columnsHidden = chromeOnly || this.noColumns;
878
+
879
+ setHidden('[data-toolbar-btn="filter"]', filterHidden);
880
+ setHidden('[data-toolbar-btn="sort"]', sortHidden);
881
+ setHidden('[data-toolbar-btn="columns"]', columnsHidden);
882
+ setHidden('[data-search]', chromeOnly || this.noSearch);
883
+
884
+ // ADR-0076 REQ-S-004, PLAN step 5 — the overflow "More" trigger's own
885
+ // menu-item-ui children mirror the SAME granular/chrome-only
886
+ // suppression as the three native buttons above: a control suppressed
887
+ // via no-*/chrome-only stays suppressed inside the overflow menu too,
888
+ // never resurrected there. Hide the whole trigger only once every item
889
+ // it would offer is suppressed — an empty "More" menu is worse than no
890
+ // trigger at all.
891
+ const overflow = root.querySelector('[data-toolbar-overflow]');
892
+ if (overflow) {
893
+ const hiddenByKind = { filter: filterHidden, sort: sortHidden, columns: columnsHidden };
894
+ for (const item of overflow.querySelectorAll('menu-item-ui')) {
895
+ item.hidden = !!hiddenByKind[item.getAttribute('value')];
896
+ }
897
+ overflow.hidden = filterHidden && sortHidden && columnsHidden;
898
+ }
564
899
  }
565
900
 
566
901
  // ── Search ───────────────────────────────────────────────────────────────