@adia-ai/web-components 0.0.18 → 0.0.19
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.
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://adiaui.dev/a2ui/v0_9/components/TableToolbar.json",
|
|
4
4
|
"title": "TableToolbar",
|
|
5
|
-
"description": "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.",
|
|
5
|
+
"description": "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 above 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.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"allOf": [
|
|
8
8
|
{
|
|
@@ -150,12 +150,16 @@
|
|
|
150
150
|
gap: var(--a-space-1);
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/* The popover head + empty hint are <text-ui variant="kicker"|caption">,
|
|
154
|
+
so their typography (uppercase, tracking, muted color) comes from
|
|
155
|
+
text-ui's own variant rules — no rule needed here.
|
|
156
|
+
Sort rows are <menu-item-ui> — hover/focus/danger styling comes from
|
|
157
|
+
menu-item-ui's @scope block.
|
|
158
|
+
Filter and columns rows are <field-ui inline> — label/control geometry
|
|
159
|
+
comes from field-ui's @scope block. */
|
|
160
|
+
|
|
153
161
|
[data-toolbar-popover] [data-popover-head] {
|
|
154
|
-
|
|
155
|
-
text-transform: uppercase;
|
|
156
|
-
letter-spacing: 0.06em;
|
|
157
|
-
color: var(--a-fg-muted);
|
|
158
|
-
padding: var(--a-space-1) var(--a-ui-px, var(--a-space-2));
|
|
162
|
+
padding: var(--a-space-1) var(--a-space-2);
|
|
159
163
|
}
|
|
160
164
|
|
|
161
165
|
[data-toolbar-popover] [data-popover-list] {
|
|
@@ -178,92 +182,21 @@
|
|
|
178
182
|
padding-top: var(--a-space-1);
|
|
179
183
|
}
|
|
180
184
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
[data-toolbar-popover] [data-filter-row] {
|
|
184
|
-
display: grid;
|
|
185
|
-
grid-template-columns: 1fr minmax(8rem, 1.2fr);
|
|
186
|
-
align-items: center;
|
|
187
|
-
gap: var(--a-space-2);
|
|
185
|
+
[data-toolbar-popover] [data-filter-row],
|
|
186
|
+
[data-toolbar-popover] [data-columns-row] {
|
|
188
187
|
padding: var(--a-space-1) var(--a-space-2);
|
|
189
|
-
border-radius: var(--a-radius-sm);
|
|
190
|
-
color: var(--a-fg-subtle);
|
|
191
|
-
transition:
|
|
192
|
-
background var(--a-duration-fast) var(--a-easing),
|
|
193
|
-
color var(--a-duration-fast) var(--a-easing);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
[data-toolbar-popover] [data-filter-row]:hover {
|
|
197
|
-
background: var(--a-bg-hover);
|
|
198
|
-
color: var(--a-fg-hover);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
[data-toolbar-popover] [data-filter-label] {
|
|
202
|
-
font-size: var(--a-ui-sm);
|
|
203
|
-
color: inherit;
|
|
204
|
-
white-space: nowrap;
|
|
205
|
-
overflow: hidden;
|
|
206
|
-
text-overflow: ellipsis;
|
|
207
188
|
}
|
|
208
189
|
|
|
209
190
|
[data-toolbar-popover] [data-filter-input] {
|
|
210
191
|
min-width: 0;
|
|
211
192
|
}
|
|
212
193
|
|
|
213
|
-
/*
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
all: unset;
|
|
217
|
-
cursor: pointer;
|
|
218
|
-
display: flex;
|
|
219
|
-
align-items: center;
|
|
220
|
-
justify-content: space-between;
|
|
221
|
-
gap: var(--a-space-2);
|
|
222
|
-
padding: var(--a-space-1) var(--a-space-2);
|
|
223
|
-
font-size: var(--a-ui-sm);
|
|
224
|
-
color: var(--a-fg-subtle);
|
|
225
|
-
border-radius: var(--a-radius-sm);
|
|
226
|
-
transition:
|
|
227
|
-
background var(--a-duration-fast) var(--a-easing),
|
|
228
|
-
color var(--a-duration-fast) var(--a-easing);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
[data-toolbar-popover] [data-sort-row]:hover {
|
|
232
|
-
background: var(--a-bg-hover);
|
|
233
|
-
color: var(--a-fg-hover);
|
|
234
|
-
}
|
|
235
|
-
|
|
194
|
+
/* Active sort row: lift the menu-item-ui's bg/fg via CSS-var override —
|
|
195
|
+
menu-item-ui exposes --menu-item-bg / --menu-item-fg / --menu-item-icon-fg
|
|
196
|
+
for exactly this. Stays inside the menu-item-ui token contract. */
|
|
236
197
|
[data-toolbar-popover] [data-sort-row][data-active] {
|
|
237
|
-
|
|
238
|
-
|
|
198
|
+
--menu-item-bg: var(--a-bg-selected);
|
|
199
|
+
--menu-item-fg: var(--a-fg);
|
|
200
|
+
--menu-item-icon-fg: var(--a-fg);
|
|
239
201
|
font-weight: var(--a-ui-weight, var(--a-weight-medium));
|
|
240
202
|
}
|
|
241
|
-
|
|
242
|
-
[data-toolbar-popover] [data-sort-indicator] {
|
|
243
|
-
color: var(--a-fg-muted);
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
[data-toolbar-popover] [data-sort-row][data-active] [data-sort-indicator] {
|
|
247
|
-
color: var(--a-fg);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/* ── Columns rows ── */
|
|
251
|
-
|
|
252
|
-
[data-toolbar-popover] [data-columns-row] {
|
|
253
|
-
display: flex;
|
|
254
|
-
align-items: center;
|
|
255
|
-
gap: var(--a-space-2);
|
|
256
|
-
padding: var(--a-space-1) var(--a-space-2);
|
|
257
|
-
font-size: var(--a-ui-sm);
|
|
258
|
-
color: var(--a-fg-subtle);
|
|
259
|
-
border-radius: var(--a-radius-sm);
|
|
260
|
-
cursor: pointer;
|
|
261
|
-
transition:
|
|
262
|
-
background var(--a-duration-fast) var(--a-easing),
|
|
263
|
-
color var(--a-duration-fast) var(--a-easing);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
[data-toolbar-popover] [data-columns-row]:hover {
|
|
267
|
-
background: var(--a-bg-hover);
|
|
268
|
-
color: var(--a-fg-hover);
|
|
269
|
-
}
|
|
@@ -43,7 +43,6 @@ class AdiaTableToolbar extends AdiaElement {
|
|
|
43
43
|
|
|
44
44
|
#target = null;
|
|
45
45
|
#targetListeners = [];
|
|
46
|
-
#searchTimer = null;
|
|
47
46
|
#activePopover = null; // { btn, panel, cleanup }
|
|
48
47
|
#docListenersBound = false;
|
|
49
48
|
|
|
@@ -57,8 +56,6 @@ class AdiaTableToolbar extends AdiaElement {
|
|
|
57
56
|
}
|
|
58
57
|
|
|
59
58
|
disconnected() {
|
|
60
|
-
clearTimeout(this.#searchTimer);
|
|
61
|
-
this.#searchTimer = null;
|
|
62
59
|
this.#closePopover();
|
|
63
60
|
this.#detachTarget();
|
|
64
61
|
}
|
|
@@ -139,13 +136,16 @@ class AdiaTableToolbar extends AdiaElement {
|
|
|
139
136
|
controls.appendChild(this.#mkButton('sort', 'Sort', 'arrows-down-up'));
|
|
140
137
|
controls.appendChild(this.#mkButton('columns', 'Columns', 'columns'));
|
|
141
138
|
|
|
142
|
-
// Search
|
|
143
|
-
|
|
139
|
+
// Search — compose <search-ui>, which already stamps input-ui with
|
|
140
|
+
// the magnifying-glass prefix + clear suffix and debounces a `search`
|
|
141
|
+
// event. Rolling our own from input-ui would re-derive that wiring
|
|
142
|
+
// and (as discovered) hit a first-paint timing race where the icon
|
|
143
|
+
// name renders as literal text before the icon registry resolves.
|
|
144
|
+
const search = document.createElement('search-ui');
|
|
144
145
|
search.setAttribute('data-search', '');
|
|
145
|
-
search.setAttribute('type', 'search');
|
|
146
|
-
search.setAttribute('prefix', 'magnifying-glass');
|
|
147
146
|
search.setAttribute('placeholder', this.placeholder);
|
|
148
|
-
search.
|
|
147
|
+
search.setAttribute('debounce', String(SEARCH_DEBOUNCE));
|
|
148
|
+
search.addEventListener('search', this.#onSearch);
|
|
149
149
|
|
|
150
150
|
// Actions slot passthrough — we move any pre-existing [slot="actions"] children here
|
|
151
151
|
const actionsSlot = document.createElement('div');
|
|
@@ -246,16 +246,13 @@ class AdiaTableToolbar extends AdiaElement {
|
|
|
246
246
|
|
|
247
247
|
// ── Search ───────────────────────────────────────────────────────────────
|
|
248
248
|
|
|
249
|
-
#
|
|
250
|
-
const value = e.
|
|
251
|
-
|
|
252
|
-
this
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
detail: { value },
|
|
257
|
-
}));
|
|
258
|
-
}, SEARCH_DEBOUNCE);
|
|
249
|
+
#onSearch = (e) => {
|
|
250
|
+
const value = e.detail?.value ?? '';
|
|
251
|
+
if (this.#target) this.#target.search = value;
|
|
252
|
+
this.dispatchEvent(new CustomEvent('search', {
|
|
253
|
+
bubbles: true,
|
|
254
|
+
detail: { value },
|
|
255
|
+
}));
|
|
259
256
|
};
|
|
260
257
|
|
|
261
258
|
// ── Sync from target (initial paint) ─────────────────────────────────────
|
|
@@ -344,40 +341,78 @@ class AdiaTableToolbar extends AdiaElement {
|
|
|
344
341
|
}
|
|
345
342
|
const filters = target.filters || {};
|
|
346
343
|
|
|
347
|
-
|
|
348
|
-
head.setAttribute('data-popover-head', '');
|
|
349
|
-
head.textContent = 'Filter rows';
|
|
350
|
-
panel.appendChild(head);
|
|
344
|
+
panel.appendChild(popoverHead('Filter rows'));
|
|
351
345
|
|
|
352
346
|
const list = document.createElement('div');
|
|
353
347
|
list.setAttribute('data-popover-list', '');
|
|
354
348
|
|
|
349
|
+
const data = target.data || [];
|
|
355
350
|
for (const col of target.columns) {
|
|
356
351
|
if (col.hidden) continue;
|
|
357
|
-
|
|
352
|
+
// <field-ui inline label="…"><…control…></…></field-ui> — canonical
|
|
353
|
+
// label-binds-to-control pair, mints id + [for]. The CONTROL is
|
|
354
|
+
// chosen per-column from the column's filter shape:
|
|
355
|
+
// • 'select' → <select-ui multiple searchable> populated from data
|
|
356
|
+
// • 'text' → <input-ui type="text"> (contains match)
|
|
357
|
+
// Shape comes from col.filter when set, otherwise auto-detected
|
|
358
|
+
// from the data (id-like keys → text; ≤ 50 distinct values → select).
|
|
359
|
+
const row = document.createElement('field-ui');
|
|
358
360
|
row.setAttribute('data-filter-row', '');
|
|
361
|
+
row.setAttribute('inline', '');
|
|
362
|
+
row.setAttribute('label', col.label || col.key);
|
|
363
|
+
|
|
364
|
+
const shape = detectFilterShape(col, data);
|
|
365
|
+
const current = filters[col.key];
|
|
366
|
+
|
|
367
|
+
if (shape === 'select') {
|
|
368
|
+
const sel = document.createElement('select-ui');
|
|
369
|
+
sel.setAttribute('data-filter-input', '');
|
|
370
|
+
sel.setAttribute('multiple', '');
|
|
371
|
+
sel.setAttribute('placeholder', '—');
|
|
372
|
+
|
|
373
|
+
const uniqueValues = collectUniqueValues(col, data);
|
|
374
|
+
// Searchable kicks in at 12+ options — below that the listbox is
|
|
375
|
+
// fully scannable and a search field is overhead.
|
|
376
|
+
if (uniqueValues.length >= 12) sel.setAttribute('searchable', '');
|
|
377
|
+
|
|
378
|
+
for (const val of uniqueValues) {
|
|
379
|
+
const opt = document.createElement('option');
|
|
380
|
+
opt.setAttribute('value', val);
|
|
381
|
+
opt.textContent = val;
|
|
382
|
+
sel.appendChild(opt);
|
|
383
|
+
}
|
|
384
|
+
if (current?.op === 'select' && current.value) {
|
|
385
|
+
sel.value = current.value;
|
|
386
|
+
}
|
|
387
|
+
sel.addEventListener('change', () => {
|
|
388
|
+
const v = sel.value || '';
|
|
389
|
+
if (v) target.setFilter(col.key, v, 'select');
|
|
390
|
+
else target.setFilter(col.key, null);
|
|
391
|
+
this.dispatchEvent(new CustomEvent('filter-change', {
|
|
392
|
+
bubbles: true,
|
|
393
|
+
detail: { filters: target.filters },
|
|
394
|
+
}));
|
|
395
|
+
});
|
|
396
|
+
row.appendChild(sel);
|
|
397
|
+
} else {
|
|
398
|
+
const input = document.createElement('input-ui');
|
|
399
|
+
input.setAttribute('type', 'text');
|
|
400
|
+
input.setAttribute('size', 'sm');
|
|
401
|
+
input.setAttribute('data-filter-input', '');
|
|
402
|
+
input.setAttribute('placeholder', '—');
|
|
403
|
+
if (current?.op === 'contains') input.value = current.value ?? '';
|
|
404
|
+
input.addEventListener('input', () => {
|
|
405
|
+
const v = input.value;
|
|
406
|
+
if (v) target.setFilter(col.key, v, 'contains');
|
|
407
|
+
else target.setFilter(col.key, null);
|
|
408
|
+
this.dispatchEvent(new CustomEvent('filter-change', {
|
|
409
|
+
bubbles: true,
|
|
410
|
+
detail: { filters: target.filters },
|
|
411
|
+
}));
|
|
412
|
+
});
|
|
413
|
+
row.appendChild(input);
|
|
414
|
+
}
|
|
359
415
|
|
|
360
|
-
const labelEl = document.createElement('span');
|
|
361
|
-
labelEl.setAttribute('data-filter-label', '');
|
|
362
|
-
labelEl.textContent = col.label || col.key;
|
|
363
|
-
row.appendChild(labelEl);
|
|
364
|
-
|
|
365
|
-
const input = document.createElement('input-ui');
|
|
366
|
-
input.setAttribute('type', 'text');
|
|
367
|
-
input.setAttribute('size', 'sm');
|
|
368
|
-
input.setAttribute('data-filter-input', '');
|
|
369
|
-
input.setAttribute('placeholder', '—');
|
|
370
|
-
input.value = filters[col.key]?.value ?? '';
|
|
371
|
-
input.addEventListener('input', () => {
|
|
372
|
-
const v = input.value;
|
|
373
|
-
if (v) target.setFilter(col.key, v, 'contains');
|
|
374
|
-
else target.setFilter(col.key, null);
|
|
375
|
-
this.dispatchEvent(new CustomEvent('filter-change', {
|
|
376
|
-
bubbles: true,
|
|
377
|
-
detail: { filters: target.filters },
|
|
378
|
-
}));
|
|
379
|
-
});
|
|
380
|
-
row.appendChild(input);
|
|
381
416
|
list.appendChild(row);
|
|
382
417
|
}
|
|
383
418
|
|
|
@@ -412,10 +447,7 @@ class AdiaTableToolbar extends AdiaElement {
|
|
|
412
447
|
return;
|
|
413
448
|
}
|
|
414
449
|
|
|
415
|
-
|
|
416
|
-
head.setAttribute('data-popover-head', '');
|
|
417
|
-
head.textContent = 'Sort by';
|
|
418
|
-
panel.appendChild(head);
|
|
450
|
+
panel.appendChild(popoverHead('Sort by'));
|
|
419
451
|
|
|
420
452
|
const sortState = target.sortState || [];
|
|
421
453
|
const dirByKey = new Map(sortState.map((s) => [s.key, s.dir]));
|
|
@@ -427,29 +459,30 @@ class AdiaTableToolbar extends AdiaElement {
|
|
|
427
459
|
if (col.hidden) continue;
|
|
428
460
|
if (col.sortable === false) continue;
|
|
429
461
|
|
|
430
|
-
|
|
431
|
-
|
|
462
|
+
// <menu-item-ui> is the canonical action-row primitive (role="menuitem",
|
|
463
|
+
// built-in icon + text slots, hover/focus tokens, danger variant). Append
|
|
464
|
+
// a trailing <icon-ui> after the auto-stamped [slot="text"] — its flex:1
|
|
465
|
+
// pushes any later child to the trailing edge.
|
|
466
|
+
const dir = dirByKey.get(col.key);
|
|
467
|
+
const row = document.createElement('menu-item-ui');
|
|
432
468
|
row.setAttribute('data-sort-row', '');
|
|
469
|
+
row.setAttribute('text', col.label || col.key);
|
|
433
470
|
row.dataset.key = col.key;
|
|
434
|
-
|
|
435
|
-
const labelEl = document.createElement('span');
|
|
436
|
-
labelEl.setAttribute('data-sort-label', '');
|
|
437
|
-
labelEl.textContent = col.label || col.key;
|
|
438
|
-
row.appendChild(labelEl);
|
|
439
|
-
|
|
440
|
-
const dir = dirByKey.get(col.key);
|
|
441
|
-
const indicator = document.createElement('icon-ui');
|
|
442
|
-
indicator.setAttribute('data-sort-indicator', '');
|
|
443
|
-
indicator.setAttribute('size', 'xs');
|
|
444
|
-
indicator.setAttribute('name', dir === 'asc' ? 'arrow-up' : dir === 'desc' ? 'arrow-down' : 'caret-up-down');
|
|
445
471
|
if (dir) row.dataset.active = dir;
|
|
446
|
-
|
|
472
|
+
|
|
473
|
+
// Trailing direction indicator — append AFTER menu-item-ui's connected()
|
|
474
|
+
// has run its #stamp() (defers via rAF so the [slot="text"] span exists).
|
|
475
|
+
requestAnimationFrame(() => {
|
|
476
|
+
const indicator = document.createElement('icon-ui');
|
|
477
|
+
indicator.setAttribute('data-sort-indicator', '');
|
|
478
|
+
indicator.setAttribute('size', 'xs');
|
|
479
|
+
indicator.setAttribute('name', dir === 'asc' ? 'arrow-up' : dir === 'desc' ? 'arrow-down' : 'caret-up-down');
|
|
480
|
+
row.appendChild(indicator);
|
|
481
|
+
});
|
|
447
482
|
|
|
448
483
|
row.addEventListener('click', (e) => {
|
|
449
|
-
//
|
|
450
|
-
//
|
|
451
|
-
// when the table is not present — same defensive shape as chart-legend's
|
|
452
|
-
// [for] target check.
|
|
484
|
+
// Forward to the bound table by simulating a header click — the table
|
|
485
|
+
// already owns the asc / desc / clear cycle, so we don't duplicate state.
|
|
453
486
|
const headerCell = target.querySelector(`:scope > [data-header] [data-sort-key="${col.key}"]`);
|
|
454
487
|
if (!headerCell) return;
|
|
455
488
|
const evt = new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: e.shiftKey });
|
|
@@ -512,17 +545,19 @@ class AdiaTableToolbar extends AdiaElement {
|
|
|
512
545
|
return;
|
|
513
546
|
}
|
|
514
547
|
|
|
515
|
-
|
|
516
|
-
head.setAttribute('data-popover-head', '');
|
|
517
|
-
head.textContent = 'Visible columns';
|
|
518
|
-
panel.appendChild(head);
|
|
548
|
+
panel.appendChild(popoverHead('Visible columns'));
|
|
519
549
|
|
|
520
550
|
const list = document.createElement('div');
|
|
521
551
|
list.setAttribute('data-popover-list', '');
|
|
522
552
|
|
|
523
553
|
for (const col of target.columns) {
|
|
524
|
-
|
|
554
|
+
// <field-ui inline label="…"><check-ui></check-ui></field-ui> —
|
|
555
|
+
// canonical label-binds-to-control pair, mints id + [for] so clicking
|
|
556
|
+
// the label toggles the check. Same primitive used for filter rows.
|
|
557
|
+
const row = document.createElement('field-ui');
|
|
525
558
|
row.setAttribute('data-columns-row', '');
|
|
559
|
+
row.setAttribute('inline', '');
|
|
560
|
+
row.setAttribute('label', col.label || col.key);
|
|
526
561
|
|
|
527
562
|
const check = document.createElement('check-ui');
|
|
528
563
|
if (!col.hidden) check.setAttribute('checked', '');
|
|
@@ -539,11 +574,6 @@ class AdiaTableToolbar extends AdiaElement {
|
|
|
539
574
|
});
|
|
540
575
|
row.appendChild(check);
|
|
541
576
|
|
|
542
|
-
const labelEl = document.createElement('span');
|
|
543
|
-
labelEl.setAttribute('data-columns-label', '');
|
|
544
|
-
labelEl.textContent = col.label || col.key;
|
|
545
|
-
row.appendChild(labelEl);
|
|
546
|
-
|
|
547
577
|
list.appendChild(row);
|
|
548
578
|
}
|
|
549
579
|
|
|
@@ -560,6 +590,96 @@ function emptyHint(text) {
|
|
|
560
590
|
return el;
|
|
561
591
|
}
|
|
562
592
|
|
|
593
|
+
function popoverHead(text) {
|
|
594
|
+
// <text-ui variant="kicker"> renders the uppercase + tracked + muted-color
|
|
595
|
+
// section-marker typography role — exactly what a popover head needs. No
|
|
596
|
+
// bespoke styling required, no [data-popover-head] CSS rule.
|
|
597
|
+
const el = document.createElement('text-ui');
|
|
598
|
+
el.setAttribute('data-popover-head', '');
|
|
599
|
+
el.setAttribute('variant', 'kicker');
|
|
600
|
+
el.setAttribute('color', 'subtle');
|
|
601
|
+
el.textContent = text;
|
|
602
|
+
return el;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// ── Filter shape detection ─────────────────────────────────────────────────
|
|
606
|
+
|
|
607
|
+
const SELECT_THRESHOLD = 50; // ≤ N distinct values → multi-select
|
|
608
|
+
const SAMPLE_LIMIT = 500; // cap data scan for cardinality detection
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Resolve a cell value for a column. Mirrors table.js getCellValue —
|
|
612
|
+
* supports col.accessor (function) and dot-notation col.key paths.
|
|
613
|
+
*/
|
|
614
|
+
function getCellValue(row, col) {
|
|
615
|
+
if (typeof col?.accessor === 'function') return col.accessor(row);
|
|
616
|
+
const path = col?.key;
|
|
617
|
+
if (!path || row == null) return undefined;
|
|
618
|
+
const parts = String(path).split('.');
|
|
619
|
+
let cur = row;
|
|
620
|
+
for (const p of parts) {
|
|
621
|
+
if (cur == null) return undefined;
|
|
622
|
+
cur = cur[p];
|
|
623
|
+
}
|
|
624
|
+
return cur;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Heuristic: id-like keys (`id`, `userId`, `worker_id`, `uuid`, `guid`)
|
|
629
|
+
* are searched as free text — picking from a 10k-entry listbox is worse
|
|
630
|
+
* UX than typing a known value.
|
|
631
|
+
*/
|
|
632
|
+
function looksLikeIdKey(key) {
|
|
633
|
+
if (!key) return false;
|
|
634
|
+
const k = String(key);
|
|
635
|
+
if (k === 'id' || k === 'ID' || k === 'uuid' || k === 'guid') return true;
|
|
636
|
+
if (/[a-z](Id|ID)$/.test(k)) return true; // camelCase: workerId, userId
|
|
637
|
+
if (/_id$/i.test(k)) return true; // snake_case: worker_id
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Choose the right filter primitive for a column.
|
|
643
|
+
* col.filter = 'select' | 'text' — explicit override (table-ui contract)
|
|
644
|
+
* col.type = 'number' | 'currency' | 'percent' → 'text' (numeric range
|
|
645
|
+
* support is a future addition; falls back to contains for now)
|
|
646
|
+
* id-like key → 'text'
|
|
647
|
+
* ≤ SELECT_THRESHOLD distinct values in the sample → 'select'
|
|
648
|
+
* otherwise → 'text'
|
|
649
|
+
*/
|
|
650
|
+
function detectFilterShape(col, data) {
|
|
651
|
+
if (col?.filter === 'select' || col?.filter === 'text') return col.filter;
|
|
652
|
+
if (looksLikeIdKey(col?.key)) return 'text';
|
|
653
|
+
|
|
654
|
+
const values = [];
|
|
655
|
+
const limit = Math.min(data?.length ?? 0, SAMPLE_LIMIT);
|
|
656
|
+
const unique = new Set();
|
|
657
|
+
for (let i = 0; i < limit; i++) {
|
|
658
|
+
const v = getCellValue(data[i], col);
|
|
659
|
+
if (v == null || v === '') continue;
|
|
660
|
+
values.push(v);
|
|
661
|
+
unique.add(String(v));
|
|
662
|
+
if (unique.size > SELECT_THRESHOLD) return 'text';
|
|
663
|
+
}
|
|
664
|
+
if (!values.length) return 'text';
|
|
665
|
+
return 'select';
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Collect distinct stringified values for the column from data, sorted.
|
|
670
|
+
* Used to populate select-ui options when filter shape === 'select'.
|
|
671
|
+
*/
|
|
672
|
+
function collectUniqueValues(col, data) {
|
|
673
|
+
const set = new Set();
|
|
674
|
+
const limit = Math.min(data?.length ?? 0, SAMPLE_LIMIT);
|
|
675
|
+
for (let i = 0; i < limit; i++) {
|
|
676
|
+
const v = getCellValue(data[i], col);
|
|
677
|
+
if (v == null || v === '') continue;
|
|
678
|
+
set.add(String(v));
|
|
679
|
+
}
|
|
680
|
+
return [...set].sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
|
|
681
|
+
}
|
|
682
|
+
|
|
563
683
|
customElements.define('table-toolbar-ui', AdiaTableToolbar);
|
|
564
684
|
|
|
565
685
|
export { AdiaTableToolbar };
|
|
@@ -10,6 +10,11 @@ description: >-
|
|
|
10
10
|
target table via an [for] id-ref. Modeled on chart-legend-ui's [for] binding
|
|
11
11
|
pattern. Drop next to (or above) any table-ui to add the standard data-grid
|
|
12
12
|
toolbar without re-implementing search, filter, sort, or column visibility.
|
|
13
|
+
Filter rows auto-pick a primitive per column: ≤ 50 distinct values → multi-
|
|
14
|
+
select (searchable above 12 options), id-like keys → free-text contains. The
|
|
15
|
+
column descriptor's `filter` field overrides the auto-detect: `'select'`
|
|
16
|
+
forces multi-select even on high-cardinality columns; `'text'` forces a
|
|
17
|
+
contains input even on small enums.
|
|
13
18
|
props:
|
|
14
19
|
for:
|
|
15
20
|
description: id-ref of the table-ui to control. Falls back to the first sibling table-ui within the same parent when omitted.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adia-ai/web-components",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.19",
|
|
4
4
|
"description": "AdiaUI web components — vanilla custom elements. A2UI runtime (renderer, registry, streams, wiring) lives in @adia-ai/a2ui-utils.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|