@adia-ai/web-components 0.8.47 → 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 (34) hide show
  1. package/CHANGELOG.md +34 -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/richtext/richtext.css +2 -2
  8. package/components/table/table.a2ui.json +5 -2
  9. package/components/table/table.class.js +36 -1
  10. package/components/table/table.d.ts +2 -2
  11. package/components/table/table.yaml +17 -1
  12. package/components/table-footer/table-footer.a2ui.json +20 -2
  13. package/components/table-footer/table-footer.class.js +94 -8
  14. package/components/table-footer/table-footer.d.ts +6 -2
  15. package/components/table-footer/table-footer.yaml +57 -6
  16. package/components/table-toolbar/table-toolbar.a2ui.json +25 -2
  17. package/components/table-toolbar/table-toolbar.class.js +47 -2
  18. package/components/table-toolbar/table-toolbar.css +6 -2
  19. package/components/table-toolbar/table-toolbar.d.ts +8 -2
  20. package/components/table-toolbar/table-toolbar.examples.md +6 -4
  21. package/components/table-toolbar/table-toolbar.yaml +83 -9
  22. package/custom-elements.json +43 -5
  23. package/dist/host.min.css +1 -1
  24. package/dist/host.sheet.js +1 -1
  25. package/dist/theme-provider.min.js +8 -8
  26. package/dist/themes.min.css +1 -1
  27. package/dist/themes.sheet.js +1 -1
  28. package/dist/web-components.min.css +1 -1
  29. package/dist/web-components.min.js +49 -49
  30. package/dist/web-components.sheet.js +1 -1
  31. package/package.json +1 -1
  32. package/styles/colors/material-static.css +34 -12
  33. package/styles/themes.css +2676 -0
  34. package/styles/type/elements.css +1 -0
@@ -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": [
@@ -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.
@@ -849,13 +862,45 @@ export class UITableToolbar extends UIElement {
849
862
  const end = Number(this.rangeEnd) || 0;
850
863
  const total = Number(this.rangeTotal) || 0;
851
864
 
865
+ // gh#1877/ADR-0082 Amendment — same open/unproven-total discipline as
866
+ // table-footer-ui's own #draw(): NOT keyed on the raw attribute
867
+ // string. rangeTotal is a Number-typed reflected prop, so
868
+ // range-total="?" parses via `+"?"` → NaN and the property setter's
869
+ // own reflect() immediately rewrites the DOM attribute to the literal
870
+ // string "NaN" (core/element.js) — getAttribute('range-total') would
871
+ // read "NaN", never "?". The coerced PROPERTY still holds NaN stably
872
+ // (Object.is(NaN, NaN) is true, so the reflect's re-parse never
873
+ // re-writes it), and that's what tells "unknown yet" apart from a
874
+ // confirmed range-total="0" (a finite 0).
875
+ const rangeOpen = !Number.isFinite(Number(this.rangeTotal));
876
+ // gh#1877/ADR-0082 Amendment — additive suffixes onto the normal
877
+ // finite "of N" text; range-of (secondary whole-set total) never
878
+ // applies to the open-total branch (no finite total to append against).
879
+ const hasRangeOf = this.hasAttribute('range-of');
880
+ const rangeOfVal = hasRangeOf ? (Number(this.rangeOf) || 0) : 0;
881
+ const rangeOfSuffix = hasRangeOf && rangeOfVal > 0 ? ` (of ${rangeOfVal} total)` : '';
882
+ const nounSuffix = this.rangeNoun ? ` ${this.rangeNoun}` : '';
883
+
884
+ if (rangeOpen && start > 0 && end > 0) {
885
+ // REQ-open (gh#1877/ADR-0082 Amendment) — "Showing X–Y" with no
886
+ // "of N"; the short-form mirrors it minus the "Showing " prefix,
887
+ // same paired-population discipline as the finite branch below.
888
+ if (text) text.textContent = `Showing ${start}–${end}${nounSuffix}`;
889
+ if (text) text.hidden = false;
890
+ if (compact) compact.textContent = `${start}–${end}`;
891
+ if (compact) compact.hidden = false;
892
+ if (empty) empty.hidden = true;
893
+ summary.hidden = false;
894
+ return;
895
+ }
896
+
852
897
  if (start > 0 && end > 0 && total > 0) {
853
898
  // Normal range text — always wins over the empty slot (AC-014).
854
899
  // Both the full ("Showing X–Y of N") and short-form ("X–Y/N", ADR-0076
855
900
  // REQ-S-003) strings are populated together — which one paints is a
856
901
  // pure CSS decision (icon-only @container rule / [stage] pin), never
857
902
  // a JS branch (REQ-M-002).
858
- if (text) text.textContent = `Showing ${start}–${end} of ${total}`;
903
+ if (text) text.textContent = `Showing ${start}–${end} of ${total}${nounSuffix}${rangeOfSuffix}`;
859
904
  if (text) text.hidden = false;
860
905
  if (compact) compact.textContent = `${start}–${end}/${total}`;
861
906
  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
 
@@ -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
 
@@ -3,11 +3,13 @@
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
 
@@ -88,11 +88,45 @@ props:
88
88
  in "Showing 1–25 of 320"). Distinct from pagination-ui's `total`
89
89
  (total PAGES) — this is total ROWS; deliberately not spelled `total`
90
90
  to avoid that cross-sibling name collision (ADR-0063 B5). See
91
- rangeStart.
92
- 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]
93
99
  default: 0
94
100
  reflect: true
95
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
96
130
  pageSize:
97
131
  description: >-
98
132
  Current rows-per-page value, shown as the selected option in the
@@ -190,6 +224,19 @@ props:
190
224
  default: false
191
225
  reflect: true
192
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
193
240
  placeholder:
194
241
  description: Placeholder text for the search input.
195
242
  type: string
@@ -343,13 +390,15 @@ a2ui:
343
390
  update the table.
344
391
  - >-
345
392
  Use [slot="scope"] for a leading scope/view switcher (an org's
346
- teams, a saved view) rendered before [text]/[count]. Use
347
- [range-start] / [range-end] / [range-total] together for a
348
- "Showing X–Y of N" summaryit renders independently of, and
349
- alongside, [text]/[count], never in place of them. Use [page-size]
350
- + [page-size-options] for a rows-per-page select; it applies
351
- directly to the bound table's [paginate] prop the same way
352
- 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.
353
402
  - >-
354
403
  Use [slot="actions-leading"] (gh#1649) for app-owned trigger
355
404
  buttons that must land BEFORE the page-size select — e.g. custom
@@ -373,6 +422,17 @@ a2ui:
373
422
  normal "Showing X-Y of N" text (ADR-0076, REQ-E). Only fires on an
374
423
  EXPLICIT range-total="0" — omitting range-* entirely (loading) never
375
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").
376
436
  - >-
377
437
  A plain <button-ui> slotted into [slot="actions-leading"] (e.g. the
378
438
  app-owned Filter/Columns triggers above) already collapses to
@@ -420,6 +480,20 @@ examples:
420
480
  {"id": "sec", "component": "Section", "bleed": true, "children": ["tbl"]},
421
481
  {"id": "tbl", "component": "Table", "id": "accounts", "sortable": true, "raw": true}
422
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
+ ]
423
497
  keywords:
424
498
  - table-toolbar
425
499
  - data-grid
@@ -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",
@@ -10083,11 +10098,26 @@
10083
10098
  },
10084
10099
  {
10085
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",
10086
10109
  "type": {
10087
10110
  "text": "number"
10088
10111
  },
10089
10112
  "default": "0",
10090
- "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\")."
10091
10121
  },
10092
10122
  {
10093
10123
  "name": "page-size",
@@ -10152,6 +10182,14 @@
10152
10182
  "default": "false",
10153
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."
10154
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
+ },
10155
10193
  {
10156
10194
  "name": "placeholder",
10157
10195
  "type": {
@@ -10337,10 +10375,10 @@
10337
10375
  {
10338
10376
  "name": "range-total",
10339
10377
  "type": {
10340
- "text": "number"
10378
+ "text": "[number, string]"
10341
10379
  },
10342
10380
  "default": "0",
10343
- "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."
10344
10382
  },
10345
10383
  {
10346
10384
  "name": "filteredCount",