@adia-ai/web-components 0.8.46 → 0.8.48

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 (39) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/components/card/card.css +25 -2
  3. package/components/chart/chart.a2ui.json +1 -1
  4. package/components/chart/chart.d.ts +1 -1
  5. package/components/chart/chart.yaml +6 -3
  6. package/components/link/link.css +3 -2
  7. package/components/nav/nav.css +7 -3
  8. package/components/nav-group/nav-group.css +38 -41
  9. package/components/nav-item/nav-item.css +11 -5
  10. package/components/preview/preview.class.js +12 -2
  11. package/components/richtext/richtext.css +2 -2
  12. package/components/table/table.a2ui.json +5 -2
  13. package/components/table/table.class.js +36 -1
  14. package/components/table/table.d.ts +2 -2
  15. package/components/table/table.yaml +17 -1
  16. package/components/table-footer/table-footer.a2ui.json +20 -2
  17. package/components/table-footer/table-footer.class.js +94 -8
  18. package/components/table-footer/table-footer.d.ts +6 -2
  19. package/components/table-footer/table-footer.yaml +57 -6
  20. package/components/table-toolbar/table-toolbar.a2ui.json +35 -2
  21. package/components/table-toolbar/table-toolbar.class.js +117 -9
  22. package/components/table-toolbar/table-toolbar.css +104 -3
  23. package/components/table-toolbar/table-toolbar.d.ts +10 -2
  24. package/components/table-toolbar/table-toolbar.examples.md +47 -45
  25. package/components/table-toolbar/table-toolbar.yaml +97 -9
  26. package/core/anchor.js +21 -1
  27. package/custom-elements.json +50 -5
  28. package/dist/host.min.css +1 -1
  29. package/dist/host.sheet.js +1 -1
  30. package/dist/theme-provider.min.js +8 -8
  31. package/dist/themes.min.css +1 -1
  32. package/dist/themes.sheet.js +1 -1
  33. package/dist/web-components.min.css +1 -1
  34. package/dist/web-components.min.js +52 -52
  35. package/dist/web-components.sheet.js +1 -1
  36. package/package.json +1 -1
  37. package/styles/colors/material-static.css +298 -234
  38. package/styles/themes.css +2676 -0
  39. package/styles/type/elements.css +1 -0
@@ -284,6 +284,21 @@
284
284
  flex: 0 0 auto;
285
285
  }
286
286
 
287
+ /* See [data-page-size]'s own comment below — same --select-min-width
288
+ floor, same fix, but targeted at the select-ui DESCENDANT directly
289
+ rather than set as an inheriting custom property on the [data-scope]
290
+ wrapper: select-ui's own `:where(:scope) { --select-min-width: 20ch }`
291
+ (select.css) directly assigns the property on the select-ui element
292
+ itself — a direct assignment always wins over an inherited value
293
+ regardless of the assigning rule's (here, zero) specificity, so a
294
+ value merely inherited from an ancestor wrapper never actually
295
+ reaches it. Matching the select-ui element directly puts this
296
+ override in the same competition as that rule, where normal
297
+ specificity comparison (0,2,0) vs (0,0,0) applies and this wins. */
298
+ [data-scope] select-ui {
299
+ --select-min-width: 0;
300
+ }
301
+
287
302
  [data-scope]:empty { display: none; }
288
303
 
289
304
  /* ═══════ Title cluster ═══════ */
@@ -305,6 +320,25 @@
305
320
 
306
321
  [data-toolbar-count-badge][hidden] { display: none; }
307
322
 
323
+ /* Scope reconciliation — a filled [slot="scope"] already names the
324
+ entity being listed (a select/menu whose own displayed value IS the
325
+ label, e.g. "All Accounts (3)"), so the separate [text]/[count] title
326
+ cluster next to it says the same thing twice ("All accounts" +
327
+ "Accounts 3"). CSS-only, not #updateTitle() (JS never even sees
328
+ [text]/[count] are redundant here — [count] still falls back to the
329
+ bound table's row length regardless, so a JS hide would need a new
330
+ prop; this reads what's already in the DOM, same `:has()` idiom the
331
+ [data-summary]/[data-page-size] reconciliation above already uses).
332
+ A consumer wanting BOTH visible can still override with normal CSS
333
+ specificity — this isn't a hard mode switch, just the default.
334
+ [title-always] (gh#1879) is the declared opt-out for that same case:
335
+ a consumer wanting BOTH the scope select AND the explicit text/count
336
+ title visible together sets the attribute instead of reaching for an
337
+ app-side CSS override. */
338
+ :scope:not([title-always]):has([data-scope]:not(:empty)) [data-title] {
339
+ display: none;
340
+ }
341
+
308
342
  /* ═══════ Range summary ═══════ (gh#1615 — "Showing X–Y of N", independent of [data-title]) */
309
343
 
310
344
  [data-summary] {
@@ -371,7 +405,17 @@
371
405
  --table-toolbar-bp-overflow's own default (same duplication +
372
406
  source-assertion discipline as the search-tight/icon-only boundaries
373
407
  above). */
374
- [data-toolbar-overflow] { display: none; }
408
+ /* BUG (found live, verified in a real browser — a fresh table-toolbar-ui
409
+ at ANY stage, including "full", rendered Filter/Sort/Columns AND the
410
+ "More" trigger simultaneously): menu-ui's own base rule (menu.css,
411
+ `:scope { display: flex; }`) is the same (0,1,0) specificity as a bare
412
+ `[data-toolbar-overflow]` selector — a cascade TIE this file's source
413
+ order loses (menu.css wins), so this default-hide rule never actually
414
+ applied. The `:scope … [data-toolbar-overflow][hidden]` override below
415
+ already carries the `:scope` prefix for exactly this reason (its own
416
+ comment: "must match or exceed the (0,3,0) specificity … to win the
417
+ tie") — this rule needed the same prefix and never got it. */
418
+ :scope [data-toolbar-overflow] { display: none; }
375
419
 
376
420
  @container table-toolbar (max-width: 26rem) {
377
421
  :scope:is(:not([stage]), [stage=""]) [data-toolbar-btn] {
@@ -405,19 +449,62 @@
405
449
  conflicting author rule. */
406
450
  :scope [data-toolbar-overflow][hidden] { display: none; }
407
451
 
452
+ /* menu-ui's own popover carries a hardcoded `min-width: 10rem` (menu.css
453
+ — a LITERAL, not even the `--menu-popover-min-width` token declared
454
+ next to it: the popover is promoted to the top layer via the Popover
455
+ API but stays a real light-DOM child of <menu-ui>, per menu.css's own
456
+ comment "the top-layer element can't inherit the scoped token", so
457
+ that rule hardcodes the value instead of reading the custom property).
458
+ Same class of bug as --input-min-width/--select-min-width above: sized
459
+ for a generic dropdown menu of arbitrary action items, not this
460
+ specific "More" trigger's three short labels (Filter/Sort/Columns),
461
+ each already narrower than 10rem on its own. Scoped to JUST this
462
+ menu-ui instance (`[data-toolbar-overflow]`) — the [data-menu-popover]
463
+ it targets is menu-ui's own internal implementation selector, not
464
+ table-toolbar's; overriding the LITERAL selector directly here rather
465
+ than touching menu.css keeps every other menu-ui consumer's own
466
+ 10rem floor (a reasonable default for a menu of arbitrary items)
467
+ untouched. */
468
+ :scope [data-toolbar-overflow] [data-menu-popover] {
469
+ min-width: 0;
470
+ }
471
+
408
472
  /* ═══════ Search ═══════ */
409
473
 
410
474
  [data-search] {
411
475
  flex: 1 1 var(--table-toolbar-search-min);
412
476
  /* gh#1076 — never collapse to 0: floor at a usable width and let the
413
- flex-wrap above move it to its own row when the container is tight. */
414
- min-width: min(10rem, 100%);
477
+ flex-wrap above move it to its own row when the container is tight.
478
+ BUG (found live, 2026-08-22): this used to be a hardcoded
479
+ `min(10rem, 100%)`, independent of `--table-toolbar-search-min` —
480
+ at the search-tight/icon-only/overflow stages that token gets
481
+ reassigned to `--table-toolbar-search-tight-min` (9rem default),
482
+ but the hardcoded 10rem floor here still won, so the tight tier's
483
+ own declared 9rem target could never actually be reached. Deriving
484
+ the floor from the SAME token as the flex-basis keeps them in sync
485
+ at every stage instead of drifting against each other. */
486
+ min-width: min(var(--table-toolbar-search-min), 100%);
415
487
  max-width: var(--table-toolbar-search-max);
416
488
  margin-inline-start: auto;
417
489
  }
418
490
 
419
491
  [data-search][hidden] { display: none; }
420
492
 
493
+ /* input-ui's own --input-min-width default (20ch — input.css) is sized
494
+ for a BARE, standalone field where the component has no other context
495
+ to size against. Inside [data-search] that's a second, independent
496
+ floor competing with the one above — at the tight/icon-only/overflow
497
+ stages input-ui's 20ch (~164px) is WIDER than
498
+ --table-toolbar-search-tight-min (9rem/144px), so it silently won
499
+ regardless of what [data-search]'s own min-width — even after the fix
500
+ above — asked for. table-toolbar-ui owns the search field's sizing
501
+ budget end-to-end (it's the one deciding how much room is left after
502
+ the title/controls/page-size clusters); input-ui's independent
503
+ opinion has nothing to add here and only fights the outer token. */
504
+ [data-search] input-ui {
505
+ --input-min-width: 0;
506
+ }
507
+
421
508
  /* ═══════ Actions-leading slot ═══════ (gh#1649 — app-owned triggers, e.g.
422
509
  custom Filter/Columns buttons, that land BEFORE the page-size select)
423
510
  Carries the SAME margin-inline-start:auto as [data-search]/[data-page-size]
@@ -449,9 +536,23 @@
449
536
  is safe — only the first one with unclaimed free space to its left
450
537
  actually receives it. */
451
538
 
539
+ /* select-ui's own --select-min-width default (20ch — select.css,
540
+ gh#1633, the SAME UA-baseline floor input.css's --input-min-width
541
+ already documents for input-ui) is sized for a bare, standalone
542
+ select. Found live (root-caused alongside the earlier search-field
543
+ min-width fix, same shape): with no override here, [data-page-size]
544
+ and [data-scope] select-ui both floor at ~151px regardless of how
545
+ short their actual content is ("25 / page", "All Accounts (3)") —
546
+ visible dead space inside the trigger. The filter/sort/columns
547
+ popover's OWN per-column selects already avoid this (they share the
548
+ `[data-filter-input]` marker with the text-input case, and
549
+ `[data-toolbar-popover] [data-filter-input] { min-width: 0; }`
550
+ already resets it there) — this is the same fix for the two
551
+ main-row selects that marker doesn't reach. */
452
552
  [data-page-size] {
453
553
  flex: 0 0 auto;
454
554
  margin-inline-start: auto;
555
+ --select-min-width: 0;
455
556
  }
456
557
 
457
558
  [data-page-size][hidden] { display: none; }
@@ -46,14 +46,22 @@ export class UITableToolbar extends UIElement {
46
46
  placeholder: string;
47
47
  /** 1-indexed last row number of the current page, for the range summary (e.g. `25` in "Showing 1–25 of 320"). See rangeStart. */
48
48
  rangeEnd: number;
49
+ /** gh#1877, ADR-0082 Amendment — optional per-slice noun appended after the range total ("of 80 users") so consumers stop hand-painting the noun themselves. Applies in both the normal finite range summary and the open/unproven-total summary ("Showing 51–100 users" with no "of N"). Unset (the default) changes nothing. Named identically to table-footer-ui's own [range-noun] (ADR-0082 §"same name on both companions"). */
50
+ rangeNoun: string;
51
+ /** gh#1877, ADR-0082 Amendment — secondary whole-set total for the "filtered-of-whole" shape: a client-side facet narrows [rangeTotal] (a filtered count) while rangeOf names the unfiltered set size, e.g. "Showing 1–4 of 4 traversals (6 total)". Appends "(of <rangeOf> total)" onto the normal finite range summary; unset (the default) changes nothing. Never applies while the summary is in its open ("?") or opted-out (all-zero) state. Named identically to table-footer-ui's own [range-of] (ADR-0082 §"same name on both companions"). */
52
+ rangeOf: number;
49
53
  /** 1-indexed first row number of the current page, for the "Showing X–Y of N" range summary (e.g. `1` in "Showing 1–25 of 320"). Renders alongside — never in place of — [text]/[count]; the range summary is its own cluster, independent of the title cluster. All three of rangeStart/rangeEnd/rangeTotal must be set (> 0) for the summary to render; omit all three (the default) to opt out entirely. */
50
54
  rangeStart: number;
51
- /** Total row count across all pages, for the range summary (e.g. `320` in "Showing 1–25 of 320"). Distinct from pagination-ui's `total` (total PAGES) — this is total ROWS; deliberately not spelled `total` to avoid that cross-sibling name collision (ADR-0063 B5). See rangeStart. */
52
- rangeTotal: number;
55
+ /** Total row count across all pages, for the range summary (e.g. `320` in "Showing 1–25 of 320"). Distinct from pagination-ui's `total` (total PAGES) — this is total ROWS; deliberately not spelled `total` to avoid that cross-sibling name collision (ADR-0063 B5). See rangeStart. Explicit range-total="?" (gh#1877, ADR-0082 Amendment) is the OPEN/unproven-total state — cursor/hasMore server paging with no known row count yet: the summary reads "Showing X–Y" with no "of N", requiring only [range-start]/[range-end] (> 0) to render, not rangeTotal too. Resolves to the normal finite summary the moment a real number replaces the "?". Named identically to table-footer-ui's own [range-total] (ADR-0082 §"same name on both companions"). */
56
+ rangeTotal: number | string;
57
+ /** Toolbar scale, via sizing.css's universal [size="sm|md|lg"] ambient tokens. Not table-toolbar-specific chrome — the same attribute that scales any component. Set it once on the toolbar instead of on each slotted scope/actions-leading/actions control individually; anything slotted with no [size] of its own inherits the ambient value. Default (empty) = md. */
58
+ size: 'sm' | 'md' | 'lg';
53
59
  /** Explicit compaction-stage override (ADR-0076). Unset (the default, empty string) means auto-snap: the toolbar's own inline-size container query picks the nearest stage from its live width against the three --table-toolbar-bp-* breakpoints, snapping discretely, never interpolating. Setting stage explicitly pins that stage's rendering regardless of the container's actual width — overriding the container query, mirroring chart-ui's ratio override shape. Primary use: visual-eval fixtures that need a deterministic stage without resizing a real container, and a consumer embedding the toolbar inside a known-narrow panel who wants to skip auto-detection entirely. All four stages' CSS is now live (ADR-0076): search-tight steps the search field down (step 2); icon-only drops control labels and compacts/hides the range summary (steps 3+4); overflow collapses Filter/Sort/Columns into one "More" trigger routed through the same menu-ui/menu-item-ui pattern used elsewhere in the corpus (step 5). The resolved stage — the pin if set, otherwise the live width classification — also reflects onto the host as `data-stage-resolved` (step 7, informational only, mirrors chart-ui's `data-ratio-resolved` convention — never drives table-toolbar's own rendering, which stays 100% CSS-driven). */
54
60
  stage: '' | 'full' | 'search-tight' | 'icon-only' | 'overflow';
55
61
  /** Title text shown on the left. */
56
62
  text: string;
63
+ /** Opt out of the scope-reconciliation title hide: by default, a filled [slot="scope"] region (a select/menu whose own displayed value already names the entity being listed) hides the separate [text]/[count] title cluster next to it, since together they'd say the same thing twice. Set [title-always] to keep the title cluster visible alongside a filled scope slot — e.g. a scope select PLUS an explicit text/count title on the same row (gh#1879). */
64
+ titleAlways: boolean;
57
65
  /** Toolbar visual variant. `default` renders bare on parent surface; `card` adds the same chrome as a card-ui header. */
58
66
  variant: 'default' | 'card';
59
67
 
@@ -3,56 +3,58 @@
3
3
  ## Wiring
4
4
 
5
5
  ```html
6
- <table-toolbar-ui for="members" text="All employees" count="32"></table-toolbar-ui>
7
6
  <card-ui>
8
- <section-ui bleed>
9
- <table-ui id="members" sortable raw>…</table-ui>
10
- </section-ui>
7
+ <header>
8
+ <table-toolbar-ui for="members" text="All employees" count="32"></table-toolbar-ui>
9
+ </header>
10
+ <section bleed>
11
+ <table-ui id="members" sortable>…</table-ui>
12
+ </section>
11
13
  </card-ui>
12
14
  ```
13
15
 
14
16
  ## Rendered demos
15
17
 
16
18
  ```html
17
- <col-ui gap="0">
18
- <table-toolbar-ui for="demo-table" text="All employees" count="32" placeholder="Search employees…"></table-toolbar-ui>
19
- <card-ui>
20
- <section bleed>
21
- <table-ui id="demo-table" sortable raw>
22
- <table>
23
- <thead>
24
- <tr>
25
- <th>Name</th>
26
- <th>Role</th>
27
- <th>Status</th>
28
- </tr>
29
- </thead>
30
- <tbody>
31
- <tr>
32
- <td>Maya Chen</td>
33
- <td>Designer</td>
34
- <td>
35
- <tag-ui variant="success">Active</tag-ui>
36
- </td>
37
- </tr>
38
- <tr>
39
- <td>Alex Park</td>
40
- <td>Engineer</td>
41
- <td>
42
- <tag-ui variant="success">Active</tag-ui>
43
- </td>
44
- </tr>
45
- <tr>
46
- <td>Jordan Lee</td>
47
- <td>Product</td>
48
- <td>
49
- <tag-ui variant="warning">Away</tag-ui>
50
- </td>
51
- </tr>
52
- </tbody>
53
- </table>
54
- </table-ui>
55
- </section>
56
- </card-ui>
57
- </col-ui>
19
+ <card-ui>
20
+ <header>
21
+ <table-toolbar-ui for="demo-table" text="All employees" count="32" placeholder="Search employees…"></table-toolbar-ui>
22
+ </header>
23
+ <section bleed>
24
+ <table-ui id="demo-table" sortable raw>
25
+ <table>
26
+ <thead>
27
+ <tr>
28
+ <th>Name</th>
29
+ <th>Role</th>
30
+ <th>Status</th>
31
+ </tr>
32
+ </thead>
33
+ <tbody>
34
+ <tr>
35
+ <td>Maya Chen</td>
36
+ <td>Designer</td>
37
+ <td>
38
+ <tag-ui variant="success">Active</tag-ui>
39
+ </td>
40
+ </tr>
41
+ <tr>
42
+ <td>Alex Park</td>
43
+ <td>Engineer</td>
44
+ <td>
45
+ <tag-ui variant="success">Active</tag-ui>
46
+ </td>
47
+ </tr>
48
+ <tr>
49
+ <td>Jordan Lee</td>
50
+ <td>Product</td>
51
+ <td>
52
+ <tag-ui variant="warning">Away</tag-ui>
53
+ </td>
54
+ </tr>
55
+ </tbody>
56
+ </table>
57
+ </table-ui>
58
+ </section>
59
+ </card-ui>
58
60
  ```
@@ -40,6 +40,20 @@ props:
40
40
  type: string
41
41
  default: ""
42
42
  reflect: true
43
+ size:
44
+ description: >-
45
+ Toolbar scale, via sizing.css's universal [size="sm|md|lg"] ambient
46
+ tokens. Not table-toolbar-specific chrome — the same attribute that
47
+ scales any component. Set it once on the toolbar instead of on each
48
+ slotted scope/actions-leading/actions control individually; anything
49
+ slotted with no [size] of its own inherits the ambient value.
50
+ Default (empty) = md.
51
+ type: string
52
+ default: ""
53
+ enum:
54
+ - sm
55
+ - md
56
+ - lg
43
57
  text:
44
58
  description: Title text shown on the left.
45
59
  type: string
@@ -74,11 +88,45 @@ props:
74
88
  in "Showing 1–25 of 320"). Distinct from pagination-ui's `total`
75
89
  (total PAGES) — this is total ROWS; deliberately not spelled `total`
76
90
  to avoid that cross-sibling name collision (ADR-0063 B5). See
77
- rangeStart.
78
- type: number
91
+ rangeStart. Explicit range-total="?" (gh#1877, ADR-0082 Amendment) is
92
+ the OPEN/unproven-total state — cursor/hasMore server paging with no
93
+ known row count yet: the summary reads "Showing X–Y" with no "of N",
94
+ requiring only [range-start]/[range-end] (> 0) to render, not
95
+ rangeTotal too. Resolves to the normal finite summary the moment a
96
+ real number replaces the "?". Named identically to table-footer-ui's
97
+ own [range-total] (ADR-0082 §"same name on both companions").
98
+ type: [number, string]
79
99
  default: 0
80
100
  reflect: true
81
101
  attribute: range-total
102
+ rangeOf:
103
+ description: >-
104
+ gh#1877, ADR-0082 Amendment — secondary whole-set total for the
105
+ "filtered-of-whole" shape: a client-side facet narrows [rangeTotal]
106
+ (a filtered count) while rangeOf names the unfiltered set size, e.g.
107
+ "Showing 1–4 of 4 traversals (6 total)". Appends "(of <rangeOf>
108
+ total)" onto the normal finite range summary; unset (the default)
109
+ changes nothing. Never applies while the summary is in its open
110
+ ("?") or opted-out (all-zero) state. Named identically to
111
+ table-footer-ui's own [range-of] (ADR-0082 §"same name on both
112
+ companions").
113
+ type: number
114
+ default: 0
115
+ reflect: true
116
+ attribute: range-of
117
+ rangeNoun:
118
+ description: >-
119
+ gh#1877, ADR-0082 Amendment — optional per-slice noun appended after
120
+ the range total ("of 80 users") so consumers stop hand-painting the
121
+ noun themselves. Applies in both the normal finite range summary and
122
+ the open/unproven-total summary ("Showing 51–100 users" with no "of
123
+ N"). Unset (the default) changes nothing. Named identically to
124
+ table-footer-ui's own [range-noun] (ADR-0082 §"same name on both
125
+ companions").
126
+ type: string
127
+ default: ""
128
+ reflect: true
129
+ attribute: range-noun
82
130
  pageSize:
83
131
  description: >-
84
132
  Current rows-per-page value, shown as the selected option in the
@@ -176,6 +224,19 @@ props:
176
224
  default: false
177
225
  reflect: true
178
226
  attribute: chrome-only
227
+ titleAlways:
228
+ description: >-
229
+ Opt out of the scope-reconciliation title hide: by default, a filled
230
+ [slot="scope"] region (a select/menu whose own displayed value already
231
+ names the entity being listed) hides the separate [text]/[count]
232
+ title cluster next to it, since together they'd say the same thing
233
+ twice. Set [title-always] to keep the title cluster visible alongside
234
+ a filled scope slot — e.g. a scope select PLUS an explicit
235
+ text/count title on the same row (gh#1879).
236
+ type: boolean
237
+ default: false
238
+ reflect: true
239
+ attribute: title-always
179
240
  placeholder:
180
241
  description: Placeholder text for the search input.
181
242
  type: string
@@ -329,13 +390,15 @@ a2ui:
329
390
  update the table.
330
391
  - >-
331
392
  Use [slot="scope"] for a leading scope/view switcher (an org's
332
- teams, a saved view) rendered before [text]/[count]. Use
333
- [range-start] / [range-end] / [range-total] together for a
334
- "Showing X–Y of N" summaryit renders independently of, and
335
- alongside, [text]/[count], never in place of them. Use [page-size]
336
- + [page-size-options] for a rows-per-page select; it applies
337
- directly to the bound table's [paginate] prop the same way
338
- filter/sort/columns changes apply directly to the target.
393
+ teams, a saved view) rendered before [text]/[count]. By default, a
394
+ filled scope slot hides the [text]/[count] title cluster (they'd
395
+ otherwise repeat the same entity name) set [title-always] to keep
396
+ both visible together. Use [range-start] / [range-end] /
397
+ [range-total] together for a "Showing X–Y of N" summary — it renders
398
+ independently of, and alongside, [text]/[count], never in place of
399
+ them. Use [page-size] + [page-size-options] for a rows-per-page
400
+ select; it applies directly to the bound table's [paginate] prop the
401
+ same way filter/sort/columns changes apply directly to the target.
339
402
  - >-
340
403
  Use [slot="actions-leading"] (gh#1649) for app-owned trigger
341
404
  buttons that must land BEFORE the page-size select — e.g. custom
@@ -359,6 +422,17 @@ a2ui:
359
422
  normal "Showing X-Y of N" text (ADR-0076, REQ-E). Only fires on an
360
423
  EXPLICIT range-total="0" — omitting range-* entirely (loading) never
361
424
  shows it.
425
+ - >-
426
+ gh#1877, ADR-0082 Amendment — cursor/hasMore server paging (no
427
+ server-reported row count) sets [range-total="?"] instead of a real
428
+ number: the summary reads "Showing 51–100" with no "of N" (needs only
429
+ [range-start]/[range-end] > 0 to render), resolving to the normal
430
+ finite summary once a real [range-total] arrives. Use [range-of] for
431
+ the separate "filtered-of-whole" shape (a filtered range-total plus a
432
+ secondary unfiltered-set total, e.g. "of 4 traversals (6 total)"),
433
+ and [range-noun] to stop hand-painting the per-slice noun ("of 80
434
+ users"). Same three attributes, same names, on table-footer-ui
435
+ (ADR-0082 §"same name on both companions").
362
436
  - >-
363
437
  A plain <button-ui> slotted into [slot="actions-leading"] (e.g. the
364
438
  app-owned Filter/Columns triggers above) already collapses to
@@ -406,6 +480,20 @@ examples:
406
480
  {"id": "sec", "component": "Section", "bleed": true, "children": ["tbl"]},
407
481
  {"id": "tbl", "component": "Table", "id": "accounts", "sortable": true, "raw": true}
408
482
  ]
483
+ - name: table-toolbar-open-total
484
+ description: >-
485
+ gh#1877, ADR-0082 Amendment — cursor/hasMore server paging with no
486
+ known row count yet: [range-total="?"] renders "Showing 1–50
487
+ subscriptions" (via [range-noun]) with no "of N", resolving to the
488
+ normal finite summary once a real total arrives.
489
+ a2ui: >-
490
+ [
491
+ {"id": "root", "component": "Column", "gap": "3", "children": ["bar", "card"]},
492
+ {"id": "bar", "component": "TableToolbar", "for": "subs", "range-start": 1, "range-end": 50, "range-total": "?", "range-noun": "subscriptions"},
493
+ {"id": "card", "component": "Card", "children": ["sec"]},
494
+ {"id": "sec", "component": "Section", "bleed": true, "children": ["tbl"]},
495
+ {"id": "tbl", "component": "Table", "id": "subs", "raw": true}
496
+ ]
409
497
  keywords:
410
498
  - table-toolbar
411
499
  - data-grid
package/core/anchor.js CHANGED
@@ -122,7 +122,27 @@ function anchorNative(anchor, popover, { placement, gap, matchWidth }) {
122
122
  popover.style.left = '';
123
123
 
124
124
  return () => {
125
- anchor.style.anchorName = prevAnchorName;
125
+ // Ownership-guarded restore (bug, found live): a trigger element that
126
+ // is ITSELF a popover's own `[slot="trigger"]` can become the anchor
127
+ // for a SECOND, nested popover opened from inside the first (menu-ui's
128
+ // dropdown item dispatching `action`, which table-toolbar-ui's overflow
129
+ // "More" trigger handles by re-anchoring a filter/sort/columns panel to
130
+ // that SAME element). Anchor-name assignment here is a blind overwrite,
131
+ // never a set/append — so whichever `anchorPopover()` call's cleanup
132
+ // runs LAST always wins, even when it's cleaning up a DIFFERENT,
133
+ // already-superseded anchor-name than the one currently on the element.
134
+ // menu-ui's own close (`render()`'s reactive `#hide()`, scheduled async
135
+ // — reordering the synchronous call site doesn't change when this
136
+ // runs) fires after the nested popover's `anchorPopover()` has already
137
+ // claimed the trigger, and unconditionally restoring `prevAnchorName`
138
+ // clobbered that fresh claim back to empty — the nested popover's own
139
+ // `position-anchor` was left pointing at a name resolving to nothing,
140
+ // rendering unpositioned at the viewport origin instead of near its
141
+ // trigger. Only restore when this cleanup's OWN name is still the
142
+ // current value — i.e., nobody has claimed the anchor since; otherwise
143
+ // leave it alone, since undoing someone else's still-active claim is
144
+ // never correct regardless of call order.
145
+ if (anchor.style.anchorName === name) anchor.style.anchorName = prevAnchorName;
126
146
  for (const [k, v] of Object.entries(prevStyles)) popover.style[k] = v;
127
147
  };
128
148
  }
@@ -2089,7 +2089,7 @@
2089
2089
  "type": {
2090
2090
  "text": "string"
2091
2091
  },
2092
- "description": "Chart 2.0 aspect-ratio bucket (ADR-0074). Unset (default) auto-snaps to the nearest of the three studied ratios by comparing the chart's own live box aspect against two midpoint boundaries (5:4 between 3:2/1:1, 4:5 between 1:1/2:3) — never interpolated; the resolved bucket reflects onto the host as `data-ratio-resolved`. Setting `ratio` explicitly pins that bucket regardless of the box's actual aspect, overriding the auto-snap. Orthogonal to `size` (overall scale) and `type`/`color` (ADR-0064's family axis) — this attribute lands the grammar + snap plumbing only; per-type studied renderings per bucket are later waves (gh#1624 plan steps 2-4)."
2092
+ "description": "Chart 2.0 aspect-ratio bucket (ADR-0074). Unset (default) auto-snaps to the nearest of the three studied ratios by comparing the chart's own live box aspect against two midpoint boundaries (5:4 between 3:2/1:1, 4:5 between 1:1/2:3) — never interpolated; the resolved bucket reflects onto the host as `data-ratio-resolved`. Setting `ratio` explicitly pins that bucket regardless of the box's actual aspect, overriding the auto-snap. Orthogonal to `size` (overall scale) and `type`/`color` (ADR-0064's family axis) — per-type studied renderings per bucket shipped across Waves A-C (gh#1624 plan steps 2-4: radial-bar/gauge/segments/stacked-bar/grouped-bar/multi-line, funnel/treemap/sankey/composed) — all 18 types have dedicated per-ratio render paths."
2093
2093
  },
2094
2094
  {
2095
2095
  "name": "labels",
@@ -9965,11 +9965,26 @@
9965
9965
  },
9966
9966
  {
9967
9967
  "name": "range-total",
9968
+ "type": {
9969
+ "text": "[number, string]"
9970
+ },
9971
+ "default": "0",
9972
+ "description": "Total ROWS across all pages. Absent (the default) means \"not yet known\" (loading) — never \"zero\"; both the range label and the pager stay hidden until this is set. Explicit range-total=\"0\" is the confirmed-empty state (pager hidden; the `empty` slot renders in the range label's position when supplied). Named identically to table-toolbar-ui's [range-total] and table-ui's own [range-total] (gh#1754, ADR-0082) and deliberately NOT `total` (pagination-ui's `total` is total PAGES — the ADR-0063 B5 collision rule). Explicit range-total=\"?\" (gh#1877, ADR-0082 Amendment) is the OPEN/unproven- total state — cursor/hasMore server paging with no known row count yet: the range label renders \"Showing X–Y\" with no \"of N\", and the pager stays in has-more mode (next enabled) rather than hiding — distinct from both \"loading\" (attribute absent) and \"confirmed empty\" (range-total=\"0\"). Resolves to the normal finite behavior the moment a real number replaces the \"?\". When this attribute is unset, the footer derives it from the resolved target via a two-branch ladder (REQ-W-006, amended 0.2.0/ADR-0082): (1) the target's own [range-total] attribute, when present — the table-authoritative SERVER mode (gh#1754 Shape 1's second lawful composition; the target's own \"?\" carries through identically); the identity rule: the footer's range-total derives from the table's range-total, never the table's filteredCount, and this branch wins even when the target's [paginate] is also > 0; (2) otherwise, when the target has [paginate] > 0, CLIENT mode: the target's filtered row count (search + column filters applied, before pagination). Refreshes on the target's `page` / `filter-change` / `sort` events in either branch. Widened named staleness edge: a programmatic `.data` swap on the target — or a `range-total` update on the target without an accompanying page move — emits no event; either re-set this footer's own attributes after such a change, or accept staleness until the target's next event."
9973
+ },
9974
+ {
9975
+ "name": "range-of",
9968
9976
  "type": {
9969
9977
  "text": "number"
9970
9978
  },
9971
9979
  "default": "0",
9972
- "description": "Total ROWS across all pages. Absent (the default) means \"not yet known\" (loading) — never \"zero\"; both the range label and the pager stay hidden until this is set. Explicit range-total=\"0\" is the confirmed-empty state (pager hidden; the `empty` slot renders in the range label's position when supplied). Named identically to table-toolbar-ui's [range-total] and table-ui's own [range-total] (gh#1754, ADR-0082) and deliberately NOT `total` (pagination-ui's `total` is total PAGES — the ADR-0063 B5 collision rule). When this attribute is unset, the footer derives it from the resolved target via a two-branch ladder (REQ-W-006, amended 0.2.0/ADR-0082): (1) the target's own [range-total] attribute, when present — the table-authoritative SERVER mode (gh#1754 Shape 1's second lawful composition); the identity rule: the footer's range-total derives from the table's range-total, never the table's filteredCount, and this branch wins even when the target's [paginate] is also > 0; (2) otherwise, when the target has [paginate] > 0, CLIENT mode: the target's filtered row count (search + column filters applied, before pagination). Refreshes on the target's `page` / `filter-change` / `sort` events in either branch. Widened named staleness edge: a programmatic `.data` swap on the target — or a `range-total` update on the target without an accompanying page move — emits no event; either re-set this footer's own attributes after such a change, or accept staleness until the target's next event."
9980
+ "description": "gh#1877, ADR-0082 Amendment secondary whole-set total for the \"filtered-of-whole\" shape: a client-side facet narrows [range-total] (a filtered count) while range-of names the unfiltered set size, e.g. \"Showing 1–4 of 4 traversals (6 total)\". Appends \"(of <range-of> total)\" onto the normal finite range text; unset (the default) changes nothing. Never applies while [range-total] is absent (loading) or \"?\" (open/unproven) there's no finite range text to append onto yet. Named identically on table-toolbar-ui (ADR-0082 §\"same name on both companions\")."
9981
+ },
9982
+ {
9983
+ "name": "range-noun",
9984
+ "type": {
9985
+ "text": "string"
9986
+ },
9987
+ "description": "gh#1877, ADR-0082 Amendment — optional per-slice noun appended after the range total (\"of 80 users\") so consumers stop hand-painting the noun themselves. Applies in both the normal finite range text and the open/unproven-total text (\"Showing 51–100 users\" with no \"of N\"). Unset (the default) changes nothing. Named identically on table-toolbar-ui (ADR-0082 §\"same name on both companions\")."
9973
9988
  },
9974
9989
  {
9975
9990
  "name": "siblings",
@@ -10044,6 +10059,13 @@
10044
10059
  },
10045
10060
  "description": "id-ref of the table-ui to control. Falls back to the first sibling table-ui within the same parent when omitted."
10046
10061
  },
10062
+ {
10063
+ "name": "size",
10064
+ "type": {
10065
+ "text": "string"
10066
+ },
10067
+ "description": "Toolbar scale, via sizing.css's universal [size=\"sm|md|lg\"] ambient tokens. Not table-toolbar-specific chrome — the same attribute that scales any component. Set it once on the toolbar instead of on each slotted scope/actions-leading/actions control individually; anything slotted with no [size] of its own inherits the ambient value. Default (empty) = md."
10068
+ },
10047
10069
  {
10048
10070
  "name": "text",
10049
10071
  "type": {
@@ -10076,11 +10098,26 @@
10076
10098
  },
10077
10099
  {
10078
10100
  "name": "range-total",
10101
+ "type": {
10102
+ "text": "[number, string]"
10103
+ },
10104
+ "default": "0",
10105
+ "description": "Total row count across all pages, for the range summary (e.g. `320` in \"Showing 1–25 of 320\"). Distinct from pagination-ui's `total` (total PAGES) — this is total ROWS; deliberately not spelled `total` to avoid that cross-sibling name collision (ADR-0063 B5). See rangeStart. Explicit range-total=\"?\" (gh#1877, ADR-0082 Amendment) is the OPEN/unproven-total state — cursor/hasMore server paging with no known row count yet: the summary reads \"Showing X–Y\" with no \"of N\", requiring only [range-start]/[range-end] (> 0) to render, not rangeTotal too. Resolves to the normal finite summary the moment a real number replaces the \"?\". Named identically to table-footer-ui's own [range-total] (ADR-0082 §\"same name on both companions\")."
10106
+ },
10107
+ {
10108
+ "name": "range-of",
10079
10109
  "type": {
10080
10110
  "text": "number"
10081
10111
  },
10082
10112
  "default": "0",
10083
- "description": "Total row count across all pages, for the range summary (e.g. `320` in \"Showing 1–25 of 320\"). Distinct from pagination-ui's `total` (total PAGES) this is total ROWS; deliberately not spelled `total` to avoid that cross-sibling name collision (ADR-0063 B5). See rangeStart."
10113
+ "description": "gh#1877, ADR-0082 Amendment secondary whole-set total for the \"filtered-of-whole\" shape: a client-side facet narrows [rangeTotal] (a filtered count) while rangeOf names the unfiltered set size, e.g. \"Showing 1–4 of 4 traversals (6 total)\". Appends \"(of <rangeOf> total)\" onto the normal finite range summary; unset (the default) changes nothing. Never applies while the summary is in its open (\"?\") or opted-out (all-zero) state. Named identically to table-footer-ui's own [range-of] (ADR-0082 §\"same name on both companions\")."
10114
+ },
10115
+ {
10116
+ "name": "range-noun",
10117
+ "type": {
10118
+ "text": "string"
10119
+ },
10120
+ "description": "gh#1877, ADR-0082 Amendment — optional per-slice noun appended after the range total (\"of 80 users\") so consumers stop hand-painting the noun themselves. Applies in both the normal finite range summary and the open/unproven-total summary (\"Showing 51–100 users\" with no \"of N\"). Unset (the default) changes nothing. Named identically to table-footer-ui's own [range-noun] (ADR-0082 §\"same name on both companions\")."
10084
10121
  },
10085
10122
  {
10086
10123
  "name": "page-size",
@@ -10145,6 +10182,14 @@
10145
10182
  "default": "false",
10146
10183
  "description": "Suppress all four native controls/search at once (filter, sort, columns, search) as additive sugar over noFilter/noSort/noColumns/ noSearch (ADR-0076, ADIA2-9123). Precedence is pure, absolute OR — while set, all four stay off regardless of any individual no-* attribute's own value, with no partial re-enable path; to re-enable one control, remove chrome-only entirely and set the other three no-* attributes explicitly instead. The four granular attributes are not deprecated or removed — they remain the independently-addressable shipped API; chrome-only never replaces them, it's a convenience preset on top."
10147
10184
  },
10185
+ {
10186
+ "name": "title-always",
10187
+ "type": {
10188
+ "text": "boolean"
10189
+ },
10190
+ "default": "false",
10191
+ "description": "Opt out of the scope-reconciliation title hide: by default, a filled [slot=\"scope\"] region (a select/menu whose own displayed value already names the entity being listed) hides the separate [text]/[count] title cluster next to it, since together they'd say the same thing twice. Set [title-always] to keep the title cluster visible alongside a filled scope slot — e.g. a scope select PLUS an explicit text/count title on the same row (gh#1879)."
10192
+ },
10148
10193
  {
10149
10194
  "name": "placeholder",
10150
10195
  "type": {
@@ -10330,10 +10375,10 @@
10330
10375
  {
10331
10376
  "name": "range-total",
10332
10377
  "type": {
10333
- "text": "number"
10378
+ "text": "[number, string]"
10334
10379
  },
10335
10380
  "default": "0",
10336
- "description": "Total row count across ALL server pages (gh#1754, ADR-0082) — the table-authoritative server-mode data contract. Presence-gated: the attribute's PRESENCE, not its value, is the mode switch (checked via hasAttribute, never the coerced value alone — the same discipline table-footer-ui's own [range-total] ships). Absent (the default) means client mode: today's behavior exactly, [paginate] slices `.data` as always. Present with [paginate] > 0 means server mode: no local slicing (`.data` IS the current page and renders whole, after local search/sort/filter), the internal pager's page count becomes `max(1, ceil(range-total / paginate))` (one calc site, consolidated), and the internal page-reset sites (`data` set, `setFilter`, `clearFilters`) are suppressed so a fetch write-back never fights the pager back to page 0 — page state then moves only via pager interaction, the `footer-page` command, or `setState()`. The existing `page` event (0-based, unchanged) becomes the fetch trigger: the consumer listens for it, fetches that server page, and writes `.data` back. Explicit `range-total=\"0\"` is server-confirmed empty (pager hidden), never conflated with absent. With [paginate] absent/0, [range-total] is inert for this table's own rendering but stays readable by a bound table-footer-ui (REQ-W-006 branch 2) for its count-only label — see the footer-authoritative shape below, which remains lawful and is unaffected by this attribute. Named identically to table-toolbar-ui's and table-footer-ui's own [range-total] (rows, never `pagination-ui[total]`'s pages — the ADR-0063 B5 collision rule); a third instance of one name for one concept."
10381
+ "description": "Total row count across ALL server pages (gh#1754, ADR-0082) — the table-authoritative server-mode data contract. Presence-gated: the attribute's PRESENCE, not its value, is the mode switch (checked via hasAttribute, never the coerced value alone — the same discipline table-footer-ui's own [range-total] ships). Absent (the default) means client mode: today's behavior exactly, [paginate] slices `.data` as always. Present with [paginate] > 0 means server mode: no local slicing (`.data` IS the current page and renders whole, after local search/sort/filter), the internal pager's page count becomes `max(1, ceil(range-total / paginate))` (one calc site, consolidated), and the internal page-reset sites (`data` set, `setFilter`, `clearFilters`) are suppressed so a fetch write-back never fights the pager back to page 0 — page state then moves only via pager interaction, the `footer-page` command, or `setState()`. The existing `page` event (0-based, unchanged) becomes the fetch trigger: the consumer listens for it, fetches that server page, and writes `.data` back. Explicit `range-total=\"0\"` is server-confirmed empty (pager hidden), never conflated with absent. With [paginate] absent/0, [range-total] is inert for this table's own rendering but stays readable by a bound table-footer-ui (REQ-W-006 branch 2) for its count-only label — see the footer-authoritative shape below, which remains lawful and is unaffected by this attribute. Named identically to table-toolbar-ui's and table-footer-ui's own [range-total] (rows, never `pagination-ui[total]`'s pages — the ADR-0063 B5 collision rule); a third instance of one name for one concept. Explicit `range-total=\"?\"` (gh#1877, ADR-0082 Amendment) is the OPEN/unproven-total state — cursor/hasMore server paging with no known row count yet: still server mode (still presence-gated), but the internal pager's page count stays exactly one page ahead of wherever the pager currently sits (never a fixed ceil(...) total), keeping `next` enabled indefinitely instead of hiding the pager the way a confirmed `range-total=\"0\"` would. Resolves to the normal finite server-mode behavior the instant a real number replaces the \"?\" — every reader downstream (table-toolbar-ui, table-footer-ui) shares this same distinction. Note: since this prop is Number-typed and reflected, the framework's own attribute-reflection immediately rewrites the DOM attribute to the literal string \"NaN\" once \"?\" is parsed (`+\"?\"` → NaN, then stringified back onto the attribute) — the coerced numeric PROPERTY is what carries the open/unproven signal (non-finite), never a literal \"?\" surviving as the live DOM attribute value."
10337
10382
  },
10338
10383
  {
10339
10384
  "name": "filteredCount",