@adia-ai/web-components 0.8.45 → 0.8.47

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 (49) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/components/card/card.css +12 -23
  3. package/components/card/card.yaml +11 -0
  4. package/components/chart/chart.a2ui.json +16 -1
  5. package/components/chart/chart.class.js +611 -41
  6. package/components/chart/chart.css +174 -0
  7. package/components/chart/chart.d.ts +5 -1
  8. package/components/chart/chart.yaml +45 -1
  9. package/components/field/field.css +24 -2
  10. package/components/index.js +1 -0
  11. package/components/input/input.css +7 -0
  12. package/components/nav/nav.css +7 -3
  13. package/components/nav-group/nav-group.css +38 -41
  14. package/components/nav-item/nav-item.css +11 -5
  15. package/components/preview/preview.class.js +12 -2
  16. package/components/table/table.a2ui.json +19 -4
  17. package/components/table/table.class.js +163 -12
  18. package/components/table/table.d.ts +9 -3
  19. package/components/table/table.yaml +109 -8
  20. package/components/table-footer/table-footer.a2ui.json +150 -0
  21. package/components/table-footer/table-footer.class.js +391 -0
  22. package/components/table-footer/table-footer.css +64 -0
  23. package/components/table-footer/table-footer.d.ts +39 -0
  24. package/components/table-footer/table-footer.examples.md +46 -0
  25. package/components/table-footer/table-footer.js +17 -0
  26. package/components/table-footer/table-footer.yaml +219 -0
  27. package/components/table-toolbar/table-toolbar.a2ui.json +10 -0
  28. package/components/table-toolbar/table-toolbar.class.js +70 -7
  29. package/components/table-toolbar/table-toolbar.css +100 -3
  30. package/components/table-toolbar/table-toolbar.d.ts +2 -0
  31. package/components/table-toolbar/table-toolbar.examples.md +41 -41
  32. package/components/table-toolbar/table-toolbar.yaml +31 -6
  33. package/core/anchor.js +21 -1
  34. package/custom-elements.json +145 -4
  35. package/dist/host.min.css +1 -1
  36. package/dist/host.sheet.js +1 -1
  37. package/dist/theme-provider.min.js +2 -2
  38. package/dist/web-components.min.css +1 -1
  39. package/dist/web-components.min.js +87 -87
  40. package/dist/web-components.sheet.js +1 -1
  41. package/package.json +1 -1
  42. package/patterns/chart-in-card/chart-in-card.examples.html +36 -9
  43. package/patterns/new-enrollments/new-enrollments.examples.html +140 -0
  44. package/patterns/new-enrollments/new-enrollments.html +54 -0
  45. package/patterns/table-in-card/table-in-card.examples.html +168 -0
  46. package/patterns/table-in-card/table-in-card.examples.js +139 -0
  47. package/patterns/table-in-card/table-in-card.html +86 -0
  48. package/styles/colors/material-static.css +276 -234
  49. package/styles/components.css +1 -0
@@ -0,0 +1,219 @@
1
+ $schema: ../../../../scripts/schemas/component.yaml.schema.json
2
+ name: UITableFooter
3
+ tag: table-footer-ui
4
+ status: beta
5
+ component: TableFooter
6
+ category: agent
7
+ version: 1
8
+ description: >-
9
+ Footer / companion bar for a sibling table-ui (gh#1807, ADR-0080) — a
10
+ "Showing X–Y of N" range label paired with a composed pagination-ui.
11
+ Derivation-model API: set `page` / `page-size` / `range-total` and the
12
+ footer derives total pages and the range window itself, so it never
13
+ drifts out of sync with its own pager. Wired to the target table via a
14
+ [for] id-ref and CustomEvents only (ADR-0079/ADR-0080) — the footer never
15
+ writes a property or calls a method on the bound table. Drop below (or
16
+ inside a card-ui footer alongside) any table-ui to add the standard
17
+ data-grid pager + range summary without re-implementing pagination-ui's
18
+ ellipsis truncation, keyboard handling, or ARIA.
19
+ # Per ADR-0027 — primitives that programmatically create other primitives
20
+ # do NOT auto-import them. Consumer (or demo shell) must explicitly import.
21
+ composes:
22
+ - pagination-ui
23
+ props:
24
+ for:
25
+ description: id-ref of the table-ui to control. Falls back to the first sibling table-ui within the same parent when omitted.
26
+ type: string
27
+ default: ""
28
+ reflect: true
29
+ page:
30
+ description: >-
31
+ Current page, 1-based. Explicit attributes always win over derivation
32
+ (per-attribute, not all-or-nothing) — see page-size/range-total.
33
+ type: number
34
+ default: 1
35
+ reflect: true
36
+ page-size:
37
+ description: >-
38
+ Rows per page. Default 0 (unknown) is the ORDINARY default state, not
39
+ an exotic edge case: with page-size absent/0 and range-total present,
40
+ the range label renders in count-only form ("128 items") and the
41
+ pager does not render — the derivation math (pages = range-total /
42
+ page-size) is undefined without a page size. In a client-mode
43
+ composition (the resolved target has [paginate] > 0) and this
44
+ attribute is unset, the footer derives page-size from the target's
45
+ own [paginate] value.
46
+ type: number
47
+ default: 0
48
+ reflect: true
49
+ range-total:
50
+ description: >-
51
+ Total ROWS across all pages. Absent (the default) means "not yet
52
+ known" (loading) — never "zero"; both the range label and the pager
53
+ stay hidden until this is set. Explicit range-total="0" is the
54
+ confirmed-empty state (pager hidden; the `empty` slot renders in the
55
+ range label's position when supplied). Named identically to
56
+ table-toolbar-ui's [range-total] and table-ui's own [range-total]
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):
61
+ (1) the target's own [range-total] attribute, when present — the
62
+ 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;
66
+ (2) otherwise, when the target has [paginate] > 0, CLIENT mode: the
67
+ target's filtered row count (search + column filters applied, before
68
+ pagination). Refreshes on the target's `page` / `filter-change` /
69
+ `sort` events in either branch. Widened named staleness edge: a
70
+ programmatic `.data` swap on the target — or a `range-total` update
71
+ on the target without an accompanying page move — emits no event;
72
+ either re-set this footer's own attributes after such a change, or
73
+ accept staleness until the target's next event.
74
+ type: number
75
+ default: 0
76
+ reflect: true
77
+ siblings:
78
+ description: Passed through to the composed pagination-ui's own [siblings] (page-number window size on each side of the current page).
79
+ type: number
80
+ default: 1
81
+ reflect: true
82
+ size:
83
+ description: Passed through to the composed pagination-ui's own [size] (sm | md | lg, the universal size system).
84
+ type: string
85
+ default: md
86
+ reflect: true
87
+ events:
88
+ footer-page:
89
+ description: >-
90
+ gh#1807, ADR-0080 (events-only interaction contract, extending
91
+ ADR-0079's sender-prefixed toolbar-* precedent to a second sender) —
92
+ dispatched directly at the resolved [for] target on every pager
93
+ interaction. Detail: { page } (1-based). table-ui listens for this on
94
+ itself, applied only when [paginate] > 0 (clamped to the valid page
95
+ range; a non-numeric/NaN detail.page is a no-op); at [paginate="0"]
96
+ the command is a documented no-op on the table. Additive: any
97
+ consumer, not only table-footer-ui, may dispatch this at a table-ui.
98
+ page-change:
99
+ description: >-
100
+ The footer's own consumer-facing notification — a distinct name from
101
+ the inbound footer-page command (ADR-0079's command≠notification
102
+ separation). Fires on every pager interaction, in both client- and
103
+ server-mode. Detail: { page } (1-based). This is the server-mode
104
+ fetch hook: a consumer listens for it, fetches the new page, then
105
+ sets `table.data` and the footer's own `page` (and `range-total` if
106
+ the server's total moved) — the footer never optimistically advances
107
+ its own state in server-mode.
108
+ slots:
109
+ empty:
110
+ description: >-
111
+ Renders in place of the range label — the same [data-range] region —
112
+ when [range-total] is EXPLICITLY present and equal to "0" (checked
113
+ via hasAttribute, never the computed value alone, so an unset
114
+ range-total during loading never flashes this content). Mutually
115
+ exclusive with a positive [range-total]: the normal "Showing X–Y of N"
116
+ (or count-only) text always wins whenever range-total resolves
117
+ positive. Plain author-supplied markup — table-footer-ui does not
118
+ template or constrain its shape. Naming matches table-toolbar-ui's own
119
+ `empty` slot convention.
120
+ states:
121
+ - name: idle
122
+ description: Default, ready for interaction.
123
+ traits: []
124
+ tokens:
125
+ --table-footer-gap:
126
+ description: Gap between the range label and the pager
127
+ --table-footer-py:
128
+ description: Vertical padding
129
+ --table-footer-px:
130
+ description: Horizontal padding
131
+ --table-footer-summary-fg:
132
+ description: Range-label text color
133
+ --table-footer-summary-size:
134
+ description: Range-label font size
135
+ a2ui:
136
+ rules:
137
+ - >-
138
+ Pair <table-footer-ui> with <table-ui> via [for="<table-id>"] (or
139
+ rely on first-sibling fallback when both are inside the same parent).
140
+ Client-mode composition (a [paginate]-sliced table): set [no-pager]
141
+ on the table-ui so its own internal pagination bar doesn't render
142
+ alongside the footer's composed one — table-footer-ui never
143
+ auto-suppresses it for you.
144
+ - >-
145
+ Server-mode composition (gh#1754 Shape 1 — the server returns the
146
+ page's rows plus the total row count): set [range-total] to the
147
+ server's total, [page] / [page-size] from your own request state, and
148
+ listen for the footer's `page-change` event to fetch the next page —
149
+ the footer does not optimistically advance its own [page] in
150
+ server-mode; write it back after the fetch resolves.
151
+ - >-
152
+ The toolbar's [count] badge and this footer's range label coexist by
153
+ design — they answer different questions (the collection summary vs.
154
+ the page-window position) and neither replaces the other. In a
155
+ composition that includes a table-footer-ui, leave the toolbar's
156
+ [range-start]/[range-end]/[range-total] unset — the range summary's
157
+ home is the footer.
158
+ - >-
159
+ Canonical composition: card-ui header (title + primary action) →
160
+ a plain <section> wrapping a title-less <table-toolbar-ui for> → a
161
+ <section bleed> wrapping the <table-ui id> → a <footer divider>
162
+ wrapping this <table-footer-ui for> — see the table-in-card pattern.
163
+ - >-
164
+ Use <span slot="empty"> (or any markup) inside a table-footer-ui that
165
+ also sets [range-total="0"] to show a "no results" message in the
166
+ range label's own position, without a layout shift versus the normal
167
+ range text. Only fires on an EXPLICIT range-total="0" — omitting
168
+ range-total entirely (loading) never shows it.
169
+ anti_patterns:
170
+ - wrong: '<table-footer-ui range-start="1" range-end="10" range-total="128"></table-footer-ui>'
171
+ why: >-
172
+ table-footer-ui is derivation-only (REQ-F-002) — there is no
173
+ [range-start]/[range-end] attribute pair (that shape belongs to
174
+ table-toolbar-ui, which has no page/page-size knowledge of its own).
175
+ Setting [page] + [page-size] + [range-total] derives the range window
176
+ instead; a permanent three-way drift hazard is exactly what the
177
+ derivation model exists to avoid.
178
+ fix: '<table-footer-ui page="1" page-size="10" range-total="128"></table-footer-ui>'
179
+ examples:
180
+ - name: table-footer-server-mode
181
+ description: Server-mode footer (gh#1754 Shape 1) paired with a for=-bound table-ui.
182
+ a2ui: >-
183
+ [
184
+ {"id": "root", "component": "Column", "gap": "0", "children": ["tests", "ftr"]},
185
+ {"id": "tests", "component": "Table", "raw": true},
186
+ {"id": "ftr", "component": "TableFooter", "for": "tests", "page": 1, "page-size": 10, "range-total": 128}
187
+ ]
188
+ - name: table-footer-client-mode
189
+ description: Client-mode footer bound to a [paginate]d table-ui with its own internal pager suppressed via [no-pager].
190
+ a2ui: >-
191
+ [
192
+ {"id": "root", "component": "Column", "gap": "0", "children": ["tests-c", "ftr"]},
193
+ {"id": "tests-c", "component": "Table", "paginate": 10, "no-pager": true, "raw": true},
194
+ {"id": "ftr", "component": "TableFooter", "for": "tests-c"}
195
+ ]
196
+ keywords:
197
+ - table-footer
198
+ - pagination
199
+ - pager
200
+ - range
201
+ - showing
202
+ - data-grid
203
+ - directory
204
+ - admin
205
+ - backoffice
206
+ - listing
207
+ - records
208
+ synonyms:
209
+ pagination:
210
+ - table-footer
211
+ - pagination
212
+ pager:
213
+ - table-footer
214
+ - pagination
215
+ related:
216
+ - table
217
+ - table-toolbar
218
+ - pagination
219
+ - card
@@ -80,6 +80,16 @@
80
80
  "type": "number",
81
81
  "default": 0
82
82
  },
83
+ "size": {
84
+ "description": "Toolbar scale, via sizing.css's universal [size=\"sm|md|lg\"] ambient tokens. Not table-toolbar-specific chrome — the same attribute that scales any component. Set it once on the toolbar instead of on each slotted scope/actions-leading/actions control individually; anything slotted with no [size] of its own inherits the ambient value. Default (empty) = md.",
85
+ "type": "string",
86
+ "enum": [
87
+ "sm",
88
+ "md",
89
+ "lg"
90
+ ],
91
+ "default": ""
92
+ },
83
93
  "stage": {
84
94
  "description": "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).",
85
95
  "type": "string",
@@ -153,6 +153,20 @@ function collectUniqueValues(col, data) {
153
153
  export class UITableToolbar extends UIElement {
154
154
  static properties = {
155
155
  for: { type: String, default: '', reflect: true },
156
+ // Not consumed by any table-toolbar-specific CSS — sizing.css's
157
+ // universal [size="sm|md|lg"] (unscoped attribute selector, ambient
158
+ // inheriting tokens) already governs the host AND every slotted
159
+ // consumer content (scope select-ui, actions-leading/actions
160
+ // button-ui) that carries no [size] of its own, same as
161
+ // `<div size="sm">…</div>`. Declared (reflect: true) purely so this
162
+ // A2UI-modeled prop survives the docs-transpiler's schema-scoped
163
+ // attribute walk (unmodeled attributes are silently dropped there —
164
+ // verified live: `<table-toolbar-ui size="sm">` authored in
165
+ // table-toolbar.examples.html rendered with no [size] attribute at
166
+ // all on the generated site-a2ui page until this was added) and so
167
+ // `el.size = 'sm'` works from JS, matching card-ui/button-ui's own
168
+ // `size` property shape.
169
+ size: { type: String, default: '', reflect: true },
156
170
  text: { type: String, default: '', reflect: false },
157
171
  count: { type: String, default: '', reflect: false },
158
172
  rangeStart: { type: Number, default: 0, reflect: true, attribute: 'range-start' },
@@ -233,6 +247,27 @@ export class UITableToolbar extends UIElement {
233
247
 
234
248
  connected() {
235
249
  this.setAttribute('role', 'toolbar');
250
+ // `size` (declared above, reflect: true) defaults to '' — the base
251
+ // property system's own reflect pass (core/element.js) deliberately
252
+ // does NOT auto-stamp a non-empty string/number default onto the DOM
253
+ // (gh#961: scoped to Boolean-true defaults only, to avoid a
254
+ // framework-wide `attr=""`-stamping cascade), so a bare
255
+ // `<table-toolbar-ui>` never actually gets a `[size]` attribute from
256
+ // the default alone. Stamped explicitly, once, HERE — sizing.css's
257
+ // `[size="sm"]` is a plain unscoped attribute selector that needs the
258
+ // literal attribute present to set the ambient `--a-size`/`--a-ui-size`
259
+ // tokens at all. Every native control this component stamps below
260
+ // (badge, search, page-size select, granular buttons, overflow
261
+ // trigger) is a light-DOM descendant with no `[size]` of its own, so
262
+ // it inherits those tokens the same way any plain
263
+ // `<div size="sm">…</div>` cascades to its children — no per-child
264
+ // `size=` stamping needed (tried first, reverted: redundant with what
265
+ // inheritance already does for every element still in this light-DOM
266
+ // subtree, and actively wrong once a consumer sets an explicit
267
+ // `size="lg"` after connect — the per-child copies would go stale
268
+ // since nothing re-stamps them on a later property change, while
269
+ // inheritance updates live).
270
+ if (!this.hasAttribute('size')) this.setAttribute('size', this.size || 'sm');
236
271
  this.#warnUnknownOptOutAttrs();
237
272
  // gh#1615 — hydrate `page-size-options="[10,25,50]"` once at connect,
238
273
  // same pattern as table-ui's `data`/`columns` JSON-array attributes
@@ -479,6 +514,26 @@ export class UITableToolbar extends UIElement {
479
514
 
480
515
  // ── DOM stamp ────────────────────────────────────────────────────────────
481
516
 
517
+ // Used ONLY for the two stamped elements that end up OUTSIDE
518
+ // table-toolbar-ui's own light DOM — the popover-panel action buttons
519
+ // (#mkPopoverAction) and the filter-input (#fillFilterPanel), both
520
+ // appended to `document.body` for the Popover API's top-layer promotion
521
+ // (#togglePopover) — so ambient CSS inheritance from the host's own
522
+ // `[size]` attribute (stamped once in connected(), above) can never
523
+ // reach them. Every OTHER stamped control (badge, search, page-size
524
+ // select, granular Filter/Sort/Columns buttons, the overflow trigger)
525
+ // stays in the host's own light DOM and inherits that ambient value
526
+ // directly — no per-element `size=` copy needed, and no risk of one
527
+ // going stale after a later `.size =` change the way an explicit copy
528
+ // would (tried first, reverted: every in-tree control hardcoded
529
+ // `size="sm"` unconditionally, which not only ignored a consumer's
530
+ // `size="lg"` but also fought its OWN copy going stale on any later
531
+ // property change nothing re-stamped). Falls back to "sm" (the
532
+ // pre-existing hardcoded default) when `size` is unset.
533
+ #stampSize() {
534
+ return this.size || 'sm';
535
+ }
536
+
482
537
  #stamp() {
483
538
  if (this.querySelector(':scope > [data-toolbar]')) return;
484
539
 
@@ -509,7 +564,8 @@ export class UITableToolbar extends UIElement {
509
564
 
510
565
  const badge = document.createElement('badge-ui');
511
566
  badge.setAttribute('data-toolbar-count-badge', '');
512
- badge.setAttribute('size', 'sm');
567
+ // No own [size] — inherits the host's ambient --a-size (stamped in
568
+ // connected(), above).
513
569
  badge.setAttribute('tone', 'muted');
514
570
  badge.hidden = true;
515
571
  title.appendChild(badge);
@@ -577,7 +633,7 @@ export class UITableToolbar extends UIElement {
577
633
  // name renders as literal text before the icon registry resolves.
578
634
  const search = document.createElement('search-ui');
579
635
  search.setAttribute('data-search', '');
580
- search.setAttribute('size', 'sm'); // match toolbar buttons + badge + filter chips
636
+ // No own [size] inherits the host's ambient --a-size, same as badge.
581
637
  search.setAttribute('placeholder', this.placeholder);
582
638
  search.setAttribute('debounce', String(SEARCH_DEBOUNCE));
583
639
  search.addEventListener('search', this.#onSearch);
@@ -590,7 +646,7 @@ export class UITableToolbar extends UIElement {
590
646
  // existing default behavior for consumers that never set pageSizeOptions.
591
647
  const pageSize = document.createElement('select-ui');
592
648
  pageSize.setAttribute('data-page-size', '');
593
- pageSize.setAttribute('size', 'sm');
649
+ // No own [size] — inherits the host's ambient --a-size, same as badge.
594
650
  pageSize.setAttribute('aria-label', 'Rows per page');
595
651
  pageSize.hidden = true;
596
652
  pageSize.addEventListener('change', this.#onPageSizeChange);
@@ -645,7 +701,12 @@ export class UITableToolbar extends UIElement {
645
701
  btn.setAttribute('data-popover-action', '');
646
702
  btn.setAttribute('text', label);
647
703
  btn.setAttribute('variant', 'ghost');
648
- btn.setAttribute('size', 'sm');
704
+ // Explicit [size] IS still needed here, unlike the in-tree stamped
705
+ // controls above — #togglePopover appends this button's panel straight
706
+ // to `document.body` (Popover API top-layer promotion), not nested
707
+ // inside table-toolbar-ui in the DOM, so ambient CSS inheritance from
708
+ // the host's own [size] never reaches it.
709
+ btn.setAttribute('size', this.#stampSize());
649
710
  btn.setAttribute('stretch', '');
650
711
  btn.addEventListener('click', onClick);
651
712
  return btn;
@@ -657,7 +718,7 @@ export class UITableToolbar extends UIElement {
657
718
  btn.setAttribute('icon', icon);
658
719
  btn.setAttribute('text', label);
659
720
  btn.setAttribute('variant', 'outline');
660
- btn.setAttribute('size', 'sm');
721
+ // No own [size] — inherits the host's ambient --a-size, same as badge.
661
722
  btn.setAttribute('aria-haspopup', 'menu');
662
723
  // ADR-0076 REQ-X-002 — unconditional (harmless at full-text stages,
663
724
  // becomes the hover-tooltip affordance at the icon-only stage once
@@ -689,7 +750,7 @@ export class UITableToolbar extends UIElement {
689
750
  trigger.setAttribute('slot', 'trigger');
690
751
  trigger.setAttribute('icon', 'dots-three-vertical');
691
752
  trigger.setAttribute('variant', 'outline');
692
- trigger.setAttribute('size', 'sm');
753
+ // No own [size] — inherits the host's ambient --a-size, same as badge.
693
754
  // REQ-X-003 — a genuinely new interactive element (not a repurposed
694
755
  // existing button), so it needs its own static accessible name rather
695
756
  // than an inherited one. No [text] attribute — always icon-only,
@@ -1085,7 +1146,9 @@ export class UITableToolbar extends UIElement {
1085
1146
  } else {
1086
1147
  const input = document.createElement('input-ui');
1087
1148
  input.setAttribute('type', 'text');
1088
- input.setAttribute('size', 'sm');
1149
+ // Explicit [size] needed — this panel is appended straight to
1150
+ // document.body (see #mkPopoverAction's comment above).
1151
+ input.setAttribute('size', this.#stampSize());
1089
1152
  input.setAttribute('data-filter-input', '');
1090
1153
  input.setAttribute('placeholder', '—');
1091
1154
  if (current?.op === 'contains') input.value = current.value ?? '';
@@ -284,6 +284,21 @@
284
284
  flex: 0 0 auto;
285
285
  }
286
286
 
287
+ /* See [data-page-size]'s own comment below — same --select-min-width
288
+ floor, same fix, but targeted at the select-ui DESCENDANT directly
289
+ rather than set as an inheriting custom property on the [data-scope]
290
+ wrapper: select-ui's own `:where(:scope) { --select-min-width: 20ch }`
291
+ (select.css) directly assigns the property on the select-ui element
292
+ itself — a direct assignment always wins over an inherited value
293
+ regardless of the assigning rule's (here, zero) specificity, so a
294
+ value merely inherited from an ancestor wrapper never actually
295
+ reaches it. Matching the select-ui element directly puts this
296
+ override in the same competition as that rule, where normal
297
+ specificity comparison (0,2,0) vs (0,0,0) applies and this wins. */
298
+ [data-scope] select-ui {
299
+ --select-min-width: 0;
300
+ }
301
+
287
302
  [data-scope]:empty { display: none; }
288
303
 
289
304
  /* ═══════ Title cluster ═══════ */
@@ -305,6 +320,21 @@
305
320
 
306
321
  [data-toolbar-count-badge][hidden] { display: none; }
307
322
 
323
+ /* Scope reconciliation — a filled [slot="scope"] already names the
324
+ entity being listed (a select/menu whose own displayed value IS the
325
+ label, e.g. "All Accounts (3)"), so the separate [text]/[count] title
326
+ cluster next to it says the same thing twice ("All accounts" +
327
+ "Accounts 3"). CSS-only, not #updateTitle() (JS never even sees
328
+ [text]/[count] are redundant here — [count] still falls back to the
329
+ bound table's row length regardless, so a JS hide would need a new
330
+ prop; this reads what's already in the DOM, same `:has()` idiom the
331
+ [data-summary]/[data-page-size] reconciliation above already uses).
332
+ A consumer wanting BOTH visible can still override with normal CSS
333
+ specificity — this isn't a hard mode switch, just the default. */
334
+ :scope:has([data-scope]:not(:empty)) [data-title] {
335
+ display: none;
336
+ }
337
+
308
338
  /* ═══════ Range summary ═══════ (gh#1615 — "Showing X–Y of N", independent of [data-title]) */
309
339
 
310
340
  [data-summary] {
@@ -371,7 +401,17 @@
371
401
  --table-toolbar-bp-overflow's own default (same duplication +
372
402
  source-assertion discipline as the search-tight/icon-only boundaries
373
403
  above). */
374
- [data-toolbar-overflow] { display: none; }
404
+ /* BUG (found live, verified in a real browser — a fresh table-toolbar-ui
405
+ at ANY stage, including "full", rendered Filter/Sort/Columns AND the
406
+ "More" trigger simultaneously): menu-ui's own base rule (menu.css,
407
+ `:scope { display: flex; }`) is the same (0,1,0) specificity as a bare
408
+ `[data-toolbar-overflow]` selector — a cascade TIE this file's source
409
+ order loses (menu.css wins), so this default-hide rule never actually
410
+ applied. The `:scope … [data-toolbar-overflow][hidden]` override below
411
+ already carries the `:scope` prefix for exactly this reason (its own
412
+ comment: "must match or exceed the (0,3,0) specificity … to win the
413
+ tie") — this rule needed the same prefix and never got it. */
414
+ :scope [data-toolbar-overflow] { display: none; }
375
415
 
376
416
  @container table-toolbar (max-width: 26rem) {
377
417
  :scope:is(:not([stage]), [stage=""]) [data-toolbar-btn] {
@@ -405,19 +445,62 @@
405
445
  conflicting author rule. */
406
446
  :scope [data-toolbar-overflow][hidden] { display: none; }
407
447
 
448
+ /* menu-ui's own popover carries a hardcoded `min-width: 10rem` (menu.css
449
+ — a LITERAL, not even the `--menu-popover-min-width` token declared
450
+ next to it: the popover is promoted to the top layer via the Popover
451
+ API but stays a real light-DOM child of <menu-ui>, per menu.css's own
452
+ comment "the top-layer element can't inherit the scoped token", so
453
+ that rule hardcodes the value instead of reading the custom property).
454
+ Same class of bug as --input-min-width/--select-min-width above: sized
455
+ for a generic dropdown menu of arbitrary action items, not this
456
+ specific "More" trigger's three short labels (Filter/Sort/Columns),
457
+ each already narrower than 10rem on its own. Scoped to JUST this
458
+ menu-ui instance (`[data-toolbar-overflow]`) — the [data-menu-popover]
459
+ it targets is menu-ui's own internal implementation selector, not
460
+ table-toolbar's; overriding the LITERAL selector directly here rather
461
+ than touching menu.css keeps every other menu-ui consumer's own
462
+ 10rem floor (a reasonable default for a menu of arbitrary items)
463
+ untouched. */
464
+ :scope [data-toolbar-overflow] [data-menu-popover] {
465
+ min-width: 0;
466
+ }
467
+
408
468
  /* ═══════ Search ═══════ */
409
469
 
410
470
  [data-search] {
411
471
  flex: 1 1 var(--table-toolbar-search-min);
412
472
  /* gh#1076 — never collapse to 0: floor at a usable width and let the
413
- flex-wrap above move it to its own row when the container is tight. */
414
- min-width: min(10rem, 100%);
473
+ flex-wrap above move it to its own row when the container is tight.
474
+ BUG (found live, 2026-08-22): this used to be a hardcoded
475
+ `min(10rem, 100%)`, independent of `--table-toolbar-search-min` —
476
+ at the search-tight/icon-only/overflow stages that token gets
477
+ reassigned to `--table-toolbar-search-tight-min` (9rem default),
478
+ but the hardcoded 10rem floor here still won, so the tight tier's
479
+ own declared 9rem target could never actually be reached. Deriving
480
+ the floor from the SAME token as the flex-basis keeps them in sync
481
+ at every stage instead of drifting against each other. */
482
+ min-width: min(var(--table-toolbar-search-min), 100%);
415
483
  max-width: var(--table-toolbar-search-max);
416
484
  margin-inline-start: auto;
417
485
  }
418
486
 
419
487
  [data-search][hidden] { display: none; }
420
488
 
489
+ /* input-ui's own --input-min-width default (20ch — input.css) is sized
490
+ for a BARE, standalone field where the component has no other context
491
+ to size against. Inside [data-search] that's a second, independent
492
+ floor competing with the one above — at the tight/icon-only/overflow
493
+ stages input-ui's 20ch (~164px) is WIDER than
494
+ --table-toolbar-search-tight-min (9rem/144px), so it silently won
495
+ regardless of what [data-search]'s own min-width — even after the fix
496
+ above — asked for. table-toolbar-ui owns the search field's sizing
497
+ budget end-to-end (it's the one deciding how much room is left after
498
+ the title/controls/page-size clusters); input-ui's independent
499
+ opinion has nothing to add here and only fights the outer token. */
500
+ [data-search] input-ui {
501
+ --input-min-width: 0;
502
+ }
503
+
421
504
  /* ═══════ Actions-leading slot ═══════ (gh#1649 — app-owned triggers, e.g.
422
505
  custom Filter/Columns buttons, that land BEFORE the page-size select)
423
506
  Carries the SAME margin-inline-start:auto as [data-search]/[data-page-size]
@@ -449,9 +532,23 @@
449
532
  is safe — only the first one with unclaimed free space to its left
450
533
  actually receives it. */
451
534
 
535
+ /* select-ui's own --select-min-width default (20ch — select.css,
536
+ gh#1633, the SAME UA-baseline floor input.css's --input-min-width
537
+ already documents for input-ui) is sized for a bare, standalone
538
+ select. Found live (root-caused alongside the earlier search-field
539
+ min-width fix, same shape): with no override here, [data-page-size]
540
+ and [data-scope] select-ui both floor at ~151px regardless of how
541
+ short their actual content is ("25 / page", "All Accounts (3)") —
542
+ visible dead space inside the trigger. The filter/sort/columns
543
+ popover's OWN per-column selects already avoid this (they share the
544
+ `[data-filter-input]` marker with the text-input case, and
545
+ `[data-toolbar-popover] [data-filter-input] { min-width: 0; }`
546
+ already resets it there) — this is the same fix for the two
547
+ main-row selects that marker doesn't reach. */
452
548
  [data-page-size] {
453
549
  flex: 0 0 auto;
454
550
  margin-inline-start: auto;
551
+ --select-min-width: 0;
455
552
  }
456
553
 
457
554
  [data-page-size][hidden] { display: none; }
@@ -50,6 +50,8 @@ export class UITableToolbar extends UIElement {
50
50
  rangeStart: number;
51
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
52
  rangeTotal: number;
53
+ /** 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
+ size: 'sm' | 'md' | 'lg';
53
55
  /** Explicit compaction-stage override (ADR-0076). Unset (the default, empty string) means auto-snap: the toolbar's own inline-size container query picks the nearest stage from its live width against the three --table-toolbar-bp-* breakpoints, snapping discretely, never interpolating. Setting stage explicitly pins that stage's rendering regardless of the container's actual width — overriding the container query, mirroring chart-ui's ratio override shape. Primary use: visual-eval fixtures that need a deterministic stage without resizing a real container, and a consumer embedding the toolbar inside a known-narrow panel who wants to skip auto-detection entirely. All four stages' CSS is now live (ADR-0076): search-tight steps the search field down (step 2); icon-only drops control labels and compacts/hides the range summary (steps 3+4); overflow collapses Filter/Sort/Columns into one "More" trigger routed through the same menu-ui/menu-item-ui pattern used elsewhere in the corpus (step 5). The resolved stage — the pin if set, otherwise the live width classification — also reflects onto the host as `data-stage-resolved` (step 7, informational only, mirrors chart-ui's `data-ratio-resolved` convention — never drives table-toolbar's own rendering, which stays 100% CSS-driven). */
54
56
  stage: '' | 'full' | 'search-tight' | 'icon-only' | 'overflow';
55
57
  /** Title text shown on the left. */
@@ -14,45 +14,45 @@
14
14
  ## Rendered demos
15
15
 
16
16
  ```html
17
- <col-ui gap="0">
18
- <table-toolbar-ui for="demo-table" text="All employees" count="32" placeholder="Search employees…"></table-toolbar-ui>
19
- <card-ui>
20
- <section bleed>
21
- <table-ui id="demo-table" sortable raw>
22
- <table>
23
- <thead>
24
- <tr>
25
- <th>Name</th>
26
- <th>Role</th>
27
- <th>Status</th>
28
- </tr>
29
- </thead>
30
- <tbody>
31
- <tr>
32
- <td>Maya Chen</td>
33
- <td>Designer</td>
34
- <td>
35
- <tag-ui variant="success">Active</tag-ui>
36
- </td>
37
- </tr>
38
- <tr>
39
- <td>Alex Park</td>
40
- <td>Engineer</td>
41
- <td>
42
- <tag-ui variant="success">Active</tag-ui>
43
- </td>
44
- </tr>
45
- <tr>
46
- <td>Jordan Lee</td>
47
- <td>Product</td>
48
- <td>
49
- <tag-ui variant="warning">Away</tag-ui>
50
- </td>
51
- </tr>
52
- </tbody>
53
- </table>
54
- </table-ui>
55
- </section>
56
- </card-ui>
57
- </col-ui>
17
+ <card-ui>
18
+ <header>
19
+ <table-toolbar-ui for="demo-table" text="All employees" count="32" placeholder="Search employees…"></table-toolbar-ui>
20
+ </header>
21
+ <section bleed>
22
+ <table-ui id="demo-table" sortable raw>
23
+ <table>
24
+ <thead>
25
+ <tr>
26
+ <th>Name</th>
27
+ <th>Role</th>
28
+ <th>Status</th>
29
+ </tr>
30
+ </thead>
31
+ <tbody>
32
+ <tr>
33
+ <td>Maya Chen</td>
34
+ <td>Designer</td>
35
+ <td>
36
+ <tag-ui variant="success">Active</tag-ui>
37
+ </td>
38
+ </tr>
39
+ <tr>
40
+ <td>Alex Park</td>
41
+ <td>Engineer</td>
42
+ <td>
43
+ <tag-ui variant="success">Active</tag-ui>
44
+ </td>
45
+ </tr>
46
+ <tr>
47
+ <td>Jordan Lee</td>
48
+ <td>Product</td>
49
+ <td>
50
+ <tag-ui variant="warning">Away</tag-ui>
51
+ </td>
52
+ </tr>
53
+ </tbody>
54
+ </table>
55
+ </table-ui>
56
+ </section>
57
+ </card-ui>
58
58
  ```