@adia-ai/web-components 0.8.42 → 0.8.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (186) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/MIGRATION.md +130 -80
  3. package/USAGE.md +1 -1
  4. package/color/index.js +1 -1
  5. package/components/action-list/action-item.a2ui.json +3 -8
  6. package/components/action-list/action-item.yaml +10 -16
  7. package/components/action-list/action-list.class.js +5 -61
  8. package/components/action-list/action-list.d.ts +1 -3
  9. package/components/avatar/avatar.class.js +12 -1
  10. package/components/button/button.d.ts +4 -0
  11. package/components/button/button.yaml +1 -1
  12. package/components/calendar-picker/calendar-picker.a2ui.json +1 -1
  13. package/components/calendar-picker/calendar-picker.class.js +2 -3
  14. package/components/calendar-picker/calendar-picker.css +16 -18
  15. package/components/calendar-picker/calendar-picker.yaml +2 -2
  16. package/components/card/card.css +3 -3
  17. package/components/chart/chart.a2ui.json +29 -18
  18. package/components/chart/chart.class.js +1308 -266
  19. package/components/chart/chart.css +151 -1
  20. package/components/chart/chart.d.ts +9 -9
  21. package/components/chart/chart.yaml +63 -31
  22. package/components/chart-legend/chart-legend.a2ui.json +20 -14
  23. package/components/chart-legend/chart-legend.class.js +79 -58
  24. package/components/chart-legend/chart-legend.css +65 -1
  25. package/components/chart-legend/chart-legend.d.ts +5 -5
  26. package/components/chart-legend/chart-legend.examples.md +4 -4
  27. package/components/chart-legend/chart-legend.yaml +50 -20
  28. package/components/check/check.css +9 -0
  29. package/components/color-area/color-area.a2ui.json +1 -1
  30. package/components/color-area/color-area.class.js +2 -2
  31. package/components/color-area/color-area.css +4 -7
  32. package/components/color-area/color-area.d.ts +1 -1
  33. package/components/color-area/color-area.yaml +3 -3
  34. package/components/color-input/color-input.a2ui.json +1 -1
  35. package/components/color-input/color-input.class.js +1 -1
  36. package/components/color-input/color-input.yaml +2 -2
  37. package/components/combobox/combobox.a2ui.json +3 -0
  38. package/components/combobox/combobox.class.js +9 -8
  39. package/components/combobox/combobox.css +25 -4
  40. package/components/combobox/combobox.yaml +6 -0
  41. package/components/command/command.css +17 -1
  42. package/components/context-menu/context-menu.d.ts +2 -0
  43. package/components/drilldown/drilldown.a2ui.json +3 -0
  44. package/components/drilldown/drilldown.css +8 -1
  45. package/components/drilldown/drilldown.yaml +2 -0
  46. package/components/field/field.class.js +5 -5
  47. package/components/heatmap/heatmap.a2ui.json +5 -0
  48. package/components/heatmap/heatmap.d.ts +2 -0
  49. package/components/heatmap/heatmap.yaml +8 -0
  50. package/components/index.js +0 -4
  51. package/components/input/input.a2ui.json +4 -4
  52. package/components/input/input.class.js +20 -7
  53. package/components/input/input.css +6 -0
  54. package/components/input/input.yaml +15 -11
  55. package/components/integration-card/integration-card.d.ts +2 -0
  56. package/components/menu/menu-item.a2ui.json +3 -8
  57. package/components/menu/menu-item.yaml +13 -21
  58. package/components/menu/menu.class.js +5 -27
  59. package/components/menu/menu.d.ts +1 -3
  60. package/components/noodles/noodles.a2ui.json +0 -15
  61. package/components/noodles/noodles.yaml +0 -12
  62. package/components/number-format/number-format.class.js +10 -0
  63. package/components/pagination/pagination.class.js +88 -4
  64. package/components/pane/pane.a2ui.json +2 -12
  65. package/components/pane/pane.class.js +5 -30
  66. package/components/pane/pane.d.ts +3 -5
  67. package/components/pane/pane.yaml +7 -19
  68. package/components/popover/popover.d.ts +2 -0
  69. package/components/progress/progress.a2ui.json +1 -1
  70. package/components/progress/progress.class.js +13 -8
  71. package/components/progress/progress.d.ts +1 -1
  72. package/components/progress/progress.yaml +2 -2
  73. package/components/radio/radio.css +7 -0
  74. package/components/range/range.class.js +9 -1
  75. package/components/search/search.css +10 -1
  76. package/components/segment/segment.a2ui.json +1 -2
  77. package/components/segment/segment.css +10 -0
  78. package/components/segment/segment.yaml +1 -2
  79. package/components/segmented/segmented.a2ui.json +1 -3
  80. package/components/segmented/segmented.class.js +2 -2
  81. package/components/segmented/segmented.yaml +2 -4
  82. package/components/select/select.a2ui.json +5 -1
  83. package/components/select/select.class.js +14 -14
  84. package/components/select/select.css +20 -1
  85. package/components/select/select.yaml +7 -1
  86. package/components/step-progress/step-progress.yaml +1 -1
  87. package/components/stream/stream.d.ts +2 -0
  88. package/components/switch/switch.css +8 -0
  89. package/components/table/table.a2ui.json +6 -1
  90. package/components/table/table.class.js +18 -1
  91. package/components/table/table.css +31 -6
  92. package/components/table/table.d.ts +3 -1
  93. package/components/table/table.examples.md +2 -2
  94. package/components/table/table.yaml +9 -1
  95. package/components/table-toolbar/table-toolbar.a2ui.json +67 -2
  96. package/components/table-toolbar/table-toolbar.class.js +492 -17
  97. package/components/table-toolbar/table-toolbar.css +311 -0
  98. package/components/table-toolbar/table-toolbar.d.ts +17 -1
  99. package/components/table-toolbar/table-toolbar.yaml +220 -15
  100. package/components/tag/tag.class.js +13 -1
  101. package/components/text/text.a2ui.json +3 -3
  102. package/components/text/text.d.ts +5 -3
  103. package/components/text/text.yaml +3 -3
  104. package/components/textarea/textarea.class.js +9 -1
  105. package/components/textarea/textarea.css +4 -0
  106. package/components/time-picker/time-picker.yaml +3 -2
  107. package/components/tour/tour.d.ts +10 -0
  108. package/components/tree/tree.yaml +1 -1
  109. package/components/upload/upload.class.js +16 -0
  110. package/components/upload/upload.d.ts +7 -0
  111. package/components/upload/upload.yaml +7 -3
  112. package/core/a11y.d.ts +22 -0
  113. package/core/a11y.js +64 -0
  114. package/core/anchor.js +9 -5
  115. package/core/element.js +1 -1
  116. package/core/icons.js +2 -5
  117. package/custom-elements.json +142 -594
  118. package/dist/host.min.css +1 -1
  119. package/dist/host.sheet.js +1 -1
  120. package/dist/theme-provider.min.js +1 -1
  121. package/dist/web-components.min.css +1 -1
  122. package/dist/web-components.min.js +122 -119
  123. package/dist/web-components.sheet.js +1 -1
  124. package/index.d.ts +0 -15
  125. package/package.json +1 -1
  126. package/patterns/access-requests/access-requests.examples.html +1 -1
  127. package/patterns/admin-keys-and-export/admin-keys-and-export.examples.html +7 -7
  128. package/patterns/agent-cost/agent-cost.examples.html +6 -6
  129. package/patterns/agent-memory/agent-memory.examples.html +3 -3
  130. package/patterns/agent-prompt-library/agent-prompt-library.examples.html +8 -8
  131. package/patterns/agent-tool-call/agent-tool-call.examples.html +1 -1
  132. package/patterns/approvals/approvals.examples.html +6 -6
  133. package/patterns/audit-log/audit-log.examples.html +1 -1
  134. package/patterns/bulk-action-toolbar/bulk-action-toolbar.examples.html +4 -4
  135. package/patterns/chart-in-card/chart-in-card.examples.html +124 -10
  136. package/patterns/chart-in-card/chart-in-card.examples.js +37 -0
  137. package/patterns/comments-and-collaboration/comments-and-collaboration.examples.html +5 -5
  138. package/patterns/conversion-funnel/conversion-funnel.html +2 -2
  139. package/patterns/data-tables-inline-edit-and-tree/data-tables-inline-edit-and-tree.examples.html +2 -2
  140. package/patterns/diff-review/diff-review.examples.html +6 -6
  141. package/patterns/editor-shell/editor-shell.examples.html +3 -3
  142. package/patterns/export-flow/export-flow.examples.html +2 -2
  143. package/patterns/filter-bar/filter-bar.examples.html +2 -2
  144. package/patterns/forms-conditional-and-autocomplete/forms-conditional-and-autocomplete.examples.html +2 -2
  145. package/patterns/inline-dialog/inline-dialog.examples.html +2 -2
  146. package/patterns/marketing-engagement/marketing-engagement.examples.html +4 -4
  147. package/patterns/notifications-bell-and-digest/notifications-bell-and-digest.examples.html +1 -1
  148. package/patterns/permissions-matrix/permissions-matrix.examples.html +1 -1
  149. package/patterns/permissions-sharing/permissions-sharing.examples.html +2 -2
  150. package/patterns/profile-public-and-verification/profile-public-and-verification.examples.html +4 -4
  151. package/patterns/search-discovery/search-discovery.examples.html +2 -2
  152. package/styles/colors/semantics/features.css +8 -1
  153. package/styles/components.css +0 -4
  154. package/styles/type/roles.css +29 -9
  155. package/traits/arrow-grid-nav/arrow-grid-nav.js +1 -1
  156. package/traits/resize-observer/resize-observer.js +1 -1
  157. package/components/color-picker/color-picker.a2ui.json +0 -148
  158. package/components/color-picker/color-picker.class.js +0 -46
  159. package/components/color-picker/color-picker.css +0 -14
  160. package/components/color-picker/color-picker.d.ts +0 -99
  161. package/components/color-picker/color-picker.examples.md +0 -19
  162. package/components/color-picker/color-picker.js +0 -17
  163. package/components/color-picker/color-picker.yaml +0 -175
  164. package/components/datetime-picker/datetime-picker.a2ui.json +0 -256
  165. package/components/datetime-picker/datetime-picker.class.js +0 -58
  166. package/components/datetime-picker/datetime-picker.css +0 -11
  167. package/components/datetime-picker/datetime-picker.d.ts +0 -87
  168. package/components/datetime-picker/datetime-picker.examples.md +0 -25
  169. package/components/datetime-picker/datetime-picker.js +0 -17
  170. package/components/datetime-picker/datetime-picker.yaml +0 -254
  171. package/components/progress-row/progress-row.a2ui.json +0 -107
  172. package/components/progress-row/progress-row.class.js +0 -118
  173. package/components/progress-row/progress-row.css +0 -60
  174. package/components/progress-row/progress-row.d.ts +0 -24
  175. package/components/progress-row/progress-row.examples.md +0 -7
  176. package/components/progress-row/progress-row.js +0 -17
  177. package/components/progress-row/progress-row.yaml +0 -85
  178. package/components/toggle-group/toggle-group.a2ui.json +0 -148
  179. package/components/toggle-group/toggle-group.class.js +0 -195
  180. package/components/toggle-group/toggle-group.css +0 -127
  181. package/components/toggle-group/toggle-group.d.ts +0 -50
  182. package/components/toggle-group/toggle-group.examples.md +0 -33
  183. package/components/toggle-group/toggle-group.js +0 -18
  184. package/components/toggle-group/toggle-group.yaml +0 -135
  185. package/components/toggle-group/toggle-option.a2ui.json +0 -86
  186. package/components/toggle-group/toggle-option.yaml +0 -75
@@ -18,7 +18,9 @@
18
18
  * • title + optional count badge
19
19
  * • filter / sort / columns popover buttons
20
20
  * • search input
21
- * • optional [slot="actions"] trailing region
21
+ * • optional [slot="actions-leading"] region, BEFORE the page-size select (gh#1649)
22
+ * • optional page-size select
23
+ * • optional [slot="actions"] trailing region, AFTER the page-size select
22
24
  *
23
25
  * [for] resolution mirrors chart-legend-ui — the toolbar mounts an element by
24
26
  * id, then dispatches state changes against it. When [for] is absent, falls
@@ -35,10 +37,24 @@
35
37
  */
36
38
 
37
39
  import { UIElement } from '../../core/element.js';
38
- import { anchorPopover } from '../../core/anchor.js';
40
+ import { anchorPopover, resolveLengthPx } from '../../core/anchor.js';
39
41
 
40
42
  const SEARCH_DEBOUNCE = 200;
41
43
 
44
+ // ADR-0076 REQ-M-006 — the three breakpoints' NORMATIVE default pixel
45
+ // widths (56rem/40rem/26rem at the standard 16px root), duplicated here
46
+ // ONLY as a fallback for #classifyStage() below when the live
47
+ // `--table-toolbar-bp-*` custom property can't be read (no table-toolbar.css
48
+ // stylesheet attached yet, an SSR shim, or a unit-test harness with no real
49
+ // cascade) — resolveLengthPx() returns NaN in that case, `||` falls
50
+ // through to these. Table-toolbar.css's own `:where(:scope)` declarations
51
+ // are the actual source of truth whenever a real stylesheet IS present;
52
+ // this fallback exists so the reflection degrades to the DOCUMENTED
53
+ // default instead of silently misclassifying against NaN.
54
+ const DEFAULT_BP_SEARCH_TIGHT_PX = 56 * 16;
55
+ const DEFAULT_BP_ICON_ONLY_PX = 40 * 16;
56
+ const DEFAULT_BP_OVERFLOW_PX = 26 * 16;
57
+
42
58
  function emptyHint(text) {
43
59
  const el = document.createElement('text-ui');
44
60
  el.setAttribute('data-popover-empty', '');
@@ -128,18 +144,36 @@ export class UITableToolbar extends UIElement {
128
144
  for: { type: String, default: '', reflect: true },
129
145
  text: { type: String, default: '', reflect: false },
130
146
  count: { type: String, default: '', reflect: false },
147
+ rangeStart: { type: Number, default: 0, reflect: true, attribute: 'range-start' },
148
+ rangeEnd: { type: Number, default: 0, reflect: true, attribute: 'range-end' },
149
+ rangeTotal: { type: Number, default: 0, reflect: true, attribute: 'range-total' },
150
+ pageSize: { type: Number, default: 0, reflect: true, attribute: 'page-size' },
131
151
  noFilter: { type: Boolean, default: false, reflect: true, attribute: 'no-filter' },
132
152
  noSort: { type: Boolean, default: false, reflect: true, attribute: 'no-sort' },
133
153
  noColumns: { type: Boolean, default: false, reflect: true, attribute: 'no-columns' },
134
154
  noSearch: { type: Boolean, default: false, reflect: true, attribute: 'no-search' },
155
+ // ADR-0076 (ADIA2-9123) — additive sugar over the four `no-*` attributes
156
+ // above. Pure, absolute OR: see #updateControlVisibility(), which is the
157
+ // single place that combines chromeOnly with each granular no-* value.
158
+ chromeOnly: { type: Boolean, default: false, reflect: true, attribute: 'chrome-only' },
135
159
  placeholder: { type: String, default: 'Search...', reflect: false },
136
160
  variant: { type: String, default: 'default', reflect: true },
161
+ // ADR-0076 REQ-O-001 — explicit compaction-stage override. Unset (the
162
+ // default, empty string) means auto-snap via the @container query
163
+ // (table-toolbar.css's :scope container-type/container-name). All four
164
+ // stages' CSS is now live: search-tight steps the search field down
165
+ // (step 2); icon-only drops control labels and compacts/hides the
166
+ // range summary (steps 3+4); overflow collapses Filter/Sort/Columns
167
+ // into one "More" trigger (step 5).
168
+ stage: { type: String, default: '', reflect: true },
137
169
  };
138
170
 
139
171
  // §205 (v0.5.7): dynamic sort-indicator icons (table-toolbar.class.js:576 — nested ternary
140
172
  // `dir === 'asc' ? 'arrow-up' : dir === 'desc' ? 'arrow-down' : 'caret-up-down'`).
141
173
  // Per FEEDBACK-16 §1 + §209 slot-11 ternary-walker discovery.
142
- static requiredIcons = ['arrow-up', 'arrow-down', 'caret-up-down'];
174
+ // 'dots-three-vertical' — the overflow-stage "More" trigger icon
175
+ // (ADR-0076 REQ-S-004, PLAN step 5, #mkOverflowTrigger()).
176
+ static requiredIcons = ['arrow-up', 'arrow-down', 'caret-up-down', 'dots-three-vertical'];
143
177
 
144
178
  static template = () => null;
145
179
 
@@ -149,6 +183,30 @@ export class UITableToolbar extends UIElement {
149
183
  #docListenersBound = false;
150
184
  #docListenerRaf = null;
151
185
  #sortIndicatorRafs = new Set();
186
+ #pageSizeOptions = [];
187
+ // ADR-0076 REQ-M-005 (ADIA2-9123 S4-ii), PLAN step 7 — data-stage-resolved
188
+ // reflection. #resizeObs classifies live width against the SAME three
189
+ // breakpoints table-toolbar.css's @container rules use (read from the
190
+ // custom properties themselves, never a second hardcoded copy of
191
+ // 56/40/26 — see #classifyStage()). #lastWidthPx caches the last
192
+ // measured inline-size so a `stage` attribute change alone (no resize)
193
+ // can still re-classify without waiting on the next ResizeObserver
194
+ // callback (REQ-M-005's dual-trigger requirement).
195
+ #resizeObs = null;
196
+ #lastWidthPx = null;
197
+ #stageRaf = null;
198
+
199
+ // ── Public API: pageSizeOptions ──────────────────────────────────────────
200
+ // Dynamic array prop (JS-set collection, custom setter) — same shape as
201
+ // table-ui's `columns`/`data`. Deliberately not in `static properties`
202
+ // (gh#970 — an array prop needs its own setter, not the scalar signal path).
203
+
204
+ set pageSizeOptions(arr) {
205
+ this.#pageSizeOptions = Array.isArray(arr) ? arr.map(Number).filter((n) => Number.isFinite(n)) : [];
206
+ this.#updatePageSizeSelect();
207
+ }
208
+
209
+ get pageSizeOptions() { return this.#pageSizeOptions; }
152
210
 
153
211
  // §381 (v0.6.10, slice F): one-shot dev-mode warning when consumers
154
212
  // set common-but-unknown opt-out attributes (e.g. `searchable`,
@@ -165,9 +223,88 @@ export class UITableToolbar extends UIElement {
165
223
  connected() {
166
224
  this.setAttribute('role', 'toolbar');
167
225
  this.#warnUnknownOptOutAttrs();
226
+ // gh#1615 — hydrate `page-size-options="[10,25,50]"` once at connect,
227
+ // same pattern as table-ui's `data`/`columns` JSON-array attributes
228
+ // (table.class.js connected()). A later programmatic `.pageSizeOptions
229
+ // = [...]` set still wins (guarded by the length check).
230
+ if (this.#pageSizeOptions.length === 0 && this.hasAttribute('page-size-options')) {
231
+ try {
232
+ const parsed = JSON.parse(this.getAttribute('page-size-options'));
233
+ if (Array.isArray(parsed)) this.pageSizeOptions = parsed;
234
+ } catch (_) { /* malformed JSON — leave empty, select renders hidden */ }
235
+ }
168
236
  this.#stamp();
169
237
  this.#resolveTarget();
170
238
  this.#syncFromTarget();
239
+
240
+ // ADR-0076 REQ-M-005, PLAN step 7 — classify synchronously off
241
+ // `clientWidth` at connect time too (0 pre-layout treated as "no
242
+ // measurement yet", the same `null`-width fallback #reflectResolvedStage
243
+ // uses) rather than waiting on the ResizeObserver's own first
244
+ // (asynchronous) callback — avoids a brief connect-time gap with no
245
+ // `data-stage-resolved` reflected at all, mirroring how chart.class.js's
246
+ // own `data-ratio-resolved` is set synchronously from `#dims()` at
247
+ // first render(), not deferred to its ResizeObserver's first tick.
248
+ this.#lastWidthPx = this.clientWidth || null;
249
+ this.#reflectResolvedStage();
250
+
251
+ // gh#285 SSR DOM shims (linkedom) have no ResizeObserver global, same
252
+ // guard chart.class.js's own ResizeObserver setup uses.
253
+ if (typeof ResizeObserver !== 'undefined') {
254
+ this.#resizeObs = new ResizeObserver((entries) => {
255
+ const { inlineSize } = entries[0].contentBoxSize[0];
256
+ this.#lastWidthPx = inlineSize;
257
+ // rAF-batch, mirroring chart.class.js's own ResizeObserver setup —
258
+ // avoids a same-tick reflect→layout→observer-refire loop.
259
+ if (this.#stageRaf) return;
260
+ this.#stageRaf = requestAnimationFrame(() => {
261
+ this.#stageRaf = null;
262
+ this.#reflectResolvedStage();
263
+ });
264
+ });
265
+ this.#resizeObs.observe(this);
266
+ }
267
+ }
268
+
269
+ // gh#1620 — this used to live in a declared `attributeChanged(name)` hook
270
+ // that the base class never calls (core/element.js only ever invokes
271
+ // `updated(changed)`, keyed by property name, not attribute name); the
272
+ // reactions below silently never fired. Migrated verbatim onto the live
273
+ // hook, keyed by the matching property names.
274
+ updated(changed) {
275
+ if (changed.has('noFilter') || changed.has('noSort')
276
+ || changed.has('noColumns') || changed.has('noSearch')
277
+ || changed.has('chromeOnly')) {
278
+ this.#updateControlVisibility();
279
+ }
280
+ if (changed.has('placeholder')) {
281
+ const search = this.querySelector(':scope [data-search]');
282
+ search?.setAttribute('placeholder', this.placeholder);
283
+ }
284
+ }
285
+
286
+ // ADR-0076 REQ-E-001 — `updated(changed)` alone MISSES a live loading→
287
+ // confirmed-zero transition: `rangeTotal`'s default is already 0, so
288
+ // setting `range-total="0"` from an unset start parses to the SAME
289
+ // Number(0), the base class's property setter's `Object.is` short-circuit
290
+ // swallows the signal write (core/element.js `installProps()`), and
291
+ // `changed` never gets a `rangeTotal` entry — `#updateSummary()`'s
292
+ // hasAttribute-based branch never re-runs. Verified live in a browser
293
+ // (not just the vitest suite, which happened to only exercise the
294
+ // positive→zero transition, where the signal DOES change). REQ-E-001's
295
+ // check is fundamentally an ATTRIBUTE-presence concern, not a coerced-
296
+ // value one, so it needs the raw attribute callback, not the signal diff.
297
+ attributeChangedCallback(name, oldVal, newVal) {
298
+ super.attributeChangedCallback(name, oldVal, newVal);
299
+ if (name === 'range-total') this.#updateSummary();
300
+ // ADR-0076 REQ-M-005 — the SECOND of the two triggers `data-stage-
301
+ // resolved` must react to: a live resize (the ResizeObserver callback
302
+ // above) AND a `stage` attribute change alone, with no resize needed.
303
+ // Re-classifies against the cached last-measured width — a pin set
304
+ // before the first resize observation (#lastWidthPx still null) simply
305
+ // reports the pin; classifyStage() below treats a null width as "no
306
+ // live width to compare against yet", never a false 0-width match.
307
+ if (name === 'stage') this.#reflectResolvedStage();
171
308
  }
172
309
 
173
310
  disconnected() {
@@ -179,6 +316,68 @@ export class UITableToolbar extends UIElement {
179
316
  this.#sortIndicatorRafs.clear();
180
317
  this.#closePopover();
181
318
  this.#detachTarget();
319
+ if (this.#stageRaf != null) {
320
+ cancelAnimationFrame(this.#stageRaf);
321
+ this.#stageRaf = null;
322
+ }
323
+ this.#resizeObs?.disconnect();
324
+ this.#resizeObs = null;
325
+ }
326
+
327
+ // ── data-stage-resolved reflection (ADR-0076 REQ-M-005, ADIA2-9123 ─────────
328
+ // S4-ii, PLAN step 7) ────────────────────────────────────────────────────
329
+ //
330
+ // Purely informational — mirrors the CSS-decided stage for a consumer's
331
+ // own [slot="actions-leading"] content, never drives table-toolbar's own
332
+ // rendering (REQ-S stays 100% CSS `@container`-driven, REQ-M-002). The
333
+ // three breakpoints are read live off the SAME custom properties the CSS
334
+ // `@container` rules consume (--table-toolbar-bp-search-tight/-icon-only/
335
+ // -overflow) via `resolveLengthPx` (core/anchor.js, shared with its
336
+ // --popover-max-height use) — one source of truth, never a second
337
+ // hardcoded copy of 56/40/26 here.
338
+
339
+ #classifyStage(widthPx) {
340
+ const cs = getComputedStyle(this);
341
+ // resolveLengthPx() returns NaN when the custom property is unreadable
342
+ // — no table-toolbar.css stylesheet attached yet, an SSR shim, or a
343
+ // headless unit-test harness with no real cascade — so an explicit
344
+ // Number.isFinite() check falls through to each breakpoint's own
345
+ // documented REQ-M-006 default. This must be a real NaN check, not a
346
+ // `|| DEFAULT` fallback: `0` is a legitimate consumer override (e.g.
347
+ // `--table-toolbar-bp-overflow: 0rem` to disable the overflow stage
348
+ // entirely) and `||` would silently discard it as falsy, substituting
349
+ // the default instead — reviewer finding on PR #1676, fixed here.
350
+ // Whenever a real stylesheet IS present, the live custom property
351
+ // (overridable per-consumer, same posture as every other
352
+ // --table-toolbar-* token) wins, zero included.
353
+ const resolveBp = (raw, fallback) => {
354
+ const v = resolveLengthPx(raw);
355
+ return Number.isFinite(v) ? v : fallback;
356
+ };
357
+ const searchTightPx = resolveBp(cs.getPropertyValue('--table-toolbar-bp-search-tight'), DEFAULT_BP_SEARCH_TIGHT_PX);
358
+ const iconOnlyPx = resolveBp(cs.getPropertyValue('--table-toolbar-bp-icon-only'), DEFAULT_BP_ICON_ONLY_PX);
359
+ const overflowPx = resolveBp(cs.getPropertyValue('--table-toolbar-bp-overflow'), DEFAULT_BP_OVERFLOW_PX);
360
+
361
+ // Inclusive-on-the-narrower-side (max-width semantics, REQ-M-006) — a
362
+ // width exactly AT a boundary classifies to the narrower stage, same
363
+ // rule the CSS `@container (max-width: …)` conditions already encode.
364
+ if (widthPx <= overflowPx) return 'overflow';
365
+ if (widthPx <= iconOnlyPx) return 'icon-only';
366
+ if (widthPx <= searchTightPx) return 'search-tight';
367
+ return 'full';
368
+ }
369
+
370
+ #reflectResolvedStage() {
371
+ // REQ-O-001/REQ-M-005 — the pin wins for the reflection exactly as it
372
+ // wins for rendering: an explicit `stage` reports itself, never the
373
+ // width-derived classification, even inside a container sized for a
374
+ // different stage.
375
+ const resolved = this.stage || (this.#lastWidthPx == null ? 'full' : this.#classifyStage(this.#lastWidthPx));
376
+ // Is-different guard — same shape as chart.class.js's own
377
+ // data-ratio-resolved reflection (chart.class.js:533).
378
+ if (this.getAttribute('data-stage-resolved') !== resolved) {
379
+ this.setAttribute('data-stage-resolved', resolved);
380
+ }
182
381
  }
183
382
 
184
383
  render() {
@@ -186,6 +385,8 @@ export class UITableToolbar extends UIElement {
186
385
  this.#resolveTarget();
187
386
  this.#syncFromTarget();
188
387
  this.#updateTitle();
388
+ this.#updateSummary();
389
+ this.#updatePageSizeSelect();
189
390
  }
190
391
 
191
392
  // ── Unknown opt-out attribute warning (§381, v0.6.10 slice F) ────────────
@@ -273,6 +474,20 @@ export class UITableToolbar extends UIElement {
273
474
  const root = document.createElement('div');
274
475
  root.setAttribute('data-toolbar', '');
275
476
 
477
+ // Scope slot passthrough (gh#1615) — leading region before the title
478
+ // cluster. Same real-insertion-point pattern as [slot="actions"] below:
479
+ // move any pre-existing [slot="scope"] children into a stamped wrapper
480
+ // rather than declaring a template-owned `parts:` entry (ADR-0067 — an
481
+ // author-fillable span is `slots:`, never `parts:`).
482
+ const scopeSlot = document.createElement('div');
483
+ scopeSlot.setAttribute('data-scope', '');
484
+ for (const node of [...this.children]) {
485
+ if (node === root) continue;
486
+ if (node.getAttribute?.('slot') === 'scope') {
487
+ scopeSlot.appendChild(node);
488
+ }
489
+ }
490
+
276
491
  // Title cluster
277
492
  const title = document.createElement('div');
278
493
  title.setAttribute('data-title', '');
@@ -288,12 +503,61 @@ export class UITableToolbar extends UIElement {
288
503
  badge.hidden = true;
289
504
  title.appendChild(badge);
290
505
 
506
+ // Range summary ("Showing X–Y of N", gh#1615) — its own cluster,
507
+ // independent of [data-title]: coexists with [text]/[count] rather than
508
+ // nesting inside their hide-with-children logic (ADIA2-8897).
509
+ const summary = document.createElement('div');
510
+ summary.setAttribute('data-summary', '');
511
+ summary.hidden = true;
512
+ const summaryText = document.createElement('span');
513
+ summaryText.setAttribute('data-summary-text', '');
514
+ summary.appendChild(summaryText);
515
+
516
+ // ADR-0076 REQ-S-003 — short-form compact text ("1–25/320"), a sibling
517
+ // of [data-summary-text] holding the SAME range values in a terser
518
+ // shape. Which of the two is visible is decided entirely by CSS
519
+ // (table-toolbar.css's icon-only @container rule / [stage] pin) —
520
+ // both nodes are always populated together in #updateSummary(), never
521
+ // a JS branch keyed on a resolved-stage signal (REQ-M-002 stays true:
522
+ // no JS decides rendering here, only which text each stage's CSS shows).
523
+ const summaryCompact = document.createElement('span');
524
+ summaryCompact.setAttribute('data-summary-compact', '');
525
+ summaryCompact.hidden = true;
526
+ summary.appendChild(summaryCompact);
527
+
528
+ // Empty-state slot (ADR-0076 REQ-E, adiav2 ADIA2-9123) — renders in
529
+ // [data-summary]'s own position, alongside [data-summary-text], never a
530
+ // separate region: only one of the two is ever visible at a time (see
531
+ // #updateSummary()). Same real-insertion-point pattern as [slot="scope"]
532
+ // / [slot="actions"] / [slot="actions-leading"] above — move any
533
+ // pre-existing [slot="empty"] children into this stamped wrapper rather
534
+ // than declaring a template-owned `parts:` entry (ADR-0067).
535
+ const summaryEmpty = document.createElement('div');
536
+ summaryEmpty.setAttribute('data-summary-empty', '');
537
+ summaryEmpty.hidden = true;
538
+ for (const node of [...this.children]) {
539
+ if (node === root) continue;
540
+ if (node.getAttribute?.('slot') === 'empty') {
541
+ summaryEmpty.appendChild(node);
542
+ }
543
+ }
544
+ summary.appendChild(summaryEmpty);
545
+
291
546
  // Controls cluster
292
547
  const controls = document.createElement('div');
293
548
  controls.setAttribute('data-controls', '');
294
549
  controls.appendChild(this.#mkButton('filter', 'Filter', 'funnel-simple'));
295
550
  controls.appendChild(this.#mkButton('sort', 'Sort', 'arrows-down-up'));
296
551
  controls.appendChild(this.#mkButton('columns', 'Columns', 'columns'));
552
+ // ADR-0076 REQ-S-004/REQ-X-003, PLAN step 5 — overflow-stage "More"
553
+ // trigger. Stamped unconditionally alongside the three granular
554
+ // buttons above (never removed/re-created), CSS-hidden except at the
555
+ // overflow breakpoint/pin (REQ-M-002 stays true: which cluster paints
556
+ // is a pure CSS decision, not a JS branch). Keeping all four buttons
557
+ // permanently in the DOM is the simplest way to honor REQ-X-004's
558
+ // tab-order guarantee — nothing is ever created/destroyed across a
559
+ // stage transition, only shown/hidden.
560
+ controls.appendChild(this.#mkOverflowTrigger());
297
561
 
298
562
  // Search — compose <search-ui>, which already stamps input-ui with
299
563
  // the magnifying-glass prefix + clear suffix and debounces a `search`
@@ -307,6 +571,38 @@ export class UITableToolbar extends UIElement {
307
571
  search.setAttribute('debounce', String(SEARCH_DEBOUNCE));
308
572
  search.addEventListener('search', this.#onSearch);
309
573
 
574
+ // Page-size select (gh#1615) — hidden until pageSizeOptions is set
575
+ // (additive opt-in, zero footprint on existing consumers). Carries the
576
+ // same `margin-inline-start: auto` as [data-search] so the trailing
577
+ // group (controls/search/page-size/actions) still pushes to the far
578
+ // edge when [no-search] drops search — without touching search's own
579
+ // existing default behavior for consumers that never set pageSizeOptions.
580
+ const pageSize = document.createElement('select-ui');
581
+ pageSize.setAttribute('data-page-size', '');
582
+ pageSize.setAttribute('size', 'sm');
583
+ pageSize.setAttribute('aria-label', 'Rows per page');
584
+ pageSize.hidden = true;
585
+ pageSize.addEventListener('change', this.#onPageSizeChange);
586
+
587
+ // Actions-leading slot passthrough (gh#1649) — a real insertion point
588
+ // (ADR-0067), same pattern as [slot="scope"]/[slot="actions"] above,
589
+ // rendered BEFORE the page-size select rather than after it: app-owned
590
+ // trigger buttons (e.g. custom Filter/Columns triggers that open the
591
+ // consumer's own drawer instead of the native filter/columns popovers)
592
+ // need to land ahead of page-size, which [slot="actions"] alone can't
593
+ // reach since it renders trailing (after page-size).
594
+ const actionsLeadingSlot = document.createElement('div');
595
+ actionsLeadingSlot.setAttribute('data-actions-leading', '');
596
+ // Move any pre-existing [slot="actions-leading"] children into this
597
+ // stamped wrapper — same real-insertion-point pattern as [slot="scope"]
598
+ // / [slot="actions"] above.
599
+ for (const node of [...this.children]) {
600
+ if (node === root) continue;
601
+ if (node.getAttribute?.('slot') === 'actions-leading') {
602
+ actionsLeadingSlot.appendChild(node);
603
+ }
604
+ }
605
+
310
606
  // Actions slot passthrough — we move any pre-existing [slot="actions"] children here
311
607
  const actionsSlot = document.createElement('div');
312
608
  actionsSlot.setAttribute('data-actions', '');
@@ -317,14 +613,20 @@ export class UITableToolbar extends UIElement {
317
613
  }
318
614
  }
319
615
 
616
+ root.appendChild(scopeSlot);
320
617
  root.appendChild(title);
618
+ root.appendChild(summary);
321
619
  root.appendChild(controls);
322
620
  root.appendChild(search);
621
+ root.appendChild(actionsLeadingSlot);
622
+ root.appendChild(pageSize);
323
623
  root.appendChild(actionsSlot);
324
624
 
325
625
  this.appendChild(root);
326
626
  this.#updateTitle();
627
+ this.#updateSummary();
327
628
  this.#updateControlVisibility();
629
+ this.#updatePageSizeSelect();
328
630
  }
329
631
 
330
632
  #mkPopoverAction(label, onClick) {
@@ -346,6 +648,14 @@ export class UITableToolbar extends UIElement {
346
648
  btn.setAttribute('variant', 'outline');
347
649
  btn.setAttribute('size', 'sm');
348
650
  btn.setAttribute('aria-haspopup', 'menu');
651
+ // ADR-0076 REQ-X-002 — unconditional (harmless at full-text stages,
652
+ // becomes the hover-tooltip affordance at the icon-only stage once
653
+ // the rendered label is CSS-hidden, REQ-M-003). button-ui's own
654
+ // title→aria-label mirror (button.class.js) only fires when no
655
+ // aria-label is already present — button-ui stamps aria-label from
656
+ // [text] unconditionally in its own render(), so this never conflicts
657
+ // with REQ-X-001's already-set accessible name.
658
+ btn.setAttribute('title', label);
349
659
  btn.addEventListener('click', (e) => {
350
660
  e.stopPropagation();
351
661
  this.#togglePopover(kind, btn);
@@ -353,6 +663,55 @@ export class UITableToolbar extends UIElement {
353
663
  return btn;
354
664
  }
355
665
 
666
+ // ADR-0076 REQ-S-004/REQ-X-003, PLAN step 5 — the overflow-stage "More"
667
+ // trigger. Reuses menu-ui/menu-item-ui unchanged (table-toolbar already
668
+ // `composes: menu-item-ui`; menu-ui is added to the yaml's composes list
669
+ // alongside it — no new runtime dependency, both already ship in this
670
+ // package) and routes every item straight through the EXISTING
671
+ // #togglePopover(kind, btn) — the same method the three granular buttons
672
+ // above call, never a reimplementation of filter/sort/columns logic.
673
+ #mkOverflowTrigger() {
674
+ const menu = document.createElement('menu-ui');
675
+ menu.setAttribute('data-toolbar-overflow', '');
676
+
677
+ const trigger = document.createElement('button-ui');
678
+ trigger.setAttribute('slot', 'trigger');
679
+ trigger.setAttribute('icon', 'dots-three-vertical');
680
+ trigger.setAttribute('variant', 'outline');
681
+ trigger.setAttribute('size', 'sm');
682
+ // REQ-X-003 — a genuinely new interactive element (not a repurposed
683
+ // existing button), so it needs its own static accessible name rather
684
+ // than an inherited one. No [text] attribute — always icon-only,
685
+ // button-ui's own render() reflects [data-icon-only] geometry from
686
+ // that (no CSS override needed here, unlike REQ-M-004's granular
687
+ // buttons which carry a real text label at wider stages).
688
+ trigger.setAttribute('aria-label', 'More table actions');
689
+ trigger.setAttribute('aria-haspopup', 'menu');
690
+ menu.appendChild(trigger);
691
+
692
+ for (const [kind, label, icon] of [
693
+ ['filter', 'Filter', 'funnel-simple'],
694
+ ['sort', 'Sort', 'arrows-down-up'],
695
+ ['columns', 'Columns', 'columns'],
696
+ ]) {
697
+ const item = document.createElement('menu-item-ui');
698
+ item.setAttribute('icon', icon);
699
+ item.setAttribute('text', label);
700
+ item.setAttribute('value', kind);
701
+ menu.appendChild(item);
702
+ }
703
+
704
+ // menu-ui fires `action` with { value, text } (menu.class.js's own
705
+ // #onItemClick) — anchor the resulting popover on the "More" trigger
706
+ // itself (not any of the three hidden granular buttons), same
707
+ // #togglePopover(kind, btn) every other entry point already calls.
708
+ menu.addEventListener('action', (e) => {
709
+ this.#togglePopover(e.detail.value, trigger);
710
+ });
711
+
712
+ return menu;
713
+ }
714
+
356
715
  #updateTitle() {
357
716
  const heading = this.querySelector(':scope [data-heading]');
358
717
  if (heading) {
@@ -397,29 +756,145 @@ export class UITableToolbar extends UIElement {
397
756
  }
398
757
  }
399
758
 
759
+ // ── Range summary ("Showing X–Y of N", gh#1615) ─────────────────────────
760
+ // Independent of #updateTitle's [data-title] cluster by design — coexists
761
+ // with [text]/[count] rather than sharing their hide-with-children logic.
762
+
763
+ // ADR-0076 REQ-E — the `empty` slot renders ONLY on an explicit
764
+ // range-total="0" (checked via hasAttribute, never the computed number
765
+ // alone — REQ-E-001/AC-013: an unset range-total, adiav2's loading state,
766
+ // must never flash the empty-state message). A positive range-total
767
+ // always wins over empty-slot content (REQ-E-002's additive guarantee /
768
+ // AC-014's mutual-exclusion check) — the two states can never render
769
+ // simultaneously.
770
+ #updateSummary() {
771
+ const summary = this.querySelector(':scope > [data-toolbar] > [data-summary]');
772
+ if (!summary) return;
773
+ const text = summary.querySelector(':scope > [data-summary-text]');
774
+ const compact = summary.querySelector(':scope > [data-summary-compact]');
775
+ const empty = summary.querySelector(':scope > [data-summary-empty]');
776
+ const start = Number(this.rangeStart) || 0;
777
+ const end = Number(this.rangeEnd) || 0;
778
+ const total = Number(this.rangeTotal) || 0;
779
+
780
+ if (start > 0 && end > 0 && total > 0) {
781
+ // Normal range text — always wins over the empty slot (AC-014).
782
+ // Both the full ("Showing X–Y of N") and short-form ("X–Y/N", ADR-0076
783
+ // REQ-S-003) strings are populated together — which one paints is a
784
+ // pure CSS decision (icon-only @container rule / [stage] pin), never
785
+ // a JS branch (REQ-M-002).
786
+ if (text) text.textContent = `Showing ${start}–${end} of ${total}`;
787
+ if (text) text.hidden = false;
788
+ if (compact) compact.textContent = `${start}–${end}/${total}`;
789
+ if (compact) compact.hidden = false;
790
+ if (empty) empty.hidden = true;
791
+ summary.hidden = false;
792
+ return;
793
+ }
794
+
795
+ const confirmedZero = this.hasAttribute('range-total') && this.getAttribute('range-total') === '0';
796
+ const hasEmptyContent = !!empty && empty.childNodes.length > 0;
797
+
798
+ if (confirmedZero && hasEmptyContent) {
799
+ // Explicit range-total="0" + author-supplied empty content (AC-011).
800
+ if (text) text.hidden = true;
801
+ if (compact) compact.hidden = true;
802
+ empty.hidden = false;
803
+ summary.hidden = false;
804
+ return;
805
+ }
806
+
807
+ // Loading (range-total unset, AC-013) or confirmed-zero-with-no-slot-
808
+ // content (AC-012) — hide [data-summary] exactly as today (REQ-E-002).
809
+ if (text) text.hidden = true;
810
+ if (compact) compact.hidden = true;
811
+ if (empty) empty.hidden = true;
812
+ summary.hidden = true;
813
+ }
814
+
815
+ // ── Page-size select (gh#1615) ───────────────────────────────────────────
816
+ // Hidden whenever pageSizeOptions is empty — additive opt-in, matching the
817
+ // [slot="actions"]:empty pattern's "no footprint unless a consumer opts in".
818
+
819
+ #updatePageSizeSelect() {
820
+ const select = this.querySelector(':scope > [data-toolbar] > [data-page-size]');
821
+ if (!select) return;
822
+ const options = this.#pageSizeOptions;
823
+ if (!options.length) {
824
+ select.hidden = true;
825
+ return;
826
+ }
827
+ select.hidden = false;
828
+
829
+ // `.options = [...]` (select-ui's documented programmatic API) rather
830
+ // than appending light-DOM <option> children: select-ui's own
831
+ // #parseOptions() PARSES-THEN-REMOVES any <option>/<optgroup> children
832
+ // once connected, and only re-parses newly-appended ones via an async
833
+ // MutationObserver — appending after this element is already connected
834
+ // (which it is, by the time a later pageSizeOptions/pageSize change
835
+ // reaches this method) races that observer. `.options=` is synchronous
836
+ // and connection-state-agnostic, matching table-toolbar's own
837
+ // initial-mount-through-any-later-update determinism elsewhere.
838
+ const desired = options.map((n) => ({ value: String(n), label: `${n} / page` }));
839
+ select.options = desired;
840
+
841
+ const size = Number(this.pageSize) || 0;
842
+ const value = options.includes(size) ? size : options[0];
843
+ select.value = String(value);
844
+ }
845
+
846
+ #onPageSizeChange = (e) => {
847
+ const pageSize = Number(e.detail?.value ?? this.querySelector(':scope [data-page-size]')?.value) || 0;
848
+ this.pageSize = pageSize;
849
+ // Applied directly to the bound table's own [paginate] (rows-per-page)
850
+ // prop — the same "mirror to URL/analytics via the event, but the
851
+ // toolbar already wires the change into the target" pattern
852
+ // columns-change/sort-change already use.
853
+ if (this.#target) this.#target.paginate = pageSize;
854
+ this.dispatchEvent(new CustomEvent('page-size-change', {
855
+ bubbles: true,
856
+ detail: { pageSize },
857
+ }));
858
+ };
859
+
860
+ // ADR-0076 REQ-C-002 — `chromeOnly` is PURE, ABSOLUTE OR over the four
861
+ // granular `no-*` attributes: while set, all four stay off regardless of
862
+ // any individual `no-*` attribute's own value, with no partial re-enable
863
+ // path (AC-010). This is the ONLY place chromeOnly is combined with the
864
+ // granular attributes — never re-derive the OR elsewhere.
400
865
  #updateControlVisibility() {
401
866
  const root = this.querySelector(':scope > [data-toolbar]');
402
867
  if (!root) return;
403
868
 
869
+ const chromeOnly = this.chromeOnly;
404
870
  const setHidden = (sel, hidden) => {
405
871
  const el = root.querySelector(sel);
406
872
  if (el) el.hidden = hidden;
407
873
  };
408
874
 
409
- setHidden('[data-toolbar-btn="filter"]', this.noFilter);
410
- setHidden('[data-toolbar-btn="sort"]', this.noSort);
411
- setHidden('[data-toolbar-btn="columns"]', this.noColumns);
412
- setHidden('[data-search]', this.noSearch);
413
- }
414
-
415
- // Re-run on attribute changes for boolean flags.
416
- attributeChanged(name) {
417
- if (['no-filter', 'no-sort', 'no-columns', 'no-search'].includes(name)) {
418
- this.#updateControlVisibility();
419
- }
420
- if (name === 'placeholder') {
421
- const search = this.querySelector(':scope [data-search]');
422
- search?.setAttribute('placeholder', this.placeholder);
875
+ const filterHidden = chromeOnly || this.noFilter;
876
+ const sortHidden = chromeOnly || this.noSort;
877
+ const columnsHidden = chromeOnly || this.noColumns;
878
+
879
+ setHidden('[data-toolbar-btn="filter"]', filterHidden);
880
+ setHidden('[data-toolbar-btn="sort"]', sortHidden);
881
+ setHidden('[data-toolbar-btn="columns"]', columnsHidden);
882
+ setHidden('[data-search]', chromeOnly || this.noSearch);
883
+
884
+ // ADR-0076 REQ-S-004, PLAN step 5 — the overflow "More" trigger's own
885
+ // menu-item-ui children mirror the SAME granular/chrome-only
886
+ // suppression as the three native buttons above: a control suppressed
887
+ // via no-*/chrome-only stays suppressed inside the overflow menu too,
888
+ // never resurrected there. Hide the whole trigger only once every item
889
+ // it would offer is suppressed — an empty "More" menu is worse than no
890
+ // trigger at all.
891
+ const overflow = root.querySelector('[data-toolbar-overflow]');
892
+ if (overflow) {
893
+ const hiddenByKind = { filter: filterHidden, sort: sortHidden, columns: columnsHidden };
894
+ for (const item of overflow.querySelectorAll('menu-item-ui')) {
895
+ item.hidden = !!hiddenByKind[item.getAttribute('value')];
896
+ }
897
+ overflow.hidden = filterHidden && sortHidden && columnsHidden;
423
898
  }
424
899
  }
425
900