@adia-ai/web-components 0.8.41 → 0.8.43

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 (134) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/MIGRATION.md +240 -3
  3. package/USAGE.md +4 -4
  4. package/color/index.js +1 -1
  5. package/components/action-list/action-item.a2ui.json +7 -7
  6. package/components/action-list/action-item.yaml +21 -9
  7. package/components/action-list/action-list.class.js +13 -7
  8. package/components/action-list/action-list.d.ts +2 -2
  9. package/components/button/button.yaml +1 -1
  10. package/components/calendar-picker/calendar-picker.a2ui.json +1 -1
  11. package/components/calendar-picker/calendar-picker.class.js +2 -3
  12. package/components/calendar-picker/calendar-picker.css +16 -18
  13. package/components/calendar-picker/calendar-picker.yaml +2 -2
  14. package/components/card/card.css +3 -3
  15. package/components/chart/chart.a2ui.json +8 -8
  16. package/components/chart/chart.class.js +21 -19
  17. package/components/chart/chart.d.ts +6 -6
  18. package/components/chart/chart.yaml +16 -9
  19. package/components/chart-legend/chart-legend.a2ui.json +7 -11
  20. package/components/chart-legend/chart-legend.class.js +17 -8
  21. package/components/chart-legend/chart-legend.css +5 -2
  22. package/components/chart-legend/chart-legend.d.ts +2 -2
  23. package/components/chart-legend/chart-legend.yaml +12 -8
  24. package/components/check/check.css +9 -0
  25. package/components/{color-picker/color-picker.a2ui.json → color-area/color-area.a2ui.json} +15 -12
  26. package/components/{color-picker/color-picker.class.js → color-area/color-area.class.js} +10 -8
  27. package/components/color-area/color-area.css +185 -0
  28. package/components/color-area/color-area.d.ts +66 -0
  29. package/components/color-area/color-area.examples.md +19 -0
  30. package/components/color-area/color-area.js +17 -0
  31. package/components/{color-picker/color-picker.yaml → color-area/color-area.yaml} +17 -9
  32. package/components/color-input/color-input.a2ui.json +5 -5
  33. package/components/color-input/color-input.class.js +9 -6
  34. package/components/color-input/color-input.css +1 -1
  35. package/components/color-input/color-input.js +2 -2
  36. package/components/color-input/color-input.yaml +14 -12
  37. package/components/description-list/description-list.a2ui.json +1 -1
  38. package/components/description-list/description-list.css +1 -1
  39. package/components/description-list/description-list.d.ts +1 -1
  40. package/components/description-list/description-list.yaml +1 -1
  41. package/components/drilldown/drilldown.a2ui.json +3 -0
  42. package/components/drilldown/drilldown.css +8 -1
  43. package/components/drilldown/drilldown.yaml +2 -0
  44. package/components/field/field.a2ui.json +1 -1
  45. package/components/field/field.class.js +5 -2
  46. package/components/field/field.yaml +1 -1
  47. package/components/index.js +1 -4
  48. package/components/input/input.a2ui.json +4 -4
  49. package/components/input/input.class.js +8 -0
  50. package/components/input/input.css +6 -0
  51. package/components/input/input.yaml +11 -8
  52. package/components/integration-card/integration-card.examples.md +2 -1
  53. package/components/menu/menu-item.a2ui.json +7 -7
  54. package/components/menu/menu-item.yaml +20 -7
  55. package/components/menu/menu.class.js +13 -7
  56. package/components/menu/menu.d.ts +2 -2
  57. package/components/pane/pane.a2ui.json +11 -11
  58. package/components/pane/pane.class.js +8 -4
  59. package/components/pane/pane.css +15 -10
  60. package/components/pane/pane.d.ts +10 -9
  61. package/components/pane/pane.yaml +9 -7
  62. package/components/progress/progress.a2ui.json +1 -1
  63. package/components/progress/progress.class.js +2 -2
  64. package/components/progress/progress.d.ts +1 -1
  65. package/components/progress/progress.yaml +2 -2
  66. package/components/radio/radio.css +7 -0
  67. package/components/search/search.css +10 -1
  68. package/components/segment/segment.a2ui.json +1 -2
  69. package/components/segment/segment.css +10 -0
  70. package/components/segment/segment.yaml +1 -2
  71. package/components/segmented/segmented.a2ui.json +1 -3
  72. package/components/segmented/segmented.class.js +2 -2
  73. package/components/segmented/segmented.yaml +2 -4
  74. package/components/select/select.css +6 -0
  75. package/components/step-progress/step-progress.yaml +1 -1
  76. package/components/swatch/swatch.yaml +1 -1
  77. package/components/switch/switch.css +8 -0
  78. package/components/table-toolbar/table-toolbar.a2ui.json +42 -1
  79. package/components/table-toolbar/table-toolbar.class.js +166 -14
  80. package/components/table-toolbar/table-toolbar.css +46 -0
  81. package/components/table-toolbar/table-toolbar.d.ts +13 -1
  82. package/components/table-toolbar/table-toolbar.yaml +104 -14
  83. package/components/textarea/textarea.css +4 -0
  84. package/components/time-picker/time-picker.yaml +3 -2
  85. package/components/tree/tree.yaml +1 -1
  86. package/components/upload/upload.class.js +16 -0
  87. package/components/upload/upload.d.ts +7 -0
  88. package/components/upload/upload.yaml +7 -3
  89. package/core/icons.js +3 -1
  90. package/custom-elements.json +147 -468
  91. package/dist/host.min.css +1 -1
  92. package/dist/host.sheet.js +1 -1
  93. package/dist/theme-provider.min.js +1 -1
  94. package/dist/web-components.min.css +1 -1
  95. package/dist/web-components.min.js +88 -85
  96. package/dist/web-components.sheet.js +1 -1
  97. package/index.d.ts +9 -13
  98. package/package.json +1 -1
  99. package/patterns/admin-shell/admin-shell.examples.html +2 -2
  100. package/patterns/chart-in-card/chart-in-card.examples.html +10 -10
  101. package/patterns/conversion-funnel/conversion-funnel.html +2 -2
  102. package/patterns/editor-shell/editor-shell.examples.html +3 -3
  103. package/patterns/form-system/form-system.examples.html +1 -1
  104. package/styles/colors/semantics/features.css +8 -1
  105. package/styles/components.css +1 -4
  106. package/traits/arrow-grid-nav/arrow-grid-nav.js +1 -1
  107. package/traits/resize-observer/resize-observer.js +1 -1
  108. package/components/color-picker/color-picker.css +0 -182
  109. package/components/color-picker/color-picker.d.ts +0 -99
  110. package/components/color-picker/color-picker.examples.md +0 -19
  111. package/components/color-picker/color-picker.js +0 -17
  112. package/components/datetime-picker/datetime-picker.a2ui.json +0 -256
  113. package/components/datetime-picker/datetime-picker.class.js +0 -58
  114. package/components/datetime-picker/datetime-picker.css +0 -11
  115. package/components/datetime-picker/datetime-picker.d.ts +0 -87
  116. package/components/datetime-picker/datetime-picker.examples.md +0 -25
  117. package/components/datetime-picker/datetime-picker.js +0 -17
  118. package/components/datetime-picker/datetime-picker.yaml +0 -254
  119. package/components/progress-row/progress-row.a2ui.json +0 -107
  120. package/components/progress-row/progress-row.class.js +0 -118
  121. package/components/progress-row/progress-row.css +0 -60
  122. package/components/progress-row/progress-row.d.ts +0 -24
  123. package/components/progress-row/progress-row.examples.md +0 -7
  124. package/components/progress-row/progress-row.js +0 -17
  125. package/components/progress-row/progress-row.yaml +0 -85
  126. package/components/toggle-group/toggle-group.a2ui.json +0 -148
  127. package/components/toggle-group/toggle-group.class.js +0 -195
  128. package/components/toggle-group/toggle-group.css +0 -127
  129. package/components/toggle-group/toggle-group.d.ts +0 -50
  130. package/components/toggle-group/toggle-group.examples.md +0 -33
  131. package/components/toggle-group/toggle-group.js +0 -18
  132. package/components/toggle-group/toggle-group.yaml +0 -135
  133. package/components/toggle-group/toggle-option.a2ui.json +0 -86
  134. package/components/toggle-group/toggle-option.yaml +0 -75
@@ -57,6 +57,18 @@ function popoverHead(text) {
57
57
  return el;
58
58
  }
59
59
 
60
+ // `col.label` is sometimes set to an nbsp or a bare space — the idiom
61
+ // consumers use on render-only columns (e.g. `_cancel` / `_download` /
62
+ // `_replay`) to keep the TABLE header cell blank while still rendering a
63
+ // custom cell. That whitespace is truthy, so a plain `col.label || col.key`
64
+ // fallback never engages and popover rows (filter / sort / columns) render
65
+ // with no visible text. Trim before testing so the fallback only skips when
66
+ // a label is genuinely present, never when it's intentional whitespace.
67
+ function columnLabel(col) {
68
+ const label = col.label;
69
+ return typeof label === 'string' && label.trim() ? label : col.key;
70
+ }
71
+
60
72
  const SELECT_THRESHOLD = 50;
61
73
  const SAMPLE_LIMIT = 500;
62
74
 
@@ -116,6 +128,10 @@ export class UITableToolbar extends UIElement {
116
128
  for: { type: String, default: '', reflect: true },
117
129
  text: { type: String, default: '', reflect: false },
118
130
  count: { type: String, default: '', reflect: false },
131
+ rangeStart: { type: Number, default: 0, reflect: true, attribute: 'range-start' },
132
+ rangeEnd: { type: Number, default: 0, reflect: true, attribute: 'range-end' },
133
+ rangeTotal: { type: Number, default: 0, reflect: true, attribute: 'range-total' },
134
+ pageSize: { type: Number, default: 0, reflect: true, attribute: 'page-size' },
119
135
  noFilter: { type: Boolean, default: false, reflect: true, attribute: 'no-filter' },
120
136
  noSort: { type: Boolean, default: false, reflect: true, attribute: 'no-sort' },
121
137
  noColumns: { type: Boolean, default: false, reflect: true, attribute: 'no-columns' },
@@ -137,6 +153,19 @@ export class UITableToolbar extends UIElement {
137
153
  #docListenersBound = false;
138
154
  #docListenerRaf = null;
139
155
  #sortIndicatorRafs = new Set();
156
+ #pageSizeOptions = [];
157
+
158
+ // ── Public API: pageSizeOptions ──────────────────────────────────────────
159
+ // Dynamic array prop (JS-set collection, custom setter) — same shape as
160
+ // table-ui's `columns`/`data`. Deliberately not in `static properties`
161
+ // (gh#970 — an array prop needs its own setter, not the scalar signal path).
162
+
163
+ set pageSizeOptions(arr) {
164
+ this.#pageSizeOptions = Array.isArray(arr) ? arr.map(Number).filter((n) => Number.isFinite(n)) : [];
165
+ this.#updatePageSizeSelect();
166
+ }
167
+
168
+ get pageSizeOptions() { return this.#pageSizeOptions; }
140
169
 
141
170
  // §381 (v0.6.10, slice F): one-shot dev-mode warning when consumers
142
171
  // set common-but-unknown opt-out attributes (e.g. `searchable`,
@@ -153,11 +182,37 @@ export class UITableToolbar extends UIElement {
153
182
  connected() {
154
183
  this.setAttribute('role', 'toolbar');
155
184
  this.#warnUnknownOptOutAttrs();
185
+ // gh#1615 — hydrate `page-size-options="[10,25,50]"` once at connect,
186
+ // same pattern as table-ui's `data`/`columns` JSON-array attributes
187
+ // (table.class.js connected()). A later programmatic `.pageSizeOptions
188
+ // = [...]` set still wins (guarded by the length check).
189
+ if (this.#pageSizeOptions.length === 0 && this.hasAttribute('page-size-options')) {
190
+ try {
191
+ const parsed = JSON.parse(this.getAttribute('page-size-options'));
192
+ if (Array.isArray(parsed)) this.pageSizeOptions = parsed;
193
+ } catch (_) { /* malformed JSON — leave empty, select renders hidden */ }
194
+ }
156
195
  this.#stamp();
157
196
  this.#resolveTarget();
158
197
  this.#syncFromTarget();
159
198
  }
160
199
 
200
+ // gh#1620 — this used to live in a declared `attributeChanged(name)` hook
201
+ // that the base class never calls (core/element.js only ever invokes
202
+ // `updated(changed)`, keyed by property name, not attribute name); the
203
+ // reactions below silently never fired. Migrated verbatim onto the live
204
+ // hook, keyed by the matching property names.
205
+ updated(changed) {
206
+ if (changed.has('noFilter') || changed.has('noSort')
207
+ || changed.has('noColumns') || changed.has('noSearch')) {
208
+ this.#updateControlVisibility();
209
+ }
210
+ if (changed.has('placeholder')) {
211
+ const search = this.querySelector(':scope [data-search]');
212
+ search?.setAttribute('placeholder', this.placeholder);
213
+ }
214
+ }
215
+
161
216
  disconnected() {
162
217
  if (this.#docListenerRaf != null) {
163
218
  cancelAnimationFrame(this.#docListenerRaf);
@@ -174,6 +229,8 @@ export class UITableToolbar extends UIElement {
174
229
  this.#resolveTarget();
175
230
  this.#syncFromTarget();
176
231
  this.#updateTitle();
232
+ this.#updateSummary();
233
+ this.#updatePageSizeSelect();
177
234
  }
178
235
 
179
236
  // ── Unknown opt-out attribute warning (§381, v0.6.10 slice F) ────────────
@@ -261,6 +318,20 @@ export class UITableToolbar extends UIElement {
261
318
  const root = document.createElement('div');
262
319
  root.setAttribute('data-toolbar', '');
263
320
 
321
+ // Scope slot passthrough (gh#1615) — leading region before the title
322
+ // cluster. Same real-insertion-point pattern as [slot="actions"] below:
323
+ // move any pre-existing [slot="scope"] children into a stamped wrapper
324
+ // rather than declaring a template-owned `parts:` entry (ADR-0067 — an
325
+ // author-fillable span is `slots:`, never `parts:`).
326
+ const scopeSlot = document.createElement('div');
327
+ scopeSlot.setAttribute('data-scope', '');
328
+ for (const node of [...this.children]) {
329
+ if (node === root) continue;
330
+ if (node.getAttribute?.('slot') === 'scope') {
331
+ scopeSlot.appendChild(node);
332
+ }
333
+ }
334
+
264
335
  // Title cluster
265
336
  const title = document.createElement('div');
266
337
  title.setAttribute('data-title', '');
@@ -276,6 +347,16 @@ export class UITableToolbar extends UIElement {
276
347
  badge.hidden = true;
277
348
  title.appendChild(badge);
278
349
 
350
+ // Range summary ("Showing X–Y of N", gh#1615) — its own cluster,
351
+ // independent of [data-title]: coexists with [text]/[count] rather than
352
+ // nesting inside their hide-with-children logic (ADIA2-8897).
353
+ const summary = document.createElement('div');
354
+ summary.setAttribute('data-summary', '');
355
+ summary.hidden = true;
356
+ const summaryText = document.createElement('span');
357
+ summaryText.setAttribute('data-summary-text', '');
358
+ summary.appendChild(summaryText);
359
+
279
360
  // Controls cluster
280
361
  const controls = document.createElement('div');
281
362
  controls.setAttribute('data-controls', '');
@@ -295,6 +376,19 @@ export class UITableToolbar extends UIElement {
295
376
  search.setAttribute('debounce', String(SEARCH_DEBOUNCE));
296
377
  search.addEventListener('search', this.#onSearch);
297
378
 
379
+ // Page-size select (gh#1615) — hidden until pageSizeOptions is set
380
+ // (additive opt-in, zero footprint on existing consumers). Carries the
381
+ // same `margin-inline-start: auto` as [data-search] so the trailing
382
+ // group (controls/search/page-size/actions) still pushes to the far
383
+ // edge when [no-search] drops search — without touching search's own
384
+ // existing default behavior for consumers that never set pageSizeOptions.
385
+ const pageSize = document.createElement('select-ui');
386
+ pageSize.setAttribute('data-page-size', '');
387
+ pageSize.setAttribute('size', 'sm');
388
+ pageSize.setAttribute('aria-label', 'Rows per page');
389
+ pageSize.hidden = true;
390
+ pageSize.addEventListener('change', this.#onPageSizeChange);
391
+
298
392
  // Actions slot passthrough — we move any pre-existing [slot="actions"] children here
299
393
  const actionsSlot = document.createElement('div');
300
394
  actionsSlot.setAttribute('data-actions', '');
@@ -305,14 +399,19 @@ export class UITableToolbar extends UIElement {
305
399
  }
306
400
  }
307
401
 
402
+ root.appendChild(scopeSlot);
308
403
  root.appendChild(title);
404
+ root.appendChild(summary);
309
405
  root.appendChild(controls);
310
406
  root.appendChild(search);
407
+ root.appendChild(pageSize);
311
408
  root.appendChild(actionsSlot);
312
409
 
313
410
  this.appendChild(root);
314
411
  this.#updateTitle();
412
+ this.#updateSummary();
315
413
  this.#updateControlVisibility();
414
+ this.#updatePageSizeSelect();
316
415
  }
317
416
 
318
417
  #mkPopoverAction(label, onClick) {
@@ -385,6 +484,70 @@ export class UITableToolbar extends UIElement {
385
484
  }
386
485
  }
387
486
 
487
+ // ── Range summary ("Showing X–Y of N", gh#1615) ─────────────────────────
488
+ // Independent of #updateTitle's [data-title] cluster by design — coexists
489
+ // with [text]/[count] rather than sharing their hide-with-children logic.
490
+
491
+ #updateSummary() {
492
+ const summary = this.querySelector(':scope > [data-toolbar] > [data-summary]');
493
+ if (!summary) return;
494
+ const start = Number(this.rangeStart) || 0;
495
+ const end = Number(this.rangeEnd) || 0;
496
+ const total = Number(this.rangeTotal) || 0;
497
+ if (start > 0 && end > 0 && total > 0) {
498
+ const text = summary.querySelector(':scope [data-summary-text]');
499
+ if (text) text.textContent = `Showing ${start}–${end} of ${total}`;
500
+ summary.hidden = false;
501
+ } else {
502
+ summary.hidden = true;
503
+ }
504
+ }
505
+
506
+ // ── Page-size select (gh#1615) ───────────────────────────────────────────
507
+ // Hidden whenever pageSizeOptions is empty — additive opt-in, matching the
508
+ // [slot="actions"]:empty pattern's "no footprint unless a consumer opts in".
509
+
510
+ #updatePageSizeSelect() {
511
+ const select = this.querySelector(':scope > [data-toolbar] > [data-page-size]');
512
+ if (!select) return;
513
+ const options = this.#pageSizeOptions;
514
+ if (!options.length) {
515
+ select.hidden = true;
516
+ return;
517
+ }
518
+ select.hidden = false;
519
+
520
+ // `.options = [...]` (select-ui's documented programmatic API) rather
521
+ // than appending light-DOM <option> children: select-ui's own
522
+ // #parseOptions() PARSES-THEN-REMOVES any <option>/<optgroup> children
523
+ // once connected, and only re-parses newly-appended ones via an async
524
+ // MutationObserver — appending after this element is already connected
525
+ // (which it is, by the time a later pageSizeOptions/pageSize change
526
+ // reaches this method) races that observer. `.options=` is synchronous
527
+ // and connection-state-agnostic, matching table-toolbar's own
528
+ // initial-mount-through-any-later-update determinism elsewhere.
529
+ const desired = options.map((n) => ({ value: String(n), label: `${n} / page` }));
530
+ select.options = desired;
531
+
532
+ const size = Number(this.pageSize) || 0;
533
+ const value = options.includes(size) ? size : options[0];
534
+ select.value = String(value);
535
+ }
536
+
537
+ #onPageSizeChange = (e) => {
538
+ const pageSize = Number(e.detail?.value ?? this.querySelector(':scope [data-page-size]')?.value) || 0;
539
+ this.pageSize = pageSize;
540
+ // Applied directly to the bound table's own [paginate] (rows-per-page)
541
+ // prop — the same "mirror to URL/analytics via the event, but the
542
+ // toolbar already wires the change into the target" pattern
543
+ // columns-change/sort-change already use.
544
+ if (this.#target) this.#target.paginate = pageSize;
545
+ this.dispatchEvent(new CustomEvent('page-size-change', {
546
+ bubbles: true,
547
+ detail: { pageSize },
548
+ }));
549
+ };
550
+
388
551
  #updateControlVisibility() {
389
552
  const root = this.querySelector(':scope > [data-toolbar]');
390
553
  if (!root) return;
@@ -400,17 +563,6 @@ export class UITableToolbar extends UIElement {
400
563
  setHidden('[data-search]', this.noSearch);
401
564
  }
402
565
 
403
- // Re-run on attribute changes for boolean flags.
404
- attributeChanged(name) {
405
- if (['no-filter', 'no-sort', 'no-columns', 'no-search'].includes(name)) {
406
- this.#updateControlVisibility();
407
- }
408
- if (name === 'placeholder') {
409
- const search = this.querySelector(':scope [data-search]');
410
- search?.setAttribute('placeholder', this.placeholder);
411
- }
412
- }
413
-
414
566
  // ── Search ───────────────────────────────────────────────────────────────
415
567
 
416
568
  #onSearch = (e) => {
@@ -530,7 +682,7 @@ export class UITableToolbar extends UIElement {
530
682
  const row = document.createElement('field-ui');
531
683
  row.setAttribute('data-filter-row', '');
532
684
  row.setAttribute('inline', '');
533
- row.setAttribute('label', col.label || col.key);
685
+ row.setAttribute('label', columnLabel(col));
534
686
 
535
687
  const shape = detectFilterShape(col, data);
536
688
  const current = filters[col.key];
@@ -637,7 +789,7 @@ export class UITableToolbar extends UIElement {
637
789
  const dir = dirByKey.get(col.key);
638
790
  const row = document.createElement('menu-item-ui');
639
791
  row.setAttribute('data-sort-row', '');
640
- row.setAttribute('text', col.label || col.key);
792
+ row.setAttribute('text', columnLabel(col));
641
793
  row.dataset.key = col.key;
642
794
  if (dir) row.dataset.active = dir;
643
795
 
@@ -731,7 +883,7 @@ export class UITableToolbar extends UIElement {
731
883
  const row = document.createElement('field-ui');
732
884
  row.setAttribute('data-columns-row', '');
733
885
  row.setAttribute('inline', '');
734
- row.setAttribute('label', col.label || col.key);
886
+ row.setAttribute('label', columnLabel(col));
735
887
 
736
888
  const check = document.createElement('check-ui');
737
889
  if (!col.hidden) check.setAttribute('checked', '');
@@ -18,6 +18,10 @@
18
18
  --table-toolbar-title-weight: var(--a-weight-medium);
19
19
  --table-toolbar-title-gap: var(--a-space-2);
20
20
 
21
+ /* ── Range summary ("Showing X–Y of N") ── */
22
+ --table-toolbar-summary-fg: var(--md-sys-color-neutral-on-surface-variant);
23
+ --table-toolbar-summary-size: var(--a-ui-size);
24
+
21
25
  /* ── Search ── */
22
26
  --table-toolbar-search-min: 14rem;
23
27
  --table-toolbar-search-max: 22rem;
@@ -68,6 +72,17 @@
68
72
  --table-toolbar-px: var(--a-space-4);
69
73
  }
70
74
 
75
+ /* ═══════ Scope slot ═══════ (gh#1615 — leading region before the title cluster) */
76
+
77
+ [data-scope] {
78
+ display: inline-flex;
79
+ align-items: center;
80
+ gap: var(--table-toolbar-cluster-gap);
81
+ flex: 0 0 auto;
82
+ }
83
+
84
+ [data-scope]:empty { display: none; }
85
+
71
86
  /* ═══════ Title cluster ═══════ */
72
87
 
73
88
  [data-title] {
@@ -87,6 +102,20 @@
87
102
 
88
103
  [data-toolbar-count-badge][hidden] { display: none; }
89
104
 
105
+ /* ═══════ Range summary ═══════ (gh#1615 — "Showing X–Y of N", independent of [data-title]) */
106
+
107
+ [data-summary] {
108
+ flex: 0 0 auto;
109
+ }
110
+
111
+ [data-summary][hidden] { display: none; }
112
+
113
+ [data-summary-text] {
114
+ font-size: var(--table-toolbar-summary-size);
115
+ color: var(--table-toolbar-summary-fg);
116
+ line-height: var(--a-font-leading-snug);
117
+ }
118
+
90
119
  /* ═══════ Controls cluster ═══════ */
91
120
 
92
121
  [data-controls] {
@@ -111,6 +140,23 @@
111
140
 
112
141
  [data-search][hidden] { display: none; }
113
142
 
143
+ /* ═══════ Page-size select ═══════ (gh#1615)
144
+ Carries the SAME margin-inline-start:auto as [data-search] so the
145
+ trailing group (controls/search/page-size/actions) still pushes to the
146
+ far edge when [no-search] drops search — without changing search's own
147
+ existing default for consumers that never set [page-size-options]
148
+ (the element doesn't render at all until then; see [hidden] below and
149
+ #updatePageSizeSelect). Multiple auto-margin siblings in one flex row
150
+ is safe — only the first one with unclaimed free space to its left
151
+ actually receives it. */
152
+
153
+ [data-page-size] {
154
+ flex: 0 0 auto;
155
+ margin-inline-start: auto;
156
+ }
157
+
158
+ [data-page-size][hidden] { display: none; }
159
+
114
160
  /* ═══════ Actions slot ═══════ */
115
161
 
116
162
  [data-actions] {
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `<table-toolbar-ui>` — Header / companion bar for a sibling table-ui. Renders title + count badge, filter / sort / columns popovers, and a search input — all wired to the target table via an [for] id-ref. Modeled on chart-legend-ui's [for] binding pattern. Drop next to (or above) any table-ui to add the standard data-grid toolbar without re-implementing search, filter, sort, or column visibility. Filter rows auto-pick a primitive per column: ≤ 50 distinct values → multi- select (searchable when ≥ 12 options), id-like keys → free-text contains. The column descriptor's `filter` field overrides the auto-detect: `'select'` forces multi-select even on high-cardinality columns; `'text'` forces a contains input even on small enums.
2
+ * `<table-toolbar-ui>` — Header / companion bar for a sibling table-ui. Renders an optional leading [slot="scope"] region, title + count badge, an optional "Showing X–Y of N" range summary, filter / sort / columns popovers, a search input, an optional page-size select, and a trailing [slot="actions"] region — all wired to the target table via an [for] id-ref. Modeled on chart-legend-ui's [for] binding pattern. Drop next to (or above) any table-ui to add the standard data-grid toolbar without re-implementing search, filter, sort, column visibility, or pagination summary/page-size. Filter rows auto-pick a primitive per column: ≤ 50 distinct values → multi-select (searchable when ≥ 12 options), id-like keys → free-text contains. The column descriptor's `filter` field overrides the auto-detect: `'select'` forces multi-select even on high-cardinality columns; `'text'` forces a contains input even on small enums.
3
3
  *
4
4
  * @see https://ui-kit.exe.xyz/site/components/table-toolbar
5
5
  *
@@ -14,6 +14,7 @@ import { UIElement } from '../../core/element.js';
14
14
 
15
15
  export type TableToolbarColumnsChangeEvent = CustomEvent<unknown>;
16
16
  export type TableToolbarFilterChangeEvent = CustomEvent<unknown>;
17
+ export type TableToolbarPageSizeChangeEvent = CustomEvent<unknown>;
17
18
  export type TableToolbarSearchEvent = CustomEvent<unknown>;
18
19
  export type TableToolbarSortChangeEvent = CustomEvent<unknown>;
19
20
 
@@ -30,8 +31,18 @@ export class UITableToolbar extends UIElement {
30
31
  noSearch: boolean;
31
32
  /** Hide the Sort popover button. Sort is shown by default; set to opt out. */
32
33
  noSort: boolean;
34
+ /** Current rows-per-page value, shown as the selected option in the page-size select. Falls back to the first entry of pageSizeOptions when unset (0) or not present among the options. Applied directly to the bound table's own [paginate] prop (rows-per-page), the same way filter/sort/columns changes are applied to the target — mirror this prop only to reflect state to URL / persistence / analytics. */
35
+ pageSize: number;
36
+ /** Rows-per-page choices rendered as `<option>`s in the page-size select (e.g. `[10, 25, 50, 100]`). JS property (`.pageSizeOptions = [...]`), or a declarative JSON-array `page-size-options="[10,25,50]"` attribute hydrated once at connect — the same dynamic-array pattern table-ui's own `columns`/`data` props use. Empty (the default) hides the page-size select entirely — additive opt-in, no footprint on existing consumers. */
37
+ pageSizeOptions: string;
33
38
  /** Placeholder text for the search input. */
34
39
  placeholder: string;
40
+ /** 1-indexed last row number of the current page, for the range summary (e.g. `25` in "Showing 1–25 of 320"). See rangeStart. */
41
+ rangeEnd: number;
42
+ /** 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. */
43
+ rangeStart: number;
44
+ /** 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. */
45
+ rangeTotal: number;
35
46
  /** Title text shown on the left. */
36
47
  text: string;
37
48
  /** Toolbar visual variant. `default` renders bare on parent surface; `card` adds the same chrome as a card-ui header. */
@@ -39,6 +50,7 @@ export class UITableToolbar extends UIElement {
39
50
 
40
51
  addEventListener(type: 'columns-change', listener: (ev: TableToolbarColumnsChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
41
52
  addEventListener(type: 'filter-change', listener: (ev: TableToolbarFilterChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
53
+ addEventListener(type: 'page-size-change', listener: (ev: TableToolbarPageSizeChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
42
54
  addEventListener(type: 'search', listener: (ev: TableToolbarSearchEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
43
55
  addEventListener(type: 'sort-change', listener: (ev: TableToolbarSortChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
44
56
  addEventListener<K extends keyof HTMLElementEventMap>(
@@ -6,16 +6,19 @@ component: TableToolbar
6
6
  category: agent
7
7
  version: 1
8
8
  description: >-
9
- Header / companion bar for a sibling table-ui. Renders title + count badge,
10
- filter / sort / columns popovers, and a search input all wired to the
11
- target table via an [for] id-ref. Modeled on chart-legend-ui's [for] binding
12
- pattern. Drop next to (or above) any table-ui to add the standard data-grid
13
- toolbar without re-implementing search, filter, sort, or column visibility.
14
- Filter rows auto-pick a primitive per column: 50 distinct values multi-
15
- select (searchable when 12 options), id-like keys → free-text contains. The
16
- column descriptor's `filter` field overrides the auto-detect: `'select'`
17
- forces multi-select even on high-cardinality columns; `'text'` forces a
18
- contains input even on small enums.
9
+ Header / companion bar for a sibling table-ui. Renders an optional leading
10
+ [slot="scope"] region, title + count badge, an optional "Showing X–Y of N"
11
+ range summary, filter / sort / columns popovers, a search input, an
12
+ optional page-size select, and a trailing [slot="actions"] region all
13
+ wired to the target table via an [for] id-ref. Modeled on chart-legend-ui's
14
+ [for] binding pattern. Drop next to (or above) any table-ui to add the
15
+ standard data-grid toolbar without re-implementing search, filter, sort,
16
+ column visibility, or pagination summary/page-size. Filter rows auto-pick
17
+ a primitive per column: 50 distinct values → multi-select (searchable
18
+ when 12 options), id-like keys → free-text contains. The column
19
+ descriptor's `filter` field overrides the auto-detect: `'select'` forces
20
+ multi-select even on high-cardinality columns; `'text'` forces a contains
21
+ input even on small enums.
19
22
  # Per ADR-0027 — primitives that programmatically create other primitives
20
23
  # do NOT auto-import them. Consumer (or demo shell) must explicitly import.
21
24
  composes:
@@ -43,6 +46,63 @@ props:
43
46
  description: Optional count badge value shown next to the title. When unset, falls back to the row count of the bound table.
44
47
  type: string
45
48
  default: ""
49
+ rangeStart:
50
+ description: >-
51
+ 1-indexed first row number of the current page, for the "Showing X–Y
52
+ of N" range summary (e.g. `1` in "Showing 1–25 of 320"). Renders
53
+ alongside — never in place of — [text]/[count]; the range summary is
54
+ its own cluster, independent of the title cluster. All three of
55
+ rangeStart/rangeEnd/rangeTotal must be set (> 0) for the summary to
56
+ render; omit all three (the default) to opt out entirely.
57
+ type: number
58
+ default: 0
59
+ reflect: true
60
+ attribute: range-start
61
+ rangeEnd:
62
+ description: >-
63
+ 1-indexed last row number of the current page, for the range summary
64
+ (e.g. `25` in "Showing 1–25 of 320"). See rangeStart.
65
+ type: number
66
+ default: 0
67
+ reflect: true
68
+ attribute: range-end
69
+ rangeTotal:
70
+ description: >-
71
+ Total row count across all pages, for the range summary (e.g. `320`
72
+ in "Showing 1–25 of 320"). Distinct from pagination-ui's `total`
73
+ (total PAGES) — this is total ROWS; deliberately not spelled `total`
74
+ to avoid that cross-sibling name collision (ADR-0063 B5). See
75
+ rangeStart.
76
+ type: number
77
+ default: 0
78
+ reflect: true
79
+ attribute: range-total
80
+ pageSize:
81
+ description: >-
82
+ Current rows-per-page value, shown as the selected option in the
83
+ page-size select. Falls back to the first entry of pageSizeOptions
84
+ when unset (0) or not present among the options. Applied directly to
85
+ the bound table's own [paginate] prop (rows-per-page), the same way
86
+ filter/sort/columns changes are applied to the target — mirror this
87
+ prop only to reflect state to URL / persistence / analytics.
88
+ type: number
89
+ default: 0
90
+ reflect: true
91
+ attribute: page-size
92
+ pageSizeOptions:
93
+ description: >-
94
+ Rows-per-page choices rendered as `<option>`s in the page-size select
95
+ (e.g. `[10, 25, 50, 100]`). JS property (`.pageSizeOptions = [...]`),
96
+ or a declarative JSON-array `page-size-options="[10,25,50]"` attribute
97
+ hydrated once at connect — the same dynamic-array pattern table-ui's
98
+ own `columns`/`data` props use. Empty (the default) hides the
99
+ page-size select entirely — additive opt-in, no footprint on existing
100
+ consumers.
101
+ type: array
102
+ items:
103
+ type: number
104
+ default: []
105
+ dynamic: true # JS-set collection prop with a custom setter — deliberately not in static properties
46
106
  noFilter:
47
107
  description: Hide the Filter popover button. Filter is shown by default; set to opt out.
48
108
  type: boolean
@@ -88,7 +148,11 @@ events:
88
148
  description: "Sort state changed. Detail: { sortState }."
89
149
  columns-change:
90
150
  description: "Column visibility changed. Detail: { hiddenColumns }."
151
+ page-size-change:
152
+ description: "Page-size select changed. Detail: { pageSize }."
91
153
  slots:
154
+ scope:
155
+ description: Leading region rendered BEFORE the [text]/[count] title cluster — e.g. a scope/view-switcher menu (an org's teams, a saved view). Positioning is CSS by DOM order, mirroring how [slot="actions"] is a real, author-fillable insertion point rather than template-stamped content.
92
156
  actions:
93
157
  description: Trailing action area — primary buttons (e.g. "New row") rendered after the search input.
94
158
  states:
@@ -114,6 +178,10 @@ tokens:
114
178
  description: Title font size
115
179
  --table-toolbar-title-weight:
116
180
  description: Title font weight
181
+ --table-toolbar-summary-fg:
182
+ description: Range-summary ("Showing X–Y of N") text color
183
+ --table-toolbar-summary-size:
184
+ description: Range-summary font size
117
185
  requiredIcons:
118
186
  - arrow-up
119
187
  - arrow-down
@@ -142,10 +210,19 @@ a2ui:
142
210
  when content is markup (a <span> + <badge-ui>, etc.).
143
211
  - >-
144
212
  Listen for toolbar events (`search`, `filter-change`,
145
- `sort-change`, `columns-change`) only to mirror state to URL /
146
- persistence / analytics. The toolbar already wires its changes
147
- into the bound table — you don't need to manually update the
148
- table.
213
+ `sort-change`, `columns-change`, `page-size-change`) only to mirror
214
+ state to URL / persistence / analytics. The toolbar already wires
215
+ its changes into the bound table — you don't need to manually
216
+ update the table.
217
+ - >-
218
+ Use [slot="scope"] for a leading scope/view switcher (an org's
219
+ teams, a saved view) rendered before [text]/[count]. Use
220
+ [range-start] / [range-end] / [range-total] together for a
221
+ "Showing X–Y of N" summary — it renders independently of, and
222
+ alongside, [text]/[count], never in place of them. Use [page-size]
223
+ + [page-size-options] for a rows-per-page select; it applies
224
+ directly to the bound table's [paginate] prop the same way
225
+ filter/sort/columns changes apply directly to the target.
149
226
  anti_patterns: []
150
227
  examples:
151
228
  - name: members-toolbar
@@ -158,6 +235,19 @@ examples:
158
235
  {"id": "sec", "component": "Section", "bleed": true, "children": ["tbl"]},
159
236
  {"id": "tbl", "component": "Table", "id": "members", "sortable": true, "raw": true}
160
237
  ]
238
+ - name: adiav2-list-page-toolbar-row
239
+ description: >-
240
+ The ratified adiav2 list-page toolbar row (ADIA2-9123): scope menu,
241
+ range summary, filter/columns triggers, page-size select, and a
242
+ primary CTA — search and sort dropped per that contract.
243
+ a2ui: >-
244
+ [
245
+ {"id": "root", "component": "Column", "gap": "3", "children": ["bar", "card"]},
246
+ {"id": "bar", "component": "TableToolbar", "for": "accounts", "no-sort": true, "no-search": true, "range-start": 1, "range-end": 25, "range-total": 320, "page-size": 25, "page-size-options": [10, 25, 50, 100]},
247
+ {"id": "card", "component": "Card", "children": ["sec"]},
248
+ {"id": "sec", "component": "Section", "bleed": true, "children": ["tbl"]},
249
+ {"id": "tbl", "component": "Table", "id": "accounts", "sortable": true, "raw": true}
250
+ ]
161
251
  keywords:
162
252
  - table-toolbar
163
253
  - data-grid
@@ -37,6 +37,9 @@ textarea-ui:not([disabled]) [slot="text"]:hover {
37
37
  --textarea-label-fg-focus: var(--md-sys-color-neutral-on-surface-variant);
38
38
  --textarea-bg-disabled: var(--a-ui-bg-disabled);
39
39
  --textarea-fg-disabled: var(--a-ui-text-disabled);
40
+ /* Same gap as gh#1595 (input-ui) — rides along here since it's the
41
+ identical shared-field pattern, same root-cause class. */
42
+ --textarea-border-disabled: var(--a-ui-border-disabled);
40
43
  text-align: start; /* §text-align-reset — blocks inheritance from centered ancestors */
41
44
  }
42
45
 
@@ -124,6 +127,7 @@ textarea-ui:not([disabled]) [slot="text"]:hover {
124
127
  :scope[disabled] [slot="text"] {
125
128
  background: var(--textarea-bg-disabled);
126
129
  color: var(--textarea-fg-disabled);
130
+ border-color: var(--textarea-border-disabled);
127
131
  cursor: not-allowed;
128
132
  }
129
133
  }
@@ -175,8 +175,9 @@ a2ui:
175
175
  (`h12` / `h23`) when the surface needs a specific cycle (cron
176
176
  editors, log queries, system surfaces).
177
177
  - >-
178
- For datetime selection use `<datetime-picker-ui>` (SPEC-038) —
179
- it composes this primitive as its time pane.
178
+ For datetime selection use `<calendar-picker-ui precision="minute">`
179
+ (SPEC-038; the former `<datetime-picker-ui>` thin alias was cut in
180
+ 0.8.43, gh#1617) — it composes this primitive as its time pane.
180
181
  anti_patterns:
181
182
  - wrong: |
182
183
  {"component": "TimePicker", "value": "9:30 AM"}
@@ -108,7 +108,7 @@ a2ui:
108
108
  for one-level collapsible groups use <accordion-ui>.
109
109
  - >-
110
110
  Canonical mount: inside <editor-sidebar slot="leading"> →
111
- <pane-ui side="leading" resizable> → <section> → <tree-ui id="…">
111
+ <pane-ui edge="leading" resizable> → <section> → <tree-ui id="…">
112
112
  as the structure / navigator pane of the three-pane editor shell.
113
113
  Pair with a <header> in the same pane (e.g. "Structure",
114
114
  "Layers", "Files").
@@ -93,6 +93,22 @@ export class UIUpload extends UIFormElement {
93
93
  return this.#files;
94
94
  }
95
95
 
96
+ /**
97
+ * Empty the selected file list and return the dropzone to idle — the
98
+ * public counterpart to `open()` (gh#1605). A consumer's own Clear/Reset
99
+ * action can call this without reaching into `[data-dropzone]`'s internal
100
+ * anatomy, which the yaml spec forbids.
101
+ */
102
+ clear() {
103
+ this.#setFiles([]);
104
+ }
105
+
106
+ // ── Form lifecycle ──
107
+
108
+ onFormReset() {
109
+ this.clear();
110
+ }
111
+
96
112
  // ── File picking ──
97
113
 
98
114
  /**
@@ -40,6 +40,13 @@ export class UIUpload extends UIFormElement {
40
40
  */
41
41
  open(): Promise<void>;
42
42
 
43
+ /**
44
+ * gh#1605: empty the selected file list and return the dropzone to idle
45
+ * — the public counterpart to `open()`. Also runs automatically on
46
+ * native form reset (the element is form-associated).
47
+ */
48
+ clear(): void;
49
+
43
50
  addEventListener<K extends keyof HTMLElementEventMap>(
44
51
  type: K,
45
52
  listener: (this: UIUpload, ev: HTMLElementEventMap[K]) => unknown,