@adia-ai/web-components 0.8.47 → 0.8.50

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 (68) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/MIGRATION.md +136 -0
  3. package/components/card/card.css +25 -2
  4. package/components/chart/chart.a2ui.json +1 -1
  5. package/components/chart/chart.class.js +41 -10
  6. package/components/chart/chart.d.ts +1 -1
  7. package/components/chart/chart.yaml +6 -3
  8. package/components/input/input.a2ui.json +2 -2
  9. package/components/input/input.css +45 -16
  10. package/components/input/input.yaml +11 -9
  11. package/components/link/link.css +3 -2
  12. package/components/richtext/richtext.css +2 -2
  13. package/components/search/search.a2ui.json +1 -1
  14. package/components/search/search.class.js +43 -0
  15. package/components/search/search.css +28 -0
  16. package/components/search/search.yaml +4 -1
  17. package/components/select/select.class.js +4 -1
  18. package/components/select/select.css +13 -0
  19. package/components/stat/stat.a2ui.json +30 -3
  20. package/components/stat/stat.css +88 -0
  21. package/components/stat/stat.d.ts +2 -2
  22. package/components/stat/stat.yaml +99 -6
  23. package/components/table/table.a2ui.json +5 -2
  24. package/components/table/table.class.js +36 -1
  25. package/components/table/table.d.ts +2 -2
  26. package/components/table/table.yaml +17 -1
  27. package/components/table-footer/table-footer.a2ui.json +20 -2
  28. package/components/table-footer/table-footer.class.js +94 -8
  29. package/components/table-footer/table-footer.d.ts +6 -2
  30. package/components/table-footer/table-footer.yaml +57 -6
  31. package/components/table-toolbar/table-toolbar.a2ui.json +28 -2
  32. package/components/table-toolbar/table-toolbar.class.js +69 -2
  33. package/components/table-toolbar/table-toolbar.css +36 -2
  34. package/components/table-toolbar/table-toolbar.d.ts +8 -2
  35. package/components/table-toolbar/table-toolbar.examples.md +6 -4
  36. package/components/table-toolbar/table-toolbar.yaml +140 -26
  37. package/components/theme-provider/theme-provider.a2ui.json +3 -5
  38. package/components/theme-provider/theme-provider.class.js +9 -30
  39. package/components/theme-provider/theme-provider.d.ts +3 -5
  40. package/components/theme-provider/theme-provider.yaml +4 -8
  41. package/core/element.js +16 -3
  42. package/custom-elements.json +52 -10
  43. package/dist/host.min.css +1 -1
  44. package/dist/host.sheet.js +1 -1
  45. package/dist/theme-provider.min.js +8 -8
  46. package/dist/themes.min.css +1 -1
  47. package/dist/themes.sheet.js +1 -1
  48. package/dist/web-components.min.css +1 -1
  49. package/dist/web-components.min.js +70 -69
  50. package/dist/web-components.sheet.js +1 -1
  51. package/index.css +1 -2
  52. package/package.json +1 -1
  53. package/styles/README.md +1 -1
  54. package/styles/api/sizing.css +1 -1
  55. package/styles/colors/material-static.css +34 -12
  56. package/styles/host.css +1 -4
  57. package/styles/index.css +2 -2
  58. package/styles/scale.css +19 -26
  59. package/styles/themes.css +2678 -2
  60. package/styles/type/elements.css +1 -0
  61. package/styles/type/roles.css +3 -3
  62. package/styles/typography.css +6 -5
  63. package/dist/prose.min.css +0 -1
  64. package/dist/prose.sheet.js +0 -11
  65. package/dist/verse.min.css +0 -1
  66. package/dist/verse.sheet.js +0 -11
  67. package/styles/prose.css +0 -211
  68. package/styles/verse.css +0 -151
@@ -31,11 +31,24 @@
31
31
  "type": "number",
32
32
  "default": 0
33
33
  },
34
- "range-total": {
35
- "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.",
34
+ "range-noun": {
35
+ "description": "gh#1877, ADR-0082 Amendmentoptional 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\").",
36
+ "type": "string",
37
+ "default": ""
38
+ },
39
+ "range-of": {
40
+ "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\").",
36
41
  "type": "number",
37
42
  "default": 0
38
43
  },
44
+ "range-total": {
45
+ "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.",
46
+ "type": [
47
+ "number",
48
+ "string"
49
+ ],
50
+ "default": 0
51
+ },
39
52
  "siblings": {
40
53
  "description": "Passed through to the composed pagination-ui's own [siblings] (page-number window size on each side of the current page).",
41
54
  "type": "number",
@@ -77,6 +90,11 @@
77
90
  "a2ui": "[\n {\"id\": \"root\", \"component\": \"Column\", \"gap\": \"0\", \"children\": [\"tests\", \"ftr\"]},\n {\"id\": \"tests\", \"component\": \"Table\", \"raw\": true},\n {\"id\": \"ftr\", \"component\": \"TableFooter\", \"for\": \"tests\", \"page\": 1, \"page-size\": 10, \"range-total\": 128}\n]",
78
91
  "name": "table-footer-server-mode"
79
92
  },
93
+ {
94
+ "description": "gh#1877, ADR-0082 Amendment — cursor/hasMore server mode with no known row count yet: [range-total=\"?\"] renders \"Showing 1–50 subscriptions\" (via [range-noun]) with `next` kept enabled until a real total resolves.",
95
+ "a2ui": "[\n {\"id\": \"root\", \"component\": \"Column\", \"gap\": \"0\", \"children\": [\"subs\", \"ftr\"]},\n {\"id\": \"subs\", \"component\": \"Table\", \"raw\": true},\n {\"id\": \"ftr\", \"component\": \"TableFooter\", \"for\": \"subs\", \"page\": 1, \"page-size\": 50, \"range-total\": \"?\", \"range-noun\": \"subscriptions\"}\n]",
96
+ "name": "table-footer-open-total"
97
+ },
80
98
  {
81
99
  "description": "Client-mode footer bound to a [paginate]d table-ui with its own internal pager suppressed via [no-pager].",
82
100
  "a2ui": "[\n {\"id\": \"root\", \"component\": \"Column\", \"gap\": \"0\", \"children\": [\"tests-c\", \"ftr\"]},\n {\"id\": \"tests-c\", \"component\": \"Table\", \"paginate\": 10, \"no-pager\": true, \"raw\": true},\n {\"id\": \"ftr\", \"component\": \"TableFooter\", \"for\": \"tests-c\"}\n]",
@@ -33,12 +33,23 @@
33
33
  *
34
34
  * Degradation (REQ-S):
35
35
  * - range-total ABSENT → indeterminate/loading: both regions hidden.
36
+ * - range-total EXPLICIT "?" → gh#1877/ADR-0082 Amendment — OPEN/unproven
37
+ * total (cursor/hasMore paging): "Showing
38
+ * X–Y" with no "of N", pager stays in
39
+ * has-more mode (next enabled) until a
40
+ * real range-total resolves.
36
41
  * - range-total EXPLICIT "0" → pager hidden; [slot="empty"] renders in
37
42
  * the range region if supplied, else hides.
38
43
  * - page-size ABSENT or "0" → count-only label ("128 items"), no pager
39
44
  * (REQ-S-005 — the ordinary DEFAULT state,
40
45
  * since page-size defaults to 0).
41
46
  * - derived pages ≤ 1 → pager hidden, range label still renders.
47
+ * - range-of="<whole>" → gh#1877/ADR-0082 Amendment — secondary
48
+ * whole-set total, appends "(of <whole>
49
+ * total)"; unset changes nothing.
50
+ * - range-noun="<noun>" → gh#1877/ADR-0082 Amendment — appends the
51
+ * per-slice noun ("of 80 users"); unset
52
+ * changes nothing.
42
53
  *
43
54
  * Target derivation (REQ-W-006, amended 0.2.0/ADR-0082) — a two-branch
44
55
  * ladder when range-total is unset, per-attribute:
@@ -94,6 +105,15 @@ export class UITableFooter extends UIElement {
94
105
  page: { type: Number, default: 1, reflect: true },
95
106
  pageSize: { type: Number, default: 0, reflect: true, attribute: 'page-size' },
96
107
  rangeTotal: { type: Number, default: 0, reflect: true, attribute: 'range-total' },
108
+ // gh#1877/ADR-0082 Amendment — secondary whole-set total (the
109
+ // "filtered-of-whole" gap: a client-side-narrowed range-total plus the
110
+ // unfiltered set size), and the optional per-slice noun consumers used
111
+ // to hand-paint ("of 80 users"). Both presence-gated like range-total —
112
+ // hasAttribute decides whether the suffix renders, never the coerced
113
+ // value alone (an author-set range-of="0" is nonsensical and treated
114
+ // as absent by #draw()'s safeRangeTotal reuse).
115
+ rangeOf: { type: Number, default: 0, reflect: true, attribute: 'range-of' },
116
+ rangeNoun: { type: String, default: '', reflect: true, attribute: 'range-noun' },
97
117
  // Pass-throughs to the composed pagination-ui (REQ-F-002) — the
98
118
  // defaults below deliberately mirror pagination-ui's own defaults, so
99
119
  // an unset pass-through is a no-op mirror, not a silent override.
@@ -133,7 +153,7 @@ export class UITableFooter extends UIElement {
133
153
  // one, so it needs the raw attribute callback, not the signal diff.
134
154
  attributeChangedCallback(name, oldVal, newVal) {
135
155
  super.attributeChangedCallback(name, oldVal, newVal);
136
- if (name === 'range-total') this.#draw();
156
+ if (name === 'range-total' || name === 'range-of' || name === 'range-noun') this.#draw();
137
157
  }
138
158
 
139
159
  // ── DOM stamp ────────────────────────────────────────────────────────────
@@ -316,11 +336,29 @@ export class UITableFooter extends UIElement {
316
336
  const n = Number(raw);
317
337
  return Number.isFinite(n) && n > 0 ? Math.trunc(n) : 0;
318
338
  };
319
- const rangeTotal = hasRangeTotal
320
- ? safeRangeTotal(this.rangeTotal)
321
- : targetServer
322
- ? safeRangeTotal(target.rangeTotal)
323
- : (clientMode ? (Number(target.filteredCount) || 0) : 0);
339
+ // gh#1877/ADR-0082 Amendment — the open/unproven-total state: the
340
+ // resolved source of range-total (local attribute if set, else the
341
+ // server-mode target's own) coerces to a non-finite number. NOT keyed
342
+ // on the raw attribute string — rangeTotal is a Number-typed reflected
343
+ // prop, so range-total="?" parses via `+"?"` → NaN. gh#1895 reflect()
344
+ // (core/element.js) now leaves a non-finite Number value unwritten
345
+ // instead of stomping the sentinel with the literal string "NaN", so
346
+ // getAttribute('range-total') keeps reading "?" as authored. The
347
+ // coerced PROPERTY still holds NaN stably (Object.is(NaN, NaN) is
348
+ // true, so re-parsing the unchanged attribute never re-triggers a
349
+ // write either) — that's what tells "unknown yet" apart from
350
+ // "confirmed zero" (a finite 0) once safeRangeTotal has clamped both
351
+ // to 0.
352
+ const rangeOpen = hasRangeTotal
353
+ ? !Number.isFinite(Number(this.rangeTotal))
354
+ : (targetServer && !Number.isFinite(Number(target.rangeTotal)));
355
+ const rangeTotal = rangeOpen
356
+ ? null
357
+ : hasRangeTotal
358
+ ? safeRangeTotal(this.rangeTotal)
359
+ : targetServer
360
+ ? safeRangeTotal(target.rangeTotal)
361
+ : (clientMode ? (Number(target.filteredCount) || 0) : 0);
324
362
  // page-size still derives from target.paginate in EITHER derived mode
325
363
  // (server or client) — unchanged behavior, just widened to cover the
326
364
  // targetServer branch too (a server-mode target's paginate is its
@@ -329,6 +367,15 @@ export class UITableFooter extends UIElement {
329
367
  ? (Number(this.pageSize) || 0)
330
368
  : ((targetServer || clientMode) ? (Number(target.paginate) || 0) : (Number(this.pageSize) || 0));
331
369
 
370
+ // gh#1877/ADR-0082 Amendment — range-of (secondary whole-set total) and
371
+ // range-noun (per-slice noun) are both additive suffixes onto the
372
+ // normal finite "of N" text; neither applies to the loading, confirmed-
373
+ // empty, or open-total branches below.
374
+ const hasRangeOf = this.hasAttribute('range-of');
375
+ const rangeOfVal = hasRangeOf ? safeRangeTotal(this.rangeOf) : 0;
376
+ const rangeOfSuffix = hasRangeOf && rangeOfVal > 0 ? ` (of ${rangeOfVal} total)` : '';
377
+ const nounSuffix = this.rangeNoun ? ` ${this.rangeNoun}` : '';
378
+
332
379
  if (!rangeEl) return;
333
380
 
334
381
  if (!totalKnown) {
@@ -342,6 +389,45 @@ export class UITableFooter extends UIElement {
342
389
 
343
390
  rangeEl.hidden = false;
344
391
 
392
+ if (rangeOpen) {
393
+ // gh#1877/ADR-0082 Amendment REQ-open — "Showing X–Y" with no
394
+ // "of N", pager stays in has-more mode (next enabled) until a real
395
+ // range-total resolves. Deterministic window math off page/page-size
396
+ // alone, same shape as the finite branch below — no clamp against a
397
+ // total we don't have yet (adiav2 ehr-subscriptions-paging.ts).
398
+ if (rangeEmpty) rangeEmpty.hidden = true;
399
+ if (pageSize <= 0) {
400
+ // No page-size known yet either — nothing to derive a window from.
401
+ if (rangeText) {
402
+ rangeText.hidden = false;
403
+ rangeText.textContent = nounSuffix ? `Loading${nounSuffix}…` : 'Loading…';
404
+ }
405
+ if (pagerWrap) pagerWrap.hidden = true;
406
+ return;
407
+ }
408
+ const page = Math.max(1, Number(this.page) || 1);
409
+ const start = (page - 1) * pageSize + 1;
410
+ const end = page * pageSize;
411
+ if (rangeText) {
412
+ rangeText.hidden = false;
413
+ rangeText.textContent = `Showing ${start}–${end}${nounSuffix}`;
414
+ }
415
+ if (pagerWrap) {
416
+ pagerWrap.hidden = false;
417
+ }
418
+ if (pag) {
419
+ // Keeps `next` enabled indefinitely (never a false "last page") —
420
+ // mirrors table.class.js's own #pageCount open-mode math
421
+ // (gh#1877/ADR-0082 Amendment): total is always one page ahead of
422
+ // wherever the pager currently sits.
423
+ pag.setAttribute('page', String(page));
424
+ pag.setAttribute('total', String(page + 1));
425
+ pag.setAttribute('siblings', String(Math.max(0, Number(this.siblings) || 0)));
426
+ pag.setAttribute('size', this.size || 'md');
427
+ }
428
+ return;
429
+ }
430
+
345
431
  if (rangeTotal === 0) {
346
432
  // REQ-S-002 — confirmed-empty. The empty slot always wins over the
347
433
  // range text whenever range-total is confirmed zero, mutually
@@ -361,7 +447,7 @@ export class UITableFooter extends UIElement {
361
447
  // "Showing 1–0 of N" math an unbounded pager would otherwise need.
362
448
  if (rangeText) {
363
449
  rangeText.hidden = false;
364
- rangeText.textContent = `${rangeTotal} items`;
450
+ rangeText.textContent = `${rangeTotal} items${rangeOfSuffix}`;
365
451
  }
366
452
  if (pagerWrap) pagerWrap.hidden = true;
367
453
  return;
@@ -374,7 +460,7 @@ export class UITableFooter extends UIElement {
374
460
 
375
461
  if (rangeText) {
376
462
  rangeText.hidden = false;
377
- rangeText.textContent = `Showing ${start}–${end} of ${rangeTotal}`;
463
+ rangeText.textContent = `Showing ${start}–${end} of ${rangeTotal}${nounSuffix}${rangeOfSuffix}`;
378
464
  }
379
465
 
380
466
  if (pagerWrap) {
@@ -22,8 +22,12 @@ export class UITableFooter extends UIElement {
22
22
  page: number;
23
23
  /** Rows per page. Default 0 (unknown) is the ORDINARY default state, not an exotic edge case: with page-size absent/0 and range-total present, the range label renders in count-only form ("128 items") and the pager does not render — the derivation math (pages = range-total / page-size) is undefined without a page size. In a client-mode composition (the resolved target has [paginate] > 0) and this attribute is unset, the footer derives page-size from the target's own [paginate] value. */
24
24
  pageSize: number;
25
- /** 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. */
26
- rangeTotal: number;
25
+ /** gh#1877, ADR-0082 Amendmentoptional 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"). */
26
+ rangeNoun: string;
27
+ /** 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"). */
28
+ rangeOf: number;
29
+ /** 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. */
30
+ rangeTotal: number | string;
27
31
  /** Passed through to the composed pagination-ui's own [siblings] (page-number window size on each side of the current page). */
28
32
  siblings: number;
29
33
  /** Passed through to the composed pagination-ui's own [size] (sm | md | lg, the universal size system). */
@@ -55,14 +55,22 @@ props:
55
55
  range label's position when supplied). Named identically to
56
56
  table-toolbar-ui's [range-total] and table-ui's own [range-total]
57
57
  (gh#1754, ADR-0082) and deliberately NOT `total` (pagination-ui's
58
- `total` is total PAGES — the ADR-0063 B5 collision rule). When this
59
- attribute is unset, the footer derives it from the resolved target
60
- via a two-branch ladder (REQ-W-006, amended 0.2.0/ADR-0082):
58
+ `total` is total PAGES — the ADR-0063 B5 collision rule). Explicit
59
+ range-total="?" (gh#1877, ADR-0082 Amendment) is the OPEN/unproven-
60
+ total state cursor/hasMore server paging with no known row count
61
+ yet: the range label renders "Showing X–Y" with no "of N", and the
62
+ pager stays in has-more mode (next enabled) rather than hiding —
63
+ distinct from both "loading" (attribute absent) and "confirmed empty"
64
+ (range-total="0"). Resolves to the normal finite behavior the moment
65
+ a real number replaces the "?". When this attribute is unset, the
66
+ footer derives it from the resolved target via a two-branch ladder
67
+ (REQ-W-006, amended 0.2.0/ADR-0082):
61
68
  (1) the target's own [range-total] attribute, when present — the
62
69
  table-authoritative SERVER mode (gh#1754 Shape 1's second lawful
63
- composition); the identity rule: the footer's range-total derives
64
- from the table's range-total, never the table's filteredCount, and
65
- this branch wins even when the target's [paginate] is also > 0;
70
+ composition; the target's own "?" carries through identically); the
71
+ identity rule: the footer's range-total derives from the table's
72
+ range-total, never the table's filteredCount, and this branch wins
73
+ even when the target's [paginate] is also > 0;
66
74
  (2) otherwise, when the target has [paginate] > 0, CLIENT mode: the
67
75
  target's filtered row count (search + column filters applied, before
68
76
  pagination). Refreshes on the target's `page` / `filter-change` /
@@ -71,9 +79,34 @@ props:
71
79
  on the target without an accompanying page move — emits no event;
72
80
  either re-set this footer's own attributes after such a change, or
73
81
  accept staleness until the target's next event.
82
+ type: [number, string]
83
+ default: 0
84
+ reflect: true
85
+ range-of:
86
+ description: >-
87
+ gh#1877, ADR-0082 Amendment — secondary whole-set total for the
88
+ "filtered-of-whole" shape: a client-side facet narrows [range-total]
89
+ (a filtered count) while range-of names the unfiltered set size, e.g.
90
+ "Showing 1–4 of 4 traversals (6 total)". Appends "(of <range-of>
91
+ total)" onto the normal finite range text; unset (the default)
92
+ changes nothing. Never applies while [range-total] is absent
93
+ (loading) or "?" (open/unproven) — there's no finite range text to
94
+ append onto yet. Named identically on table-toolbar-ui (ADR-0082
95
+ §"same name on both companions").
74
96
  type: number
75
97
  default: 0
76
98
  reflect: true
99
+ range-noun:
100
+ description: >-
101
+ gh#1877, ADR-0082 Amendment — optional per-slice noun appended after
102
+ the range total ("of 80 users") so consumers stop hand-painting the
103
+ noun themselves. Applies in both the normal finite range text and the
104
+ open/unproven-total text ("Showing 51–100 users" with no "of N").
105
+ Unset (the default) changes nothing. Named identically on
106
+ table-toolbar-ui (ADR-0082 §"same name on both companions").
107
+ type: string
108
+ default: ""
109
+ reflect: true
77
110
  siblings:
78
111
  description: Passed through to the composed pagination-ui's own [siblings] (page-number window size on each side of the current page).
79
112
  type: number
@@ -166,6 +199,13 @@ a2ui:
166
199
  range label's own position, without a layout shift versus the normal
167
200
  range text. Only fires on an EXPLICIT range-total="0" — omitting
168
201
  range-total entirely (loading) never shows it.
202
+ - >-
203
+ gh#1877, ADR-0082 Amendment — cursor/hasMore server paging (no
204
+ server-reported row count, only "more rows exist or not") sets
205
+ [range-total="?"] instead of a real number: the range label reads
206
+ "Showing 51–100" with no "of N", and the pager keeps `next` enabled
207
+ until a later response supplies a real [range-total], at which point
208
+ the footer renders the normal finite text automatically.
169
209
  anti_patterns:
170
210
  - wrong: '<table-footer-ui range-start="1" range-end="10" range-total="128"></table-footer-ui>'
171
211
  why: >-
@@ -185,6 +225,17 @@ examples:
185
225
  {"id": "tests", "component": "Table", "raw": true},
186
226
  {"id": "ftr", "component": "TableFooter", "for": "tests", "page": 1, "page-size": 10, "range-total": 128}
187
227
  ]
228
+ - name: table-footer-open-total
229
+ description: >-
230
+ gh#1877, ADR-0082 Amendment — cursor/hasMore server mode with no known
231
+ row count yet: [range-total="?"] renders "Showing 1–50 subscriptions"
232
+ (via [range-noun]) with `next` kept enabled until a real total resolves.
233
+ a2ui: >-
234
+ [
235
+ {"id": "root", "component": "Column", "gap": "0", "children": ["subs", "ftr"]},
236
+ {"id": "subs", "component": "Table", "raw": true},
237
+ {"id": "ftr", "component": "TableFooter", "for": "subs", "page": 1, "page-size": 50, "range-total": "?", "range-noun": "subscriptions"}
238
+ ]
188
239
  - name: table-footer-client-mode
189
240
  description: Client-mode footer bound to a [paginate]d table-ui with its own internal pager suppressed via [no-pager].
190
241
  a2ui: >-
@@ -70,14 +70,27 @@
70
70
  "type": "number",
71
71
  "default": 0
72
72
  },
73
+ "rangeNoun": {
74
+ "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\").",
75
+ "type": "string",
76
+ "default": ""
77
+ },
78
+ "rangeOf": {
79
+ "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\").",
80
+ "type": "number",
81
+ "default": 0
82
+ },
73
83
  "rangeStart": {
74
84
  "description": "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.",
75
85
  "type": "number",
76
86
  "default": 0
77
87
  },
78
88
  "rangeTotal": {
79
- "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.",
80
- "type": "number",
89
+ "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\").",
90
+ "type": [
91
+ "number",
92
+ "string"
93
+ ],
81
94
  "default": 0
82
95
  },
83
96
  "size": {
@@ -107,6 +120,11 @@
107
120
  "type": "string",
108
121
  "default": ""
109
122
  },
123
+ "titleAlways": {
124
+ "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).",
125
+ "type": "boolean",
126
+ "default": false
127
+ },
110
128
  "variant": {
111
129
  "description": "Toolbar visual variant. `default` renders bare on parent surface; `card` adds the same chrome as a card-ui header.",
112
130
  "type": "string",
@@ -179,6 +197,11 @@
179
197
  "description": "The ratified adiav2 list-page toolbar row (ADIA2-9123, gh#1649): scope menu, range summary, app-owned Filter/Columns triggers (their own filter drawer, not the native popovers — [no-filter]/[no-columns]/ [no-sort]/[no-search] drop the native affordances), page-size select, and a primary CTA — [slot=\"actions-leading\"] carries the Filter/Columns triggers so they land BEFORE the page-size select, while [slot=\"actions\"] carries the CTA so it lands after.",
180
198
  "a2ui": "[\n {\"id\": \"root\", \"component\": \"Column\", \"gap\": \"3\", \"children\": [\"bar\", \"card\"]},\n {\"id\": \"bar\", \"component\": \"TableToolbar\", \"for\": \"accounts\", \"no-filter\": true, \"no-sort\": true, \"no-columns\": true, \"no-search\": true, \"range-start\": 1, \"range-end\": 25, \"range-total\": 320, \"page-size\": 25, \"page-size-options\": [10, 25, 50, 100], \"children\": [\"btn-filter\", \"btn-columns\", \"btn-new\"]},\n {\"id\": \"btn-filter\", \"component\": \"Button\", \"text\": \"Filter\", \"variant\": \"ghost\", \"slot\": \"actions-leading\"},\n {\"id\": \"btn-columns\", \"component\": \"Button\", \"text\": \"Columns\", \"variant\": \"ghost\", \"slot\": \"actions-leading\"},\n {\"id\": \"btn-new\", \"component\": \"Button\", \"text\": \"New account\", \"variant\": \"primary\", \"slot\": \"actions\"},\n {\"id\": \"card\", \"component\": \"Card\", \"children\": [\"sec\"]},\n {\"id\": \"sec\", \"component\": \"Section\", \"bleed\": true, \"children\": [\"tbl\"]},\n {\"id\": \"tbl\", \"component\": \"Table\", \"id\": \"accounts\", \"sortable\": true, \"raw\": true}\n]",
181
199
  "name": "adiav2-list-page-toolbar-row"
200
+ },
201
+ {
202
+ "description": "gh#1877, ADR-0082 Amendment — cursor/hasMore server paging with no known row count yet: [range-total=\"?\"] renders \"Showing 1–50 subscriptions\" (via [range-noun]) with no \"of N\", resolving to the normal finite summary once a real total arrives.",
203
+ "a2ui": "[\n {\"id\": \"root\", \"component\": \"Column\", \"gap\": \"3\", \"children\": [\"bar\", \"card\"]},\n {\"id\": \"bar\", \"component\": \"TableToolbar\", \"for\": \"subs\", \"range-start\": 1, \"range-end\": 50, \"range-total\": \"?\", \"range-noun\": \"subscriptions\"},\n {\"id\": \"card\", \"component\": \"Card\", \"children\": [\"sec\"]},\n {\"id\": \"sec\", \"component\": \"Section\", \"bleed\": true, \"children\": [\"tbl\"]},\n {\"id\": \"tbl\", \"component\": \"Table\", \"id\": \"subs\", \"raw\": true}\n]",
204
+ "name": "table-toolbar-open-total"
182
205
  }
183
206
  ],
184
207
  "keywords": [
@@ -216,6 +239,9 @@
216
239
  },
217
240
  "scope": {
218
241
  "description": "Leading region rendered BEFORE the [text]/[count] title cluster — e.g. a scope/view-switcher menu (an org's teams, a saved view). Positioning is CSS by DOM order, mirroring how [slot=\"actions\"] is a real, author-fillable insertion point rather than template-stamped content."
242
+ },
243
+ "summary": {
244
+ "description": "gh#1883 — trailing, text-tier cluster for an aggregate datum that belongs with the table but has no home in the range summary or the actions area (e.g. batch-detail's line-items Subtotal: `<span slot=\"summary\">Subtotal <strong>$1,240.00</strong></span>`). Rendered in the same flex row, after [slot=\"actions-leading\"] and before the page-size select / [slot=\"actions\"] — closer to the trailing edge than [slot=\"scope\"]/[range-*], since it reads as a result/total rather than pagination or view-switching context. Text sized via [--table-toolbar-summary-size] (shared with the \"Showing X–Y of N\" range summary — both are the same text tier). Plain author-supplied markup, same real-insertion-point pattern as every other table-toolbar slot — no forced typography beyond the font-size token. Stays visible at every ADR-0076 compaction stage: it is DATA (an aggregate figure the consumer chose to show), not chrome, so it is never dropped or compacted the way the range summary/ controls are — a consumer needing narrower-container behavior owns that via its own slotted content, not a component-side compaction rule. Empty (the default) has zero footprint on existing consumers."
219
245
  }
220
246
  },
221
247
  "states": [
@@ -172,6 +172,12 @@ export class UITableToolbar extends UIElement {
172
172
  rangeStart: { type: Number, default: 0, reflect: true, attribute: 'range-start' },
173
173
  rangeEnd: { type: Number, default: 0, reflect: true, attribute: 'range-end' },
174
174
  rangeTotal: { type: Number, default: 0, reflect: true, attribute: 'range-total' },
175
+ // gh#1877/ADR-0082 Amendment — same-name mirrors of table-footer-ui's
176
+ // own range-of/range-noun (ADR-0082 §"same name on both companions"):
177
+ // range-of is the secondary whole-set total ("filtered-of-whole"),
178
+ // range-noun the optional per-slice noun. Both presence-gated.
179
+ rangeOf: { type: Number, default: 0, reflect: true, attribute: 'range-of' },
180
+ rangeNoun: { type: String, default: '', reflect: true, attribute: 'range-noun' },
175
181
  pageSize: { type: Number, default: 0, reflect: true, attribute: 'page-size' },
176
182
  noFilter: { type: Boolean, default: false, reflect: true, attribute: 'no-filter' },
177
183
  noSort: { type: Boolean, default: false, reflect: true, attribute: 'no-sort' },
@@ -181,6 +187,13 @@ export class UITableToolbar extends UIElement {
181
187
  // above. Pure, absolute OR: see #updateControlVisibility(), which is the
182
188
  // single place that combines chromeOnly with each granular no-* value.
183
189
  chromeOnly: { type: Boolean, default: false, reflect: true, attribute: 'chrome-only' },
190
+ // gh#1879 — opt-out for the scope-reconciliation title hide
191
+ // (table-toolbar.css's `:scope:not([title-always]):has([data-scope]:not(:empty))
192
+ // [data-title] { display: none; }`). Presence-only: the CSS rule reads
193
+ // the reflected attribute directly, so no JS branch is needed here
194
+ // beyond the standard Boolean reflect declaration (same shape as
195
+ // noFilter/noSort/noColumns/noSearch/chromeOnly above).
196
+ titleAlways: { type: Boolean, default: false, reflect: true, attribute: 'title-always' },
184
197
  placeholder: { type: String, default: 'Search...', reflect: false },
185
198
  variant: { type: String, default: 'default', reflect: true },
186
199
  // ADR-0076 REQ-O-001 — explicit compaction-stage override. Unset (the
@@ -342,7 +355,7 @@ export class UITableToolbar extends UIElement {
342
355
  // value one, so it needs the raw attribute callback, not the signal diff.
343
356
  attributeChangedCallback(name, oldVal, newVal) {
344
357
  super.attributeChangedCallback(name, oldVal, newVal);
345
- if (name === 'range-total') this.#updateSummary();
358
+ if (name === 'range-total' || name === 'range-of' || name === 'range-noun') this.#updateSummary();
346
359
  // ADR-0076 REQ-M-005 — the SECOND of the two triggers `data-stage-
347
360
  // resolved` must react to: a live resize (the ResizeObserver callback
348
361
  // above) AND a `stage` attribute change alone, with no resize needed.
@@ -670,6 +683,26 @@ export class UITableToolbar extends UIElement {
670
683
  }
671
684
  }
672
685
 
686
+ // Summary slot passthrough (gh#1883) — a real insertion point (ADR-0067),
687
+ // same pattern as [slot="scope"]/[slot="actions-leading"]/[slot="actions"]
688
+ // above, for a trailing aggregate/total datum (e.g. line-items Subtotal)
689
+ // that belongs with the table but isn't pagination context. Rendered
690
+ // AFTER [slot="actions-leading"] and BEFORE the page-size select /
691
+ // [slot="actions"] — closer to the trailing edge than the range summary,
692
+ // controls, or search, since it reads as a result rather than view
693
+ // chrome. Deliberately NOT named [data-summary] — that marker already
694
+ // belongs to the "Showing X–Y of N" range-summary cluster stamped above;
695
+ // reusing it here would collide with every existing #updateSummary()/CSS
696
+ // selector keyed on that attribute.
697
+ const summarySlot = document.createElement('div');
698
+ summarySlot.setAttribute('data-summary-slot', '');
699
+ for (const node of [...this.children]) {
700
+ if (node === root) continue;
701
+ if (node.getAttribute?.('slot') === 'summary') {
702
+ summarySlot.appendChild(node);
703
+ }
704
+ }
705
+
673
706
  // Actions slot passthrough — we move any pre-existing [slot="actions"] children here
674
707
  const actionsSlot = document.createElement('div');
675
708
  actionsSlot.setAttribute('data-actions', '');
@@ -686,6 +719,7 @@ export class UITableToolbar extends UIElement {
686
719
  root.appendChild(controls);
687
720
  root.appendChild(search);
688
721
  root.appendChild(actionsLeadingSlot);
722
+ root.appendChild(summarySlot);
689
723
  root.appendChild(pageSize);
690
724
  root.appendChild(actionsSlot);
691
725
 
@@ -849,13 +883,46 @@ export class UITableToolbar extends UIElement {
849
883
  const end = Number(this.rangeEnd) || 0;
850
884
  const total = Number(this.rangeTotal) || 0;
851
885
 
886
+ // gh#1877/ADR-0082 Amendment — same open/unproven-total discipline as
887
+ // table-footer-ui's own #draw(): NOT keyed on the raw attribute
888
+ // string. rangeTotal is a Number-typed reflected prop, so
889
+ // range-total="?" parses via `+"?"` → NaN. gh#1895 — reflect()
890
+ // (core/element.js) leaves a non-finite Number value unwritten instead
891
+ // of stomping the sentinel with the literal string "NaN", so
892
+ // getAttribute('range-total') keeps reading "?" as authored. The
893
+ // coerced PROPERTY still holds NaN stably (Object.is(NaN, NaN) is
894
+ // true, so re-parsing the unchanged attribute never re-triggers a
895
+ // write either), and that's what tells "unknown yet" apart from a
896
+ // confirmed range-total="0" (a finite 0).
897
+ const rangeOpen = !Number.isFinite(Number(this.rangeTotal));
898
+ // gh#1877/ADR-0082 Amendment — additive suffixes onto the normal
899
+ // finite "of N" text; range-of (secondary whole-set total) never
900
+ // applies to the open-total branch (no finite total to append against).
901
+ const hasRangeOf = this.hasAttribute('range-of');
902
+ const rangeOfVal = hasRangeOf ? (Number(this.rangeOf) || 0) : 0;
903
+ const rangeOfSuffix = hasRangeOf && rangeOfVal > 0 ? ` (of ${rangeOfVal} total)` : '';
904
+ const nounSuffix = this.rangeNoun ? ` ${this.rangeNoun}` : '';
905
+
906
+ if (rangeOpen && start > 0 && end > 0) {
907
+ // REQ-open (gh#1877/ADR-0082 Amendment) — "Showing X–Y" with no
908
+ // "of N"; the short-form mirrors it minus the "Showing " prefix,
909
+ // same paired-population discipline as the finite branch below.
910
+ if (text) text.textContent = `Showing ${start}–${end}${nounSuffix}`;
911
+ if (text) text.hidden = false;
912
+ if (compact) compact.textContent = `${start}–${end}`;
913
+ if (compact) compact.hidden = false;
914
+ if (empty) empty.hidden = true;
915
+ summary.hidden = false;
916
+ return;
917
+ }
918
+
852
919
  if (start > 0 && end > 0 && total > 0) {
853
920
  // Normal range text — always wins over the empty slot (AC-014).
854
921
  // Both the full ("Showing X–Y of N") and short-form ("X–Y/N", ADR-0076
855
922
  // REQ-S-003) strings are populated together — which one paints is a
856
923
  // pure CSS decision (icon-only @container rule / [stage] pin), never
857
924
  // a JS branch (REQ-M-002).
858
- if (text) text.textContent = `Showing ${start}–${end} of ${total}`;
925
+ if (text) text.textContent = `Showing ${start}–${end} of ${total}${nounSuffix}${rangeOfSuffix}`;
859
926
  if (text) text.hidden = false;
860
927
  if (compact) compact.textContent = `${start}–${end}/${total}`;
861
928
  if (compact) compact.hidden = false;
@@ -330,8 +330,12 @@
330
330
  prop; this reads what's already in the DOM, same `:has()` idiom the
331
331
  [data-summary]/[data-page-size] reconciliation above already uses).
332
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
- :scope:has([data-scope]:not(:empty)) [data-title] {
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] {
335
339
  display: none;
336
340
  }
337
341
 
@@ -522,6 +526,36 @@
522
526
 
523
527
  [data-actions-leading]:empty { display: none; }
524
528
 
529
+ /* ═══════ Summary slot ═══════ (gh#1883 — trailing aggregate/total datum,
530
+ e.g. line-items Subtotal, rendered AFTER [slot="actions-leading"] and
531
+ BEFORE the page-size select / [slot="actions"]. Text-tier sized via
532
+ --table-toolbar-summary-size — the SAME token the range summary
533
+ ([data-summary-text]/[data-summary-compact]) already uses, since both
534
+ read as the same "small figure next to the toolbar chrome" tier;
535
+ color is NOT forced (unlike the range summary, which paints its own
536
+ generated text) — this slot holds plain author-supplied markup that
537
+ may want its own emphasis, e.g. a bold `<strong>` total.
538
+ Deliberately carries no margin-inline-start:auto of its own: it sits
539
+ between [data-actions-leading] and [data-page-size], both of which
540
+ already push the trailing group to the far edge, so a third auto
541
+ margin here would only ever be a no-op (the first unclaimed-space
542
+ sibling already claims it) — see [data-page-size]'s own comment below.
543
+ No compaction-stage hide/compact rule at any breakpoint or [stage]
544
+ pin, unlike [data-summary] above — this is DATA the consumer chose to
545
+ show, not chrome; a consumer needing narrower-container behavior owns
546
+ that via its own slotted content. */
547
+
548
+ [data-summary-slot] {
549
+ display: inline-flex;
550
+ align-items: center;
551
+ gap: var(--table-toolbar-cluster-gap);
552
+ flex: 0 0 auto;
553
+ font-size: var(--table-toolbar-summary-size);
554
+ line-height: var(--a-font-leading-snug);
555
+ }
556
+
557
+ [data-summary-slot]:empty { display: none; }
558
+
525
559
  /* ═══════ Page-size select ═══════ (gh#1615)
526
560
  Carries the SAME margin-inline-start:auto as [data-search] so the
527
561
  trailing group (controls/search/page-size/actions) still pushes to the
@@ -46,16 +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;
53
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. */
54
58
  size: 'sm' | 'md' | 'lg';
55
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). */
56
60
  stage: '' | 'full' | 'search-tight' | 'icon-only' | 'overflow';
57
61
  /** Title text shown on the left. */
58
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;
59
65
  /** Toolbar visual variant. `default` renders bare on parent surface; `card` adds the same chrome as a card-ui header. */
60
66
  variant: 'default' | 'card';
61
67