@adia-ai/web-components 0.8.43 → 0.8.45
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.
- package/CHANGELOG.md +54 -0
- package/components/avatar/avatar.class.js +12 -1
- package/components/button/button.d.ts +4 -0
- package/components/chart/chart.a2ui.json +26 -0
- package/components/chart/chart.class.js +1304 -222
- package/components/chart/chart.css +151 -1
- package/components/chart/chart.d.ts +6 -0
- package/components/chart/chart.yaml +57 -0
- package/components/chart-legend/chart-legend.a2ui.json +17 -2
- package/components/chart-legend/chart-legend.class.js +69 -4
- package/components/chart-legend/chart-legend.css +65 -1
- package/components/chart-legend/chart-legend.d.ts +4 -2
- package/components/chart-legend/chart-legend.examples.md +4 -4
- package/components/chart-legend/chart-legend.yaml +43 -3
- package/components/combobox/combobox.a2ui.json +3 -0
- package/components/combobox/combobox.class.js +9 -8
- package/components/combobox/combobox.css +25 -4
- package/components/combobox/combobox.yaml +6 -0
- package/components/command/command.css +17 -1
- package/components/context-menu/context-menu.a2ui.json +8 -3
- package/components/context-menu/context-menu.class.js +46 -5
- package/components/context-menu/context-menu.d.ts +8 -3
- package/components/context-menu/context-menu.examples.md +2 -2
- package/components/context-menu/context-menu.yaml +22 -5
- package/components/heatmap/heatmap.a2ui.json +5 -0
- package/components/heatmap/heatmap.d.ts +2 -0
- package/components/heatmap/heatmap.yaml +8 -0
- package/components/input/input.a2ui.json +2 -2
- package/components/input/input.class.js +12 -7
- package/components/input/input.yaml +6 -5
- package/components/integration-card/integration-card.d.ts +2 -0
- package/components/nav/nav.a2ui.json +2 -2
- package/components/nav/nav.css +1 -1
- package/components/nav/nav.d.ts +1 -1
- package/components/nav/nav.yaml +14 -3
- package/components/nav-group/nav-group.css +37 -3
- package/components/noodles/noodles.a2ui.json +0 -15
- package/components/noodles/noodles.yaml +0 -12
- package/components/number-format/number-format.class.js +10 -0
- package/components/pagination/pagination.class.js +140 -26
- package/components/popover/popover.d.ts +2 -0
- package/components/progress/progress.class.js +11 -6
- package/components/range/range.class.js +9 -1
- package/components/search/search.class.js +39 -5
- package/components/select/select.a2ui.json +10 -1
- package/components/select/select.class.js +34 -14
- package/components/select/select.css +39 -1
- package/components/select/select.d.ts +2 -0
- package/components/select/select.yaml +19 -1
- package/components/stream/stream.d.ts +2 -0
- package/components/table/cell-types.js +9 -0
- package/components/table/table.a2ui.json +6 -1
- package/components/table/table.class.js +265 -34
- package/components/table/table.css +38 -10
- package/components/table/table.d.ts +3 -1
- package/components/table/table.examples.md +2 -2
- package/components/table/table.yaml +15 -2
- package/components/table-toolbar/table-toolbar.a2ui.json +43 -4
- package/components/table-toolbar/table-toolbar.class.js +407 -28
- package/components/table-toolbar/table-toolbar.css +299 -0
- package/components/table-toolbar/table-toolbar.d.ts +15 -1
- package/components/table-toolbar/table-toolbar.yaml +162 -15
- package/components/tag/tag.class.js +13 -1
- package/components/text/text.a2ui.json +3 -3
- package/components/text/text.d.ts +5 -3
- package/components/text/text.yaml +3 -3
- package/components/textarea/textarea.class.js +9 -1
- package/components/tour/tour.d.ts +10 -0
- package/core/a11y.d.ts +22 -0
- package/core/a11y.js +64 -0
- package/core/anchor.js +9 -5
- package/core/data-stream.js +37 -2
- package/core/element.js +1 -1
- package/core/index.d.ts +1 -0
- package/core/index.js +1 -0
- package/core/provider.d.ts +9 -13
- package/core/provider.js +9 -113
- package/core/store.d.ts +46 -0
- package/core/store.js +89 -0
- package/custom-elements.json +127 -33
- package/dist/host.min.css +1 -1
- package/dist/host.sheet.js +1 -1
- package/dist/theme-provider.min.js +1 -1
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +119 -119
- package/dist/web-components.sheet.js +1 -1
- package/package.json +1 -1
- package/patterns/access-requests/access-requests.examples.html +1 -1
- package/patterns/admin-keys-and-export/admin-keys-and-export.examples.html +7 -7
- package/patterns/agent-cost/agent-cost.examples.html +6 -6
- package/patterns/agent-memory/agent-memory.examples.html +3 -3
- package/patterns/agent-prompt-library/agent-prompt-library.examples.html +8 -8
- package/patterns/agent-tool-call/agent-tool-call.examples.html +1 -1
- package/patterns/approvals/approvals.examples.html +6 -6
- package/patterns/audit-log/audit-log.examples.html +1 -1
- package/patterns/bulk-action-toolbar/bulk-action-toolbar.examples.html +4 -4
- package/patterns/chart-in-card/chart-in-card.examples.html +114 -0
- package/patterns/chart-in-card/chart-in-card.examples.js +37 -0
- package/patterns/comments-and-collaboration/comments-and-collaboration.examples.html +5 -5
- package/patterns/data-tables-inline-edit-and-tree/data-tables-inline-edit-and-tree.examples.html +2 -2
- package/patterns/diff-review/diff-review.examples.html +6 -6
- package/patterns/export-flow/export-flow.examples.html +2 -2
- package/patterns/filter-bar/filter-bar.examples.html +2 -2
- package/patterns/forms-conditional-and-autocomplete/forms-conditional-and-autocomplete.examples.html +2 -2
- package/patterns/inline-dialog/inline-dialog.examples.html +2 -2
- package/patterns/marketing-engagement/marketing-engagement.examples.html +4 -4
- package/patterns/notifications-bell-and-digest/notifications-bell-and-digest.examples.html +1 -1
- package/patterns/permissions-matrix/permissions-matrix.examples.html +1 -1
- package/patterns/permissions-sharing/permissions-sharing.examples.html +2 -2
- package/patterns/profile-public-and-verification/profile-public-and-verification.examples.html +4 -4
- package/patterns/search-discovery/search-discovery.examples.html +2 -2
- package/styles/api/sizing.css +46 -0
- package/styles/type/roles.css +29 -9
|
@@ -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"]
|
|
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
|
|
@@ -27,18 +29,43 @@
|
|
|
27
29
|
* Popovers use the platform Popover API + core/anchor.js, the same primitives
|
|
28
30
|
* that menu-ui / popover-ui / toolbar-ui already use in this package.
|
|
29
31
|
*
|
|
30
|
-
* State flow
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* State flow — events-only interaction contract (gh#1764/#1780, ADR-0079;
|
|
33
|
+
* mirrors chart-legend-ui↔chart-ui's own bubbling-CustomEvent model). The
|
|
34
|
+
* toolbar never writes a property or calls a method on the bound table
|
|
35
|
+
* directly — every command is a `toolbar-*` CustomEvent dispatched AT the
|
|
36
|
+
* resolved target, which table.class.js listens for in its own
|
|
37
|
+
* connected()/disconnected() and applies via its existing public surface:
|
|
38
|
+
* search → `toolbar-search` { value } → table.search =
|
|
39
|
+
* filters → `toolbar-filter-set` { key, value, op } → table.setFilter()
|
|
40
|
+
* filters clear → `toolbar-filter-clear` {} → table.clearFilters()
|
|
41
|
+
* sort → simulated click on table's [data-sort-key] header (already
|
|
42
|
+
* event-shaped — a real DOM event dispatched at the target,
|
|
43
|
+
* not a property write; left as-is, see gh#1780's PR notes)
|
|
44
|
+
* column hidden → `toolbar-columns-set` { columns } → table.columns =
|
|
45
|
+
* page size → `toolbar-paginate` { pageSize } → table.paginate =
|
|
46
|
+
* The table→toolbar direction (`sort`, `filter-change` listened on #target)
|
|
47
|
+
* was already events-only and is unchanged.
|
|
35
48
|
*/
|
|
36
49
|
|
|
37
50
|
import { UIElement } from '../../core/element.js';
|
|
38
|
-
import { anchorPopover } from '../../core/anchor.js';
|
|
51
|
+
import { anchorPopover, resolveLengthPx } from '../../core/anchor.js';
|
|
39
52
|
|
|
40
53
|
const SEARCH_DEBOUNCE = 200;
|
|
41
54
|
|
|
55
|
+
// ADR-0076 REQ-M-006 — the three breakpoints' NORMATIVE default pixel
|
|
56
|
+
// widths (56rem/40rem/26rem at the standard 16px root), duplicated here
|
|
57
|
+
// ONLY as a fallback for #classifyStage() below when the live
|
|
58
|
+
// `--table-toolbar-bp-*` custom property can't be read (no table-toolbar.css
|
|
59
|
+
// stylesheet attached yet, an SSR shim, or a unit-test harness with no real
|
|
60
|
+
// cascade) — resolveLengthPx() returns NaN in that case, `||` falls
|
|
61
|
+
// through to these. Table-toolbar.css's own `:where(:scope)` declarations
|
|
62
|
+
// are the actual source of truth whenever a real stylesheet IS present;
|
|
63
|
+
// this fallback exists so the reflection degrades to the DOCUMENTED
|
|
64
|
+
// default instead of silently misclassifying against NaN.
|
|
65
|
+
const DEFAULT_BP_SEARCH_TIGHT_PX = 56 * 16;
|
|
66
|
+
const DEFAULT_BP_ICON_ONLY_PX = 40 * 16;
|
|
67
|
+
const DEFAULT_BP_OVERFLOW_PX = 26 * 16;
|
|
68
|
+
|
|
42
69
|
function emptyHint(text) {
|
|
43
70
|
const el = document.createElement('text-ui');
|
|
44
71
|
el.setAttribute('data-popover-empty', '');
|
|
@@ -136,14 +163,28 @@ export class UITableToolbar extends UIElement {
|
|
|
136
163
|
noSort: { type: Boolean, default: false, reflect: true, attribute: 'no-sort' },
|
|
137
164
|
noColumns: { type: Boolean, default: false, reflect: true, attribute: 'no-columns' },
|
|
138
165
|
noSearch: { type: Boolean, default: false, reflect: true, attribute: 'no-search' },
|
|
166
|
+
// ADR-0076 (ADIA2-9123) — additive sugar over the four `no-*` attributes
|
|
167
|
+
// above. Pure, absolute OR: see #updateControlVisibility(), which is the
|
|
168
|
+
// single place that combines chromeOnly with each granular no-* value.
|
|
169
|
+
chromeOnly: { type: Boolean, default: false, reflect: true, attribute: 'chrome-only' },
|
|
139
170
|
placeholder: { type: String, default: 'Search...', reflect: false },
|
|
140
171
|
variant: { type: String, default: 'default', reflect: true },
|
|
172
|
+
// ADR-0076 REQ-O-001 — explicit compaction-stage override. Unset (the
|
|
173
|
+
// default, empty string) means auto-snap via the @container query
|
|
174
|
+
// (table-toolbar.css's :scope container-type/container-name). All four
|
|
175
|
+
// stages' CSS is now live: search-tight steps the search field down
|
|
176
|
+
// (step 2); icon-only drops control labels and compacts/hides the
|
|
177
|
+
// range summary (steps 3+4); overflow collapses Filter/Sort/Columns
|
|
178
|
+
// into one "More" trigger (step 5).
|
|
179
|
+
stage: { type: String, default: '', reflect: true },
|
|
141
180
|
};
|
|
142
181
|
|
|
143
182
|
// §205 (v0.5.7): dynamic sort-indicator icons (table-toolbar.class.js:576 — nested ternary
|
|
144
183
|
// `dir === 'asc' ? 'arrow-up' : dir === 'desc' ? 'arrow-down' : 'caret-up-down'`).
|
|
145
184
|
// Per FEEDBACK-16 §1 + §209 slot-11 ternary-walker discovery.
|
|
146
|
-
|
|
185
|
+
// 'dots-three-vertical' — the overflow-stage "More" trigger icon
|
|
186
|
+
// (ADR-0076 REQ-S-004, PLAN step 5, #mkOverflowTrigger()).
|
|
187
|
+
static requiredIcons = ['arrow-up', 'arrow-down', 'caret-up-down', 'dots-three-vertical'];
|
|
147
188
|
|
|
148
189
|
static template = () => null;
|
|
149
190
|
|
|
@@ -154,6 +195,17 @@ export class UITableToolbar extends UIElement {
|
|
|
154
195
|
#docListenerRaf = null;
|
|
155
196
|
#sortIndicatorRafs = new Set();
|
|
156
197
|
#pageSizeOptions = [];
|
|
198
|
+
// ADR-0076 REQ-M-005 (ADIA2-9123 S4-ii), PLAN step 7 — data-stage-resolved
|
|
199
|
+
// reflection. #resizeObs classifies live width against the SAME three
|
|
200
|
+
// breakpoints table-toolbar.css's @container rules use (read from the
|
|
201
|
+
// custom properties themselves, never a second hardcoded copy of
|
|
202
|
+
// 56/40/26 — see #classifyStage()). #lastWidthPx caches the last
|
|
203
|
+
// measured inline-size so a `stage` attribute change alone (no resize)
|
|
204
|
+
// can still re-classify without waiting on the next ResizeObserver
|
|
205
|
+
// callback (REQ-M-005's dual-trigger requirement).
|
|
206
|
+
#resizeObs = null;
|
|
207
|
+
#lastWidthPx = null;
|
|
208
|
+
#stageRaf = null;
|
|
157
209
|
|
|
158
210
|
// ── Public API: pageSizeOptions ──────────────────────────────────────────
|
|
159
211
|
// Dynamic array prop (JS-set collection, custom setter) — same shape as
|
|
@@ -195,6 +247,34 @@ export class UITableToolbar extends UIElement {
|
|
|
195
247
|
this.#stamp();
|
|
196
248
|
this.#resolveTarget();
|
|
197
249
|
this.#syncFromTarget();
|
|
250
|
+
|
|
251
|
+
// ADR-0076 REQ-M-005, PLAN step 7 — classify synchronously off
|
|
252
|
+
// `clientWidth` at connect time too (0 pre-layout treated as "no
|
|
253
|
+
// measurement yet", the same `null`-width fallback #reflectResolvedStage
|
|
254
|
+
// uses) rather than waiting on the ResizeObserver's own first
|
|
255
|
+
// (asynchronous) callback — avoids a brief connect-time gap with no
|
|
256
|
+
// `data-stage-resolved` reflected at all, mirroring how chart.class.js's
|
|
257
|
+
// own `data-ratio-resolved` is set synchronously from `#dims()` at
|
|
258
|
+
// first render(), not deferred to its ResizeObserver's first tick.
|
|
259
|
+
this.#lastWidthPx = this.clientWidth || null;
|
|
260
|
+
this.#reflectResolvedStage();
|
|
261
|
+
|
|
262
|
+
// gh#285 SSR DOM shims (linkedom) have no ResizeObserver global, same
|
|
263
|
+
// guard chart.class.js's own ResizeObserver setup uses.
|
|
264
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
265
|
+
this.#resizeObs = new ResizeObserver((entries) => {
|
|
266
|
+
const { inlineSize } = entries[0].contentBoxSize[0];
|
|
267
|
+
this.#lastWidthPx = inlineSize;
|
|
268
|
+
// rAF-batch, mirroring chart.class.js's own ResizeObserver setup —
|
|
269
|
+
// avoids a same-tick reflect→layout→observer-refire loop.
|
|
270
|
+
if (this.#stageRaf) return;
|
|
271
|
+
this.#stageRaf = requestAnimationFrame(() => {
|
|
272
|
+
this.#stageRaf = null;
|
|
273
|
+
this.#reflectResolvedStage();
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
this.#resizeObs.observe(this);
|
|
277
|
+
}
|
|
198
278
|
}
|
|
199
279
|
|
|
200
280
|
// gh#1620 — this used to live in a declared `attributeChanged(name)` hook
|
|
@@ -204,7 +284,8 @@ export class UITableToolbar extends UIElement {
|
|
|
204
284
|
// hook, keyed by the matching property names.
|
|
205
285
|
updated(changed) {
|
|
206
286
|
if (changed.has('noFilter') || changed.has('noSort')
|
|
207
|
-
|| changed.has('noColumns') || changed.has('noSearch')
|
|
287
|
+
|| changed.has('noColumns') || changed.has('noSearch')
|
|
288
|
+
|| changed.has('chromeOnly')) {
|
|
208
289
|
this.#updateControlVisibility();
|
|
209
290
|
}
|
|
210
291
|
if (changed.has('placeholder')) {
|
|
@@ -213,6 +294,30 @@ export class UITableToolbar extends UIElement {
|
|
|
213
294
|
}
|
|
214
295
|
}
|
|
215
296
|
|
|
297
|
+
// ADR-0076 REQ-E-001 — `updated(changed)` alone MISSES a live loading→
|
|
298
|
+
// confirmed-zero transition: `rangeTotal`'s default is already 0, so
|
|
299
|
+
// setting `range-total="0"` from an unset start parses to the SAME
|
|
300
|
+
// Number(0), the base class's property setter's `Object.is` short-circuit
|
|
301
|
+
// swallows the signal write (core/element.js `installProps()`), and
|
|
302
|
+
// `changed` never gets a `rangeTotal` entry — `#updateSummary()`'s
|
|
303
|
+
// hasAttribute-based branch never re-runs. Verified live in a browser
|
|
304
|
+
// (not just the vitest suite, which happened to only exercise the
|
|
305
|
+
// positive→zero transition, where the signal DOES change). REQ-E-001's
|
|
306
|
+
// check is fundamentally an ATTRIBUTE-presence concern, not a coerced-
|
|
307
|
+
// value one, so it needs the raw attribute callback, not the signal diff.
|
|
308
|
+
attributeChangedCallback(name, oldVal, newVal) {
|
|
309
|
+
super.attributeChangedCallback(name, oldVal, newVal);
|
|
310
|
+
if (name === 'range-total') this.#updateSummary();
|
|
311
|
+
// ADR-0076 REQ-M-005 — the SECOND of the two triggers `data-stage-
|
|
312
|
+
// resolved` must react to: a live resize (the ResizeObserver callback
|
|
313
|
+
// above) AND a `stage` attribute change alone, with no resize needed.
|
|
314
|
+
// Re-classifies against the cached last-measured width — a pin set
|
|
315
|
+
// before the first resize observation (#lastWidthPx still null) simply
|
|
316
|
+
// reports the pin; classifyStage() below treats a null width as "no
|
|
317
|
+
// live width to compare against yet", never a false 0-width match.
|
|
318
|
+
if (name === 'stage') this.#reflectResolvedStage();
|
|
319
|
+
}
|
|
320
|
+
|
|
216
321
|
disconnected() {
|
|
217
322
|
if (this.#docListenerRaf != null) {
|
|
218
323
|
cancelAnimationFrame(this.#docListenerRaf);
|
|
@@ -222,6 +327,68 @@ export class UITableToolbar extends UIElement {
|
|
|
222
327
|
this.#sortIndicatorRafs.clear();
|
|
223
328
|
this.#closePopover();
|
|
224
329
|
this.#detachTarget();
|
|
330
|
+
if (this.#stageRaf != null) {
|
|
331
|
+
cancelAnimationFrame(this.#stageRaf);
|
|
332
|
+
this.#stageRaf = null;
|
|
333
|
+
}
|
|
334
|
+
this.#resizeObs?.disconnect();
|
|
335
|
+
this.#resizeObs = null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ── data-stage-resolved reflection (ADR-0076 REQ-M-005, ADIA2-9123 ─────────
|
|
339
|
+
// S4-ii, PLAN step 7) ────────────────────────────────────────────────────
|
|
340
|
+
//
|
|
341
|
+
// Purely informational — mirrors the CSS-decided stage for a consumer's
|
|
342
|
+
// own [slot="actions-leading"] content, never drives table-toolbar's own
|
|
343
|
+
// rendering (REQ-S stays 100% CSS `@container`-driven, REQ-M-002). The
|
|
344
|
+
// three breakpoints are read live off the SAME custom properties the CSS
|
|
345
|
+
// `@container` rules consume (--table-toolbar-bp-search-tight/-icon-only/
|
|
346
|
+
// -overflow) via `resolveLengthPx` (core/anchor.js, shared with its
|
|
347
|
+
// --popover-max-height use) — one source of truth, never a second
|
|
348
|
+
// hardcoded copy of 56/40/26 here.
|
|
349
|
+
|
|
350
|
+
#classifyStage(widthPx) {
|
|
351
|
+
const cs = getComputedStyle(this);
|
|
352
|
+
// resolveLengthPx() returns NaN when the custom property is unreadable
|
|
353
|
+
// — no table-toolbar.css stylesheet attached yet, an SSR shim, or a
|
|
354
|
+
// headless unit-test harness with no real cascade — so an explicit
|
|
355
|
+
// Number.isFinite() check falls through to each breakpoint's own
|
|
356
|
+
// documented REQ-M-006 default. This must be a real NaN check, not a
|
|
357
|
+
// `|| DEFAULT` fallback: `0` is a legitimate consumer override (e.g.
|
|
358
|
+
// `--table-toolbar-bp-overflow: 0rem` to disable the overflow stage
|
|
359
|
+
// entirely) and `||` would silently discard it as falsy, substituting
|
|
360
|
+
// the default instead — reviewer finding on PR #1676, fixed here.
|
|
361
|
+
// Whenever a real stylesheet IS present, the live custom property
|
|
362
|
+
// (overridable per-consumer, same posture as every other
|
|
363
|
+
// --table-toolbar-* token) wins, zero included.
|
|
364
|
+
const resolveBp = (raw, fallback) => {
|
|
365
|
+
const v = resolveLengthPx(raw);
|
|
366
|
+
return Number.isFinite(v) ? v : fallback;
|
|
367
|
+
};
|
|
368
|
+
const searchTightPx = resolveBp(cs.getPropertyValue('--table-toolbar-bp-search-tight'), DEFAULT_BP_SEARCH_TIGHT_PX);
|
|
369
|
+
const iconOnlyPx = resolveBp(cs.getPropertyValue('--table-toolbar-bp-icon-only'), DEFAULT_BP_ICON_ONLY_PX);
|
|
370
|
+
const overflowPx = resolveBp(cs.getPropertyValue('--table-toolbar-bp-overflow'), DEFAULT_BP_OVERFLOW_PX);
|
|
371
|
+
|
|
372
|
+
// Inclusive-on-the-narrower-side (max-width semantics, REQ-M-006) — a
|
|
373
|
+
// width exactly AT a boundary classifies to the narrower stage, same
|
|
374
|
+
// rule the CSS `@container (max-width: …)` conditions already encode.
|
|
375
|
+
if (widthPx <= overflowPx) return 'overflow';
|
|
376
|
+
if (widthPx <= iconOnlyPx) return 'icon-only';
|
|
377
|
+
if (widthPx <= searchTightPx) return 'search-tight';
|
|
378
|
+
return 'full';
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
#reflectResolvedStage() {
|
|
382
|
+
// REQ-O-001/REQ-M-005 — the pin wins for the reflection exactly as it
|
|
383
|
+
// wins for rendering: an explicit `stage` reports itself, never the
|
|
384
|
+
// width-derived classification, even inside a container sized for a
|
|
385
|
+
// different stage.
|
|
386
|
+
const resolved = this.stage || (this.#lastWidthPx == null ? 'full' : this.#classifyStage(this.#lastWidthPx));
|
|
387
|
+
// Is-different guard — same shape as chart.class.js's own
|
|
388
|
+
// data-ratio-resolved reflection (chart.class.js:533).
|
|
389
|
+
if (this.getAttribute('data-stage-resolved') !== resolved) {
|
|
390
|
+
this.setAttribute('data-stage-resolved', resolved);
|
|
391
|
+
}
|
|
225
392
|
}
|
|
226
393
|
|
|
227
394
|
render() {
|
|
@@ -357,12 +524,51 @@ export class UITableToolbar extends UIElement {
|
|
|
357
524
|
summaryText.setAttribute('data-summary-text', '');
|
|
358
525
|
summary.appendChild(summaryText);
|
|
359
526
|
|
|
527
|
+
// ADR-0076 REQ-S-003 — short-form compact text ("1–25/320"), a sibling
|
|
528
|
+
// of [data-summary-text] holding the SAME range values in a terser
|
|
529
|
+
// shape. Which of the two is visible is decided entirely by CSS
|
|
530
|
+
// (table-toolbar.css's icon-only @container rule / [stage] pin) —
|
|
531
|
+
// both nodes are always populated together in #updateSummary(), never
|
|
532
|
+
// a JS branch keyed on a resolved-stage signal (REQ-M-002 stays true:
|
|
533
|
+
// no JS decides rendering here, only which text each stage's CSS shows).
|
|
534
|
+
const summaryCompact = document.createElement('span');
|
|
535
|
+
summaryCompact.setAttribute('data-summary-compact', '');
|
|
536
|
+
summaryCompact.hidden = true;
|
|
537
|
+
summary.appendChild(summaryCompact);
|
|
538
|
+
|
|
539
|
+
// Empty-state slot (ADR-0076 REQ-E, adiav2 ADIA2-9123) — renders in
|
|
540
|
+
// [data-summary]'s own position, alongside [data-summary-text], never a
|
|
541
|
+
// separate region: only one of the two is ever visible at a time (see
|
|
542
|
+
// #updateSummary()). Same real-insertion-point pattern as [slot="scope"]
|
|
543
|
+
// / [slot="actions"] / [slot="actions-leading"] above — move any
|
|
544
|
+
// pre-existing [slot="empty"] children into this stamped wrapper rather
|
|
545
|
+
// than declaring a template-owned `parts:` entry (ADR-0067).
|
|
546
|
+
const summaryEmpty = document.createElement('div');
|
|
547
|
+
summaryEmpty.setAttribute('data-summary-empty', '');
|
|
548
|
+
summaryEmpty.hidden = true;
|
|
549
|
+
for (const node of [...this.children]) {
|
|
550
|
+
if (node === root) continue;
|
|
551
|
+
if (node.getAttribute?.('slot') === 'empty') {
|
|
552
|
+
summaryEmpty.appendChild(node);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
summary.appendChild(summaryEmpty);
|
|
556
|
+
|
|
360
557
|
// Controls cluster
|
|
361
558
|
const controls = document.createElement('div');
|
|
362
559
|
controls.setAttribute('data-controls', '');
|
|
363
560
|
controls.appendChild(this.#mkButton('filter', 'Filter', 'funnel-simple'));
|
|
364
561
|
controls.appendChild(this.#mkButton('sort', 'Sort', 'arrows-down-up'));
|
|
365
562
|
controls.appendChild(this.#mkButton('columns', 'Columns', 'columns'));
|
|
563
|
+
// ADR-0076 REQ-S-004/REQ-X-003, PLAN step 5 — overflow-stage "More"
|
|
564
|
+
// trigger. Stamped unconditionally alongside the three granular
|
|
565
|
+
// buttons above (never removed/re-created), CSS-hidden except at the
|
|
566
|
+
// overflow breakpoint/pin (REQ-M-002 stays true: which cluster paints
|
|
567
|
+
// is a pure CSS decision, not a JS branch). Keeping all four buttons
|
|
568
|
+
// permanently in the DOM is the simplest way to honor REQ-X-004's
|
|
569
|
+
// tab-order guarantee — nothing is ever created/destroyed across a
|
|
570
|
+
// stage transition, only shown/hidden.
|
|
571
|
+
controls.appendChild(this.#mkOverflowTrigger());
|
|
366
572
|
|
|
367
573
|
// Search — compose <search-ui>, which already stamps input-ui with
|
|
368
574
|
// the magnifying-glass prefix + clear suffix and debounces a `search`
|
|
@@ -389,6 +595,25 @@ export class UITableToolbar extends UIElement {
|
|
|
389
595
|
pageSize.hidden = true;
|
|
390
596
|
pageSize.addEventListener('change', this.#onPageSizeChange);
|
|
391
597
|
|
|
598
|
+
// Actions-leading slot passthrough (gh#1649) — a real insertion point
|
|
599
|
+
// (ADR-0067), same pattern as [slot="scope"]/[slot="actions"] above,
|
|
600
|
+
// rendered BEFORE the page-size select rather than after it: app-owned
|
|
601
|
+
// trigger buttons (e.g. custom Filter/Columns triggers that open the
|
|
602
|
+
// consumer's own drawer instead of the native filter/columns popovers)
|
|
603
|
+
// need to land ahead of page-size, which [slot="actions"] alone can't
|
|
604
|
+
// reach since it renders trailing (after page-size).
|
|
605
|
+
const actionsLeadingSlot = document.createElement('div');
|
|
606
|
+
actionsLeadingSlot.setAttribute('data-actions-leading', '');
|
|
607
|
+
// Move any pre-existing [slot="actions-leading"] children into this
|
|
608
|
+
// stamped wrapper — same real-insertion-point pattern as [slot="scope"]
|
|
609
|
+
// / [slot="actions"] above.
|
|
610
|
+
for (const node of [...this.children]) {
|
|
611
|
+
if (node === root) continue;
|
|
612
|
+
if (node.getAttribute?.('slot') === 'actions-leading') {
|
|
613
|
+
actionsLeadingSlot.appendChild(node);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
392
617
|
// Actions slot passthrough — we move any pre-existing [slot="actions"] children here
|
|
393
618
|
const actionsSlot = document.createElement('div');
|
|
394
619
|
actionsSlot.setAttribute('data-actions', '');
|
|
@@ -404,6 +629,7 @@ export class UITableToolbar extends UIElement {
|
|
|
404
629
|
root.appendChild(summary);
|
|
405
630
|
root.appendChild(controls);
|
|
406
631
|
root.appendChild(search);
|
|
632
|
+
root.appendChild(actionsLeadingSlot);
|
|
407
633
|
root.appendChild(pageSize);
|
|
408
634
|
root.appendChild(actionsSlot);
|
|
409
635
|
|
|
@@ -433,6 +659,14 @@ export class UITableToolbar extends UIElement {
|
|
|
433
659
|
btn.setAttribute('variant', 'outline');
|
|
434
660
|
btn.setAttribute('size', 'sm');
|
|
435
661
|
btn.setAttribute('aria-haspopup', 'menu');
|
|
662
|
+
// ADR-0076 REQ-X-002 — unconditional (harmless at full-text stages,
|
|
663
|
+
// becomes the hover-tooltip affordance at the icon-only stage once
|
|
664
|
+
// the rendered label is CSS-hidden, REQ-M-003). button-ui's own
|
|
665
|
+
// title→aria-label mirror (button.class.js) only fires when no
|
|
666
|
+
// aria-label is already present — button-ui stamps aria-label from
|
|
667
|
+
// [text] unconditionally in its own render(), so this never conflicts
|
|
668
|
+
// with REQ-X-001's already-set accessible name.
|
|
669
|
+
btn.setAttribute('title', label);
|
|
436
670
|
btn.addEventListener('click', (e) => {
|
|
437
671
|
e.stopPropagation();
|
|
438
672
|
this.#togglePopover(kind, btn);
|
|
@@ -440,6 +674,55 @@ export class UITableToolbar extends UIElement {
|
|
|
440
674
|
return btn;
|
|
441
675
|
}
|
|
442
676
|
|
|
677
|
+
// ADR-0076 REQ-S-004/REQ-X-003, PLAN step 5 — the overflow-stage "More"
|
|
678
|
+
// trigger. Reuses menu-ui/menu-item-ui unchanged (table-toolbar already
|
|
679
|
+
// `composes: menu-item-ui`; menu-ui is added to the yaml's composes list
|
|
680
|
+
// alongside it — no new runtime dependency, both already ship in this
|
|
681
|
+
// package) and routes every item straight through the EXISTING
|
|
682
|
+
// #togglePopover(kind, btn) — the same method the three granular buttons
|
|
683
|
+
// above call, never a reimplementation of filter/sort/columns logic.
|
|
684
|
+
#mkOverflowTrigger() {
|
|
685
|
+
const menu = document.createElement('menu-ui');
|
|
686
|
+
menu.setAttribute('data-toolbar-overflow', '');
|
|
687
|
+
|
|
688
|
+
const trigger = document.createElement('button-ui');
|
|
689
|
+
trigger.setAttribute('slot', 'trigger');
|
|
690
|
+
trigger.setAttribute('icon', 'dots-three-vertical');
|
|
691
|
+
trigger.setAttribute('variant', 'outline');
|
|
692
|
+
trigger.setAttribute('size', 'sm');
|
|
693
|
+
// REQ-X-003 — a genuinely new interactive element (not a repurposed
|
|
694
|
+
// existing button), so it needs its own static accessible name rather
|
|
695
|
+
// than an inherited one. No [text] attribute — always icon-only,
|
|
696
|
+
// button-ui's own render() reflects [data-icon-only] geometry from
|
|
697
|
+
// that (no CSS override needed here, unlike REQ-M-004's granular
|
|
698
|
+
// buttons which carry a real text label at wider stages).
|
|
699
|
+
trigger.setAttribute('aria-label', 'More table actions');
|
|
700
|
+
trigger.setAttribute('aria-haspopup', 'menu');
|
|
701
|
+
menu.appendChild(trigger);
|
|
702
|
+
|
|
703
|
+
for (const [kind, label, icon] of [
|
|
704
|
+
['filter', 'Filter', 'funnel-simple'],
|
|
705
|
+
['sort', 'Sort', 'arrows-down-up'],
|
|
706
|
+
['columns', 'Columns', 'columns'],
|
|
707
|
+
]) {
|
|
708
|
+
const item = document.createElement('menu-item-ui');
|
|
709
|
+
item.setAttribute('icon', icon);
|
|
710
|
+
item.setAttribute('text', label);
|
|
711
|
+
item.setAttribute('value', kind);
|
|
712
|
+
menu.appendChild(item);
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// menu-ui fires `action` with { value, text } (menu.class.js's own
|
|
716
|
+
// #onItemClick) — anchor the resulting popover on the "More" trigger
|
|
717
|
+
// itself (not any of the three hidden granular buttons), same
|
|
718
|
+
// #togglePopover(kind, btn) every other entry point already calls.
|
|
719
|
+
menu.addEventListener('action', (e) => {
|
|
720
|
+
this.#togglePopover(e.detail.value, trigger);
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
return menu;
|
|
724
|
+
}
|
|
725
|
+
|
|
443
726
|
#updateTitle() {
|
|
444
727
|
const heading = this.querySelector(':scope [data-heading]');
|
|
445
728
|
if (heading) {
|
|
@@ -488,19 +771,56 @@ export class UITableToolbar extends UIElement {
|
|
|
488
771
|
// Independent of #updateTitle's [data-title] cluster by design — coexists
|
|
489
772
|
// with [text]/[count] rather than sharing their hide-with-children logic.
|
|
490
773
|
|
|
774
|
+
// ADR-0076 REQ-E — the `empty` slot renders ONLY on an explicit
|
|
775
|
+
// range-total="0" (checked via hasAttribute, never the computed number
|
|
776
|
+
// alone — REQ-E-001/AC-013: an unset range-total, adiav2's loading state,
|
|
777
|
+
// must never flash the empty-state message). A positive range-total
|
|
778
|
+
// always wins over empty-slot content (REQ-E-002's additive guarantee /
|
|
779
|
+
// AC-014's mutual-exclusion check) — the two states can never render
|
|
780
|
+
// simultaneously.
|
|
491
781
|
#updateSummary() {
|
|
492
782
|
const summary = this.querySelector(':scope > [data-toolbar] > [data-summary]');
|
|
493
783
|
if (!summary) return;
|
|
784
|
+
const text = summary.querySelector(':scope > [data-summary-text]');
|
|
785
|
+
const compact = summary.querySelector(':scope > [data-summary-compact]');
|
|
786
|
+
const empty = summary.querySelector(':scope > [data-summary-empty]');
|
|
494
787
|
const start = Number(this.rangeStart) || 0;
|
|
495
788
|
const end = Number(this.rangeEnd) || 0;
|
|
496
789
|
const total = Number(this.rangeTotal) || 0;
|
|
790
|
+
|
|
497
791
|
if (start > 0 && end > 0 && total > 0) {
|
|
498
|
-
|
|
792
|
+
// Normal range text — always wins over the empty slot (AC-014).
|
|
793
|
+
// Both the full ("Showing X–Y of N") and short-form ("X–Y/N", ADR-0076
|
|
794
|
+
// REQ-S-003) strings are populated together — which one paints is a
|
|
795
|
+
// pure CSS decision (icon-only @container rule / [stage] pin), never
|
|
796
|
+
// a JS branch (REQ-M-002).
|
|
499
797
|
if (text) text.textContent = `Showing ${start}–${end} of ${total}`;
|
|
798
|
+
if (text) text.hidden = false;
|
|
799
|
+
if (compact) compact.textContent = `${start}–${end}/${total}`;
|
|
800
|
+
if (compact) compact.hidden = false;
|
|
801
|
+
if (empty) empty.hidden = true;
|
|
500
802
|
summary.hidden = false;
|
|
501
|
-
|
|
502
|
-
summary.hidden = true;
|
|
803
|
+
return;
|
|
503
804
|
}
|
|
805
|
+
|
|
806
|
+
const confirmedZero = this.hasAttribute('range-total') && this.getAttribute('range-total') === '0';
|
|
807
|
+
const hasEmptyContent = !!empty && empty.childNodes.length > 0;
|
|
808
|
+
|
|
809
|
+
if (confirmedZero && hasEmptyContent) {
|
|
810
|
+
// Explicit range-total="0" + author-supplied empty content (AC-011).
|
|
811
|
+
if (text) text.hidden = true;
|
|
812
|
+
if (compact) compact.hidden = true;
|
|
813
|
+
empty.hidden = false;
|
|
814
|
+
summary.hidden = false;
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Loading (range-total unset, AC-013) or confirmed-zero-with-no-slot-
|
|
819
|
+
// content (AC-012) — hide [data-summary] exactly as today (REQ-E-002).
|
|
820
|
+
if (text) text.hidden = true;
|
|
821
|
+
if (compact) compact.hidden = true;
|
|
822
|
+
if (empty) empty.hidden = true;
|
|
823
|
+
summary.hidden = true;
|
|
504
824
|
}
|
|
505
825
|
|
|
506
826
|
// ── Page-size select (gh#1615) ───────────────────────────────────────────
|
|
@@ -537,37 +857,76 @@ export class UITableToolbar extends UIElement {
|
|
|
537
857
|
#onPageSizeChange = (e) => {
|
|
538
858
|
const pageSize = Number(e.detail?.value ?? this.querySelector(':scope [data-page-size]')?.value) || 0;
|
|
539
859
|
this.pageSize = pageSize;
|
|
540
|
-
//
|
|
541
|
-
//
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
if (this.#target)
|
|
860
|
+
// gh#1764/#1780 (events-only interaction contract, ADR-0079) — the
|
|
861
|
+
// bound table applies its own [paginate] through the `toolbar-paginate`
|
|
862
|
+
// command event rather than a direct property write; table.class.js
|
|
863
|
+
// listens for it in connected() and sets `this.paginate` itself.
|
|
864
|
+
if (this.#target) {
|
|
865
|
+
this.#target.dispatchEvent(new CustomEvent('toolbar-paginate', {
|
|
866
|
+
bubbles: true,
|
|
867
|
+
detail: { pageSize },
|
|
868
|
+
}));
|
|
869
|
+
}
|
|
545
870
|
this.dispatchEvent(new CustomEvent('page-size-change', {
|
|
546
871
|
bubbles: true,
|
|
547
872
|
detail: { pageSize },
|
|
548
873
|
}));
|
|
549
874
|
};
|
|
550
875
|
|
|
876
|
+
// ADR-0076 REQ-C-002 — `chromeOnly` is PURE, ABSOLUTE OR over the four
|
|
877
|
+
// granular `no-*` attributes: while set, all four stay off regardless of
|
|
878
|
+
// any individual `no-*` attribute's own value, with no partial re-enable
|
|
879
|
+
// path (AC-010). This is the ONLY place chromeOnly is combined with the
|
|
880
|
+
// granular attributes — never re-derive the OR elsewhere.
|
|
551
881
|
#updateControlVisibility() {
|
|
552
882
|
const root = this.querySelector(':scope > [data-toolbar]');
|
|
553
883
|
if (!root) return;
|
|
554
884
|
|
|
885
|
+
const chromeOnly = this.chromeOnly;
|
|
555
886
|
const setHidden = (sel, hidden) => {
|
|
556
887
|
const el = root.querySelector(sel);
|
|
557
888
|
if (el) el.hidden = hidden;
|
|
558
889
|
};
|
|
559
890
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
891
|
+
const filterHidden = chromeOnly || this.noFilter;
|
|
892
|
+
const sortHidden = chromeOnly || this.noSort;
|
|
893
|
+
const columnsHidden = chromeOnly || this.noColumns;
|
|
894
|
+
|
|
895
|
+
setHidden('[data-toolbar-btn="filter"]', filterHidden);
|
|
896
|
+
setHidden('[data-toolbar-btn="sort"]', sortHidden);
|
|
897
|
+
setHidden('[data-toolbar-btn="columns"]', columnsHidden);
|
|
898
|
+
setHidden('[data-search]', chromeOnly || this.noSearch);
|
|
899
|
+
|
|
900
|
+
// ADR-0076 REQ-S-004, PLAN step 5 — the overflow "More" trigger's own
|
|
901
|
+
// menu-item-ui children mirror the SAME granular/chrome-only
|
|
902
|
+
// suppression as the three native buttons above: a control suppressed
|
|
903
|
+
// via no-*/chrome-only stays suppressed inside the overflow menu too,
|
|
904
|
+
// never resurrected there. Hide the whole trigger only once every item
|
|
905
|
+
// it would offer is suppressed — an empty "More" menu is worse than no
|
|
906
|
+
// trigger at all.
|
|
907
|
+
const overflow = root.querySelector('[data-toolbar-overflow]');
|
|
908
|
+
if (overflow) {
|
|
909
|
+
const hiddenByKind = { filter: filterHidden, sort: sortHidden, columns: columnsHidden };
|
|
910
|
+
for (const item of overflow.querySelectorAll('menu-item-ui')) {
|
|
911
|
+
item.hidden = !!hiddenByKind[item.getAttribute('value')];
|
|
912
|
+
}
|
|
913
|
+
overflow.hidden = filterHidden && sortHidden && columnsHidden;
|
|
914
|
+
}
|
|
564
915
|
}
|
|
565
916
|
|
|
566
917
|
// ── Search ───────────────────────────────────────────────────────────────
|
|
567
918
|
|
|
568
919
|
#onSearch = (e) => {
|
|
569
920
|
const value = e.detail?.value ?? '';
|
|
570
|
-
|
|
921
|
+
// gh#1764/#1780 (events-only interaction contract, ADR-0079) —
|
|
922
|
+
// `toolbar-search` replaces the direct `.search =` write; table.class.js
|
|
923
|
+
// listens for it in connected() and sets `this.search` itself.
|
|
924
|
+
if (this.#target) {
|
|
925
|
+
this.#target.dispatchEvent(new CustomEvent('toolbar-search', {
|
|
926
|
+
bubbles: true,
|
|
927
|
+
detail: { value },
|
|
928
|
+
}));
|
|
929
|
+
}
|
|
571
930
|
this.dispatchEvent(new CustomEvent('search', {
|
|
572
931
|
bubbles: true,
|
|
573
932
|
detail: { value },
|
|
@@ -709,8 +1068,14 @@ export class UITableToolbar extends UIElement {
|
|
|
709
1068
|
}
|
|
710
1069
|
sel.addEventListener('change', () => {
|
|
711
1070
|
const v = sel.value || '';
|
|
712
|
-
|
|
713
|
-
|
|
1071
|
+
// gh#1764/#1780 (events-only interaction contract, ADR-0079) —
|
|
1072
|
+
// `toolbar-filter-set` replaces the direct target.setFilter()
|
|
1073
|
+
// call; table.class.js applies it synchronously in its own
|
|
1074
|
+
// listener, so `target.filters` below already reflects it.
|
|
1075
|
+
target.dispatchEvent(new CustomEvent('toolbar-filter-set', {
|
|
1076
|
+
bubbles: true,
|
|
1077
|
+
detail: { key: col.key, value: v || null, op: 'select' },
|
|
1078
|
+
}));
|
|
714
1079
|
this.dispatchEvent(new CustomEvent('filter-change', {
|
|
715
1080
|
bubbles: true,
|
|
716
1081
|
detail: { filters: target.filters },
|
|
@@ -726,8 +1091,12 @@ export class UITableToolbar extends UIElement {
|
|
|
726
1091
|
if (current?.op === 'contains') input.value = current.value ?? '';
|
|
727
1092
|
input.addEventListener('input', () => {
|
|
728
1093
|
const v = input.value;
|
|
729
|
-
|
|
730
|
-
|
|
1094
|
+
// gh#1764/#1780 (events-only interaction contract, ADR-0079) —
|
|
1095
|
+
// see the select-filter handler above for the same normalization.
|
|
1096
|
+
target.dispatchEvent(new CustomEvent('toolbar-filter-set', {
|
|
1097
|
+
bubbles: true,
|
|
1098
|
+
detail: { key: col.key, value: v || null, op: 'contains' },
|
|
1099
|
+
}));
|
|
731
1100
|
this.dispatchEvent(new CustomEvent('filter-change', {
|
|
732
1101
|
bubbles: true,
|
|
733
1102
|
detail: { filters: target.filters },
|
|
@@ -743,7 +1112,11 @@ export class UITableToolbar extends UIElement {
|
|
|
743
1112
|
|
|
744
1113
|
if (Object.keys(filters).length) {
|
|
745
1114
|
const clear = this.#mkPopoverAction('Clear all filters', () => {
|
|
746
|
-
|
|
1115
|
+
// gh#1764/#1780 (events-only interaction contract, ADR-0079) —
|
|
1116
|
+
// `toolbar-filter-clear` replaces the direct target.clearFilters()
|
|
1117
|
+
// call; table.class.js applies it synchronously in its own
|
|
1118
|
+
// listener before #refreshFilterPanel() re-reads target.filters.
|
|
1119
|
+
target.dispatchEvent(new CustomEvent('toolbar-filter-clear', { bubbles: true }));
|
|
747
1120
|
this.dispatchEvent(new CustomEvent('filter-change', {
|
|
748
1121
|
bubbles: true,
|
|
749
1122
|
detail: { filters: {} },
|
|
@@ -892,7 +1265,13 @@ export class UITableToolbar extends UIElement {
|
|
|
892
1265
|
const next = target.columns.map((c) => (
|
|
893
1266
|
c.key === col.key ? { ...c, hidden: !check.hasAttribute('checked') } : { ...c }
|
|
894
1267
|
));
|
|
895
|
-
|
|
1268
|
+
// gh#1764/#1780 (events-only interaction contract, ADR-0079) —
|
|
1269
|
+
// `toolbar-columns-set` replaces the direct target.columns= write;
|
|
1270
|
+
// table.class.js applies it synchronously in its own listener.
|
|
1271
|
+
target.dispatchEvent(new CustomEvent('toolbar-columns-set', {
|
|
1272
|
+
bubbles: true,
|
|
1273
|
+
detail: { columns: next },
|
|
1274
|
+
}));
|
|
896
1275
|
this.dispatchEvent(new CustomEvent('columns-change', {
|
|
897
1276
|
bubbles: true,
|
|
898
1277
|
detail: { hiddenColumns: next.filter((c) => c.hidden).map((c) => c.key) },
|