@adia-ai/web-components 0.0.17 → 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.
@@ -38,6 +38,7 @@ export { AdiaCommand } from './command/command.js';
38
38
  export { AdiaColorPicker } from './color-picker/color-picker.js';
39
39
  export { AdiaNoodles } from './noodles/noodles.js';
40
40
  export { AdiaTable } from './table/table.js';
41
+ export { AdiaTableToolbar } from './table-toolbar/table-toolbar.js';
41
42
  export { AdiaTimeline, AdiaTimelineItem } from './timeline/timeline.js';
42
43
  export { AdiaStepper, AdiaStepperItem } from './stepper/stepper.js';
43
44
  export { AdiaSwiper } from './swiper/swiper.js';
@@ -241,12 +241,26 @@ class AdiaNoodles extends AdiaElement {
241
241
  }
242
242
 
243
243
  // ── Port position computation ──────────────────────────────
244
-
244
+ // Uses bounding client rects so nested port-bearing descendants
245
+ // (e.g. inside an absolutely-positioned card wrapper) project
246
+ // correctly into the noodles-ui coordinate system.
247
+ //
248
+ // When an ancestor applies a CSS transform (zoom/pan in a graph
249
+ // editor), bounding rects are in *screen* pixels — but SVG paths
250
+ // and port-indicator dots are positioned in *local* (untransformed)
251
+ // pixels. We detect any ancestor scale by comparing this element's
252
+ // visible width to its layout width and divide deltas accordingly.
253
+ // No transform → ratio is 1, no-op.
245
254
  #getPortPosition(el, side) {
246
- const left = el.offsetLeft;
247
- const top = el.offsetTop;
248
- const w = el.offsetWidth;
249
- const h = el.offsetHeight;
255
+ const elRect = el.getBoundingClientRect();
256
+ const myRect = this.getBoundingClientRect();
257
+ const localScale = (this.offsetWidth && myRect.width)
258
+ ? (myRect.width / this.offsetWidth)
259
+ : 1;
260
+ const left = (elRect.left - myRect.left) / localScale;
261
+ const top = (elRect.top - myRect.top) / localScale;
262
+ const w = elRect.width / localScale;
263
+ const h = elRect.height / localScale;
250
264
 
251
265
  switch (side) {
252
266
  case 'left': return { x: left, y: top + h / 2 };
@@ -0,0 +1,212 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://adiaui.dev/a2ui/v0_9/components/TableToolbar.json",
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. 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
+ "type": "object",
7
+ "allOf": [
8
+ {
9
+ "$ref": "common_types.json#/$defs/ComponentCommon"
10
+ },
11
+ {
12
+ "$ref": "common_types.json#/$defs/CatalogComponentCommon"
13
+ }
14
+ ],
15
+ "properties": {
16
+ "columns": {
17
+ "description": "Show the Columns visibility popover button.",
18
+ "type": "boolean",
19
+ "default": true
20
+ },
21
+ "component": {
22
+ "const": "TableToolbar"
23
+ },
24
+ "count": {
25
+ "description": "Optional count badge value shown next to the title. When unset, falls back to the row count of the bound table.",
26
+ "type": "string",
27
+ "default": ""
28
+ },
29
+ "filterable": {
30
+ "description": "Show the Filter popover button.",
31
+ "type": "boolean",
32
+ "default": true
33
+ },
34
+ "for": {
35
+ "description": "id-ref of the table-ui to control. Falls back to the first sibling table-ui within the same parent when omitted.",
36
+ "type": "string",
37
+ "default": ""
38
+ },
39
+ "placeholder": {
40
+ "description": "Placeholder text for the search input.",
41
+ "type": "string",
42
+ "default": "Search..."
43
+ },
44
+ "searchable": {
45
+ "description": "Show the search input.",
46
+ "type": "boolean",
47
+ "default": true
48
+ },
49
+ "sortable": {
50
+ "description": "Show the Sort popover button.",
51
+ "type": "boolean",
52
+ "default": true
53
+ },
54
+ "text": {
55
+ "description": "Title text shown on the left. Alternative to slotted heading content.",
56
+ "type": "string",
57
+ "default": ""
58
+ },
59
+ "variant": {
60
+ "description": "Toolbar visual variant. `default` renders bare on parent surface; `card` adds the same chrome as a card-ui header.",
61
+ "type": "string",
62
+ "enum": [
63
+ "default",
64
+ "card"
65
+ ],
66
+ "default": "default"
67
+ }
68
+ },
69
+ "required": [
70
+ "component"
71
+ ],
72
+ "unevaluatedProperties": false,
73
+ "x-adiaui": {
74
+ "anti_patterns": [],
75
+ "category": "agent",
76
+ "events": {
77
+ "columns-change": {
78
+ "description": "Column visibility changed. Detail: { hiddenColumns }."
79
+ },
80
+ "filter-change": {
81
+ "description": "Filter set changed. Detail: { filters }."
82
+ },
83
+ "search": {
84
+ "description": "Debounced search query change. Detail: { value }."
85
+ },
86
+ "sort-change": {
87
+ "description": "Sort state changed. Detail: { sortState }."
88
+ }
89
+ },
90
+ "examples": [
91
+ {
92
+ "description": "Members table with filter/sort/columns/search wired to a sibling table-ui.",
93
+ "a2ui": "[\n {\"id\": \"root\", \"component\": \"Column\", \"gap\": \"3\", \"children\": [\"bar\", \"card\"]},\n {\"id\": \"bar\", \"component\": \"TableToolbar\", \"for\": \"members\", \"text\": \"All Employees\", \"count\": \"32\"},\n {\"id\": \"card\", \"component\": \"Card\", \"children\": [\"sec\"]},\n {\"id\": \"sec\", \"component\": \"Section\", \"bleed\": true, \"children\": [\"tbl\"]},\n {\"id\": \"tbl\", \"component\": \"Table\", \"id\": \"members\", \"sortable\": true, \"raw\": true}\n]",
94
+ "name": "members-toolbar"
95
+ }
96
+ ],
97
+ "keywords": [
98
+ "table-toolbar",
99
+ "data-grid",
100
+ "data-grid-toolbar",
101
+ "filter",
102
+ "sort",
103
+ "columns",
104
+ "search",
105
+ "directory",
106
+ "admin",
107
+ "backoffice",
108
+ "listing",
109
+ "records"
110
+ ],
111
+ "name": "AdiaTableToolbar",
112
+ "related": [
113
+ "table",
114
+ "search",
115
+ "button",
116
+ "badge",
117
+ "popover"
118
+ ],
119
+ "slots": {
120
+ "default": {
121
+ "description": "Optional title content. Used when [text] is empty."
122
+ },
123
+ "actions": {
124
+ "description": "Trailing action area — primary buttons (e.g. \"New row\") rendered after the search input."
125
+ }
126
+ },
127
+ "states": [
128
+ {
129
+ "description": "Default, ready for interaction.",
130
+ "name": "idle"
131
+ }
132
+ ],
133
+ "synonyms": {
134
+ "columns": [
135
+ "table-toolbar",
136
+ "table"
137
+ ],
138
+ "data-grid": [
139
+ "table-toolbar",
140
+ "table"
141
+ ],
142
+ "data-grid-toolbar": [
143
+ "table-toolbar",
144
+ "table"
145
+ ],
146
+ "filter": [
147
+ "table-toolbar",
148
+ "table"
149
+ ],
150
+ "sort": [
151
+ "table-toolbar",
152
+ "table"
153
+ ]
154
+ },
155
+ "tag": "table-toolbar-ui",
156
+ "tokens": {
157
+ "--table-toolbar-bg": {
158
+ "description": "Toolbar background (variant=card)"
159
+ },
160
+ "--table-toolbar-border": {
161
+ "description": "Toolbar border color (variant=card)"
162
+ },
163
+ "--table-toolbar-gap": {
164
+ "description": "Gap between toolbar clusters"
165
+ },
166
+ "--table-toolbar-popover-bg": {
167
+ "description": "Popover background"
168
+ },
169
+ "--table-toolbar-popover-border": {
170
+ "description": "Popover border"
171
+ },
172
+ "--table-toolbar-popover-fg": {
173
+ "description": "Popover text color"
174
+ },
175
+ "--table-toolbar-popover-gap": {
176
+ "description": "Popover content gap"
177
+ },
178
+ "--table-toolbar-popover-min": {
179
+ "description": "Popover minimum width"
180
+ },
181
+ "--table-toolbar-popover-pad": {
182
+ "description": "Popover padding"
183
+ },
184
+ "--table-toolbar-popover-radius": {
185
+ "description": "Popover radius"
186
+ },
187
+ "--table-toolbar-popover-shadow": {
188
+ "description": "Popover shadow"
189
+ },
190
+ "--table-toolbar-px": {
191
+ "description": "Horizontal padding"
192
+ },
193
+ "--table-toolbar-py": {
194
+ "description": "Vertical padding"
195
+ },
196
+ "--table-toolbar-radius": {
197
+ "description": "Toolbar corner radius (variant=card)"
198
+ },
199
+ "--table-toolbar-title-fg": {
200
+ "description": "Title text color"
201
+ },
202
+ "--table-toolbar-title-size": {
203
+ "description": "Title font size"
204
+ },
205
+ "--table-toolbar-title-weight": {
206
+ "description": "Title font weight"
207
+ }
208
+ },
209
+ "traits": [],
210
+ "version": 1
211
+ }
212
+ }
@@ -0,0 +1,202 @@
1
+ @scope (table-toolbar-ui) {
2
+ :where(:scope) {
3
+ /* ── Layout ── */
4
+ --table-toolbar-gap: var(--a-space-3);
5
+ --table-toolbar-py: var(--a-space-2);
6
+ --table-toolbar-px: 0;
7
+ --table-toolbar-cluster-gap: var(--a-space-2);
8
+
9
+ /* ── Surface ── */
10
+ --table-toolbar-bg: transparent;
11
+ --table-toolbar-border: transparent;
12
+ --table-toolbar-radius: var(--a-radius-lg);
13
+
14
+ /* ── Title ── */
15
+ --table-toolbar-title-fg: var(--a-fg-strong);
16
+ --table-toolbar-title-size: var(--a-ui-lg);
17
+ --table-toolbar-title-weight: var(--a-weight-medium);
18
+ --table-toolbar-title-gap: var(--a-space-2);
19
+
20
+ /* ── Search ── */
21
+ --table-toolbar-search-min: 14rem;
22
+ --table-toolbar-search-max: 22rem;
23
+
24
+ /* ── Popover ── */
25
+ --table-toolbar-popover-bg: var(--a-canvas);
26
+ --table-toolbar-popover-fg: var(--a-canvas-text);
27
+ --table-toolbar-popover-border: var(--a-border);
28
+ --table-toolbar-popover-radius: var(--a-radius-lg);
29
+ --table-toolbar-popover-shadow: var(--a-shadow-lg);
30
+ --table-toolbar-popover-pad: var(--a-space-2);
31
+ --table-toolbar-popover-gap: var(--a-space-1);
32
+ --table-toolbar-popover-min: 16rem;
33
+ --table-toolbar-popover-head-fg: var(--a-fg-muted);
34
+ --table-toolbar-popover-head-size: var(--a-ui-tiny);
35
+ --table-toolbar-popover-head-pad: var(--a-space-2);
36
+ --table-toolbar-popover-row-pad: var(--a-space-2);
37
+ --table-toolbar-popover-row-radius: var(--a-radius-sm);
38
+ --table-toolbar-popover-row-bg-hover: var(--a-bg-hover);
39
+ --table-toolbar-popover-input-bg: var(--a-bg-subtle);
40
+ --table-toolbar-popover-input-border: var(--a-border-subtle);
41
+ --table-toolbar-popover-input-radius: var(--a-radius-sm);
42
+ --table-toolbar-popover-input-py: var(--a-space-1);
43
+ --table-toolbar-popover-input-px: var(--a-space-2);
44
+ --table-toolbar-popover-input-size: var(--a-ui-sm);
45
+ --table-toolbar-popover-action-fg: var(--a-accent);
46
+ --table-toolbar-popover-action-size: var(--a-ui-sm);
47
+ }
48
+
49
+ /* ═══════ Base ═══════ */
50
+
51
+ :scope {
52
+ box-sizing: border-box;
53
+ display: block;
54
+ color: var(--a-fg);
55
+ }
56
+
57
+ [data-toolbar] {
58
+ display: flex;
59
+ flex-direction: row;
60
+ align-items: center;
61
+ gap: var(--table-toolbar-gap);
62
+ padding: var(--table-toolbar-py) var(--table-toolbar-px);
63
+ min-width: 0;
64
+ }
65
+
66
+ /* ═══════ Variant: card ═══════ */
67
+
68
+ :scope[variant="card"] [data-toolbar] {
69
+ background: var(--table-toolbar-bg);
70
+ border: 1px solid var(--table-toolbar-border);
71
+ border-radius: var(--table-toolbar-radius);
72
+ padding: var(--a-space-3) var(--a-space-4);
73
+ }
74
+
75
+ /* ═══════ Title cluster ═══════ */
76
+
77
+ [data-title] {
78
+ display: inline-flex;
79
+ align-items: center;
80
+ gap: var(--table-toolbar-title-gap);
81
+ flex: 0 0 auto;
82
+ min-width: 0;
83
+ }
84
+
85
+ [data-heading] {
86
+ font-size: var(--table-toolbar-title-size);
87
+ font-weight: var(--table-toolbar-title-weight);
88
+ color: var(--table-toolbar-title-fg);
89
+ line-height: 1.2;
90
+ }
91
+
92
+ [data-count-badge][hidden] { display: none; }
93
+
94
+ /* ═══════ Controls cluster ═══════ */
95
+
96
+ [data-controls] {
97
+ display: inline-flex;
98
+ align-items: center;
99
+ gap: var(--table-toolbar-cluster-gap);
100
+ flex: 0 0 auto;
101
+ }
102
+
103
+ [data-toolbar-btn][hidden] { display: none; }
104
+
105
+ /* ═══════ Search ═══════ */
106
+
107
+ [data-search] {
108
+ flex: 1 1 var(--table-toolbar-search-min);
109
+ min-width: 0;
110
+ max-width: var(--table-toolbar-search-max);
111
+ margin-inline-start: auto;
112
+ }
113
+
114
+ [data-search][hidden] { display: none; }
115
+
116
+ /* ═══════ Actions slot ═══════ */
117
+
118
+ [data-actions] {
119
+ display: inline-flex;
120
+ align-items: center;
121
+ gap: var(--table-toolbar-cluster-gap);
122
+ flex: 0 0 auto;
123
+ }
124
+
125
+ [data-actions]:empty { display: none; }
126
+ }
127
+
128
+ /* ═══════ Popover (top layer — outside @scope) ═══════
129
+ Popovers escape to the top layer and cannot inherit --table-toolbar-* tokens
130
+ from the @scope block. Style with raw --a-* tokens — same pattern used by
131
+ tooltip-ui and toolbar-ui's spillover menu. */
132
+
133
+ [data-toolbar-popover]:not(:popover-open) {
134
+ display: none !important;
135
+ }
136
+
137
+ [data-toolbar-popover]:popover-open {
138
+ margin: 0;
139
+ padding: var(--a-space-1);
140
+ background: var(--a-canvas-bright);
141
+ color: var(--a-fg);
142
+ border: 1px solid var(--a-ui-border);
143
+ border-radius: var(--a-radius);
144
+ box-shadow: var(--a-shadow-lg);
145
+ min-width: 16rem;
146
+ font-family: inherit;
147
+ font-size: var(--a-ui-size);
148
+ display: flex;
149
+ flex-direction: column;
150
+ gap: var(--a-space-1);
151
+ }
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
+
161
+ [data-toolbar-popover] [data-popover-head] {
162
+ padding: var(--a-space-1) var(--a-space-2);
163
+ }
164
+
165
+ [data-toolbar-popover] [data-popover-list] {
166
+ display: flex;
167
+ flex-direction: column;
168
+ gap: var(--a-space-1);
169
+ max-height: 22rem;
170
+ overflow-y: auto;
171
+ }
172
+
173
+ [data-toolbar-popover] [data-popover-empty] {
174
+ display: block;
175
+ padding: var(--a-space-3) var(--a-space-2);
176
+ text-align: center;
177
+ }
178
+
179
+ [data-toolbar-popover] [data-popover-action] {
180
+ margin-top: var(--a-space-1);
181
+ border-top: 1px solid var(--a-border-subtle);
182
+ padding-top: var(--a-space-1);
183
+ }
184
+
185
+ [data-toolbar-popover] [data-filter-row],
186
+ [data-toolbar-popover] [data-columns-row] {
187
+ padding: var(--a-space-1) var(--a-space-2);
188
+ }
189
+
190
+ [data-toolbar-popover] [data-filter-input] {
191
+ min-width: 0;
192
+ }
193
+
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. */
197
+ [data-toolbar-popover] [data-sort-row][data-active] {
198
+ --menu-item-bg: var(--a-bg-selected);
199
+ --menu-item-fg: var(--a-fg);
200
+ --menu-item-icon-fg: var(--a-fg);
201
+ font-weight: var(--a-ui-weight, var(--a-weight-medium));
202
+ }
@@ -0,0 +1,685 @@
1
+ /**
2
+ * <table-toolbar-ui for="emps" text="All Employees" count="32"></table-toolbar-ui>
3
+ *
4
+ * Companion / header bar for a sibling table-ui. Renders:
5
+ * • title + optional count badge
6
+ * • filter / sort / columns popover buttons
7
+ * • search input
8
+ * • optional [slot="actions"] trailing region
9
+ *
10
+ * [for] resolution mirrors chart-legend-ui — the toolbar mounts an element by
11
+ * id, then dispatches state changes against it. When [for] is absent, falls
12
+ * back to the first table-ui sibling under the same parent.
13
+ *
14
+ * Popovers use the platform Popover API + core/anchor.js, the same primitives
15
+ * that menu-ui / popover-ui / toolbar-ui already use in this package.
16
+ *
17
+ * State flow:
18
+ * search → table.search (string property)
19
+ * filters → table.setFilter() (per-key)
20
+ * sort → simulated click on table's [data-sort-key] header
21
+ * column hidden → table.columns = (clone with hidden flag flipped)
22
+ */
23
+
24
+ import { AdiaElement } from '../../core/element.js';
25
+ import { anchorPopover } from '../../core/anchor.js';
26
+
27
+ const SEARCH_DEBOUNCE = 200;
28
+
29
+ class AdiaTableToolbar extends AdiaElement {
30
+ static properties = {
31
+ for: { type: String, default: '', reflect: true },
32
+ text: { type: String, default: '', reflect: false },
33
+ count: { type: String, default: '', reflect: false },
34
+ filterable: { type: Boolean, default: true, reflect: true },
35
+ sortable: { type: Boolean, default: true, reflect: true },
36
+ columns: { type: Boolean, default: true, reflect: true },
37
+ searchable: { type: Boolean, default: true, reflect: true },
38
+ placeholder: { type: String, default: 'Search...', reflect: false },
39
+ variant: { type: String, default: 'default', reflect: true },
40
+ };
41
+
42
+ static template = () => null;
43
+
44
+ #target = null;
45
+ #targetListeners = [];
46
+ #activePopover = null; // { btn, panel, cleanup }
47
+ #docListenersBound = false;
48
+
49
+ // ── Lifecycle ────────────────────────────────────────────────────────────
50
+
51
+ connected() {
52
+ this.setAttribute('role', 'toolbar');
53
+ this.#stamp();
54
+ this.#resolveTarget();
55
+ this.#syncFromTarget();
56
+ }
57
+
58
+ disconnected() {
59
+ this.#closePopover();
60
+ this.#detachTarget();
61
+ }
62
+
63
+ render() {
64
+ // [for] / count / text changes come through here.
65
+ this.#resolveTarget();
66
+ this.#syncFromTarget();
67
+ this.#updateTitle();
68
+ }
69
+
70
+ // ── Target resolution ────────────────────────────────────────────────────
71
+
72
+ #resolveTarget() {
73
+ const next = this.#findTarget();
74
+ if (next === this.#target) return;
75
+ this.#detachTarget();
76
+ if (!next) return;
77
+ this.#target = next;
78
+
79
+ const onSort = () => this.#refreshSortPanel();
80
+ const onFilter = () => this.#refreshFilterPanel();
81
+ next.addEventListener('sort', onSort);
82
+ next.addEventListener('filter-change', onFilter);
83
+ this.#targetListeners.push(['sort', onSort], ['filter-change', onFilter]);
84
+ }
85
+
86
+ #findTarget() {
87
+ if (this.for) {
88
+ const root = this.getRootNode?.();
89
+ const byId = root?.getElementById?.(this.for) || document.getElementById(this.for);
90
+ if (byId && byId.tagName?.toLowerCase() === 'table-ui') return byId;
91
+ return null;
92
+ }
93
+ // Fallback — first sibling table-ui in the same parent.
94
+ const parent = this.parentElement;
95
+ if (!parent) return null;
96
+ return parent.querySelector(':scope table-ui') || null;
97
+ }
98
+
99
+ #detachTarget() {
100
+ if (this.#target) {
101
+ for (const [evt, fn] of this.#targetListeners) {
102
+ this.#target.removeEventListener(evt, fn);
103
+ }
104
+ }
105
+ this.#targetListeners = [];
106
+ this.#target = null;
107
+ }
108
+
109
+ // ── DOM stamp ────────────────────────────────────────────────────────────
110
+
111
+ #stamp() {
112
+ if (this.querySelector(':scope > [data-toolbar]')) return;
113
+
114
+ const root = document.createElement('div');
115
+ root.setAttribute('data-toolbar', '');
116
+
117
+ // Title cluster
118
+ const title = document.createElement('div');
119
+ title.setAttribute('data-title', '');
120
+
121
+ const heading = document.createElement('span');
122
+ heading.setAttribute('data-heading', '');
123
+ title.appendChild(heading);
124
+
125
+ const badge = document.createElement('badge-ui');
126
+ badge.setAttribute('data-count-badge', '');
127
+ badge.setAttribute('size', 'sm');
128
+ badge.setAttribute('variant', 'muted');
129
+ badge.hidden = true;
130
+ title.appendChild(badge);
131
+
132
+ // Controls cluster
133
+ const controls = document.createElement('div');
134
+ controls.setAttribute('data-controls', '');
135
+ controls.appendChild(this.#mkButton('filter', 'Filter', 'funnel-simple'));
136
+ controls.appendChild(this.#mkButton('sort', 'Sort', 'arrows-down-up'));
137
+ controls.appendChild(this.#mkButton('columns', 'Columns', 'columns'));
138
+
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');
145
+ search.setAttribute('data-search', '');
146
+ search.setAttribute('placeholder', this.placeholder);
147
+ search.setAttribute('debounce', String(SEARCH_DEBOUNCE));
148
+ search.addEventListener('search', this.#onSearch);
149
+
150
+ // Actions slot passthrough — we move any pre-existing [slot="actions"] children here
151
+ const actionsSlot = document.createElement('div');
152
+ actionsSlot.setAttribute('data-actions', '');
153
+ for (const node of [...this.children]) {
154
+ if (node === root) continue;
155
+ if (node.getAttribute?.('slot') === 'actions') {
156
+ actionsSlot.appendChild(node);
157
+ }
158
+ }
159
+
160
+ root.appendChild(title);
161
+ root.appendChild(controls);
162
+ root.appendChild(search);
163
+ root.appendChild(actionsSlot);
164
+
165
+ this.appendChild(root);
166
+ this.#updateTitle();
167
+ this.#updateControlVisibility();
168
+ }
169
+
170
+ #mkPopoverAction(label, onClick) {
171
+ const btn = document.createElement('button-ui');
172
+ btn.setAttribute('data-popover-action', '');
173
+ btn.setAttribute('text', label);
174
+ btn.setAttribute('variant', 'ghost');
175
+ btn.setAttribute('size', 'sm');
176
+ btn.setAttribute('stretch', '');
177
+ btn.addEventListener('click', onClick);
178
+ return btn;
179
+ }
180
+
181
+ #mkButton(kind, label, icon) {
182
+ const btn = document.createElement('button-ui');
183
+ btn.setAttribute('data-toolbar-btn', kind);
184
+ btn.setAttribute('icon', icon);
185
+ btn.setAttribute('text', label);
186
+ btn.setAttribute('variant', 'outline');
187
+ btn.setAttribute('size', 'sm');
188
+ btn.setAttribute('aria-haspopup', 'menu');
189
+ btn.addEventListener('click', (e) => {
190
+ e.stopPropagation();
191
+ this.#togglePopover(kind, btn);
192
+ });
193
+ return btn;
194
+ }
195
+
196
+ #updateTitle() {
197
+ const heading = this.querySelector(':scope [data-heading]');
198
+ if (!heading) return;
199
+ if (this.text) {
200
+ heading.textContent = this.text;
201
+ heading.hidden = false;
202
+ } else if (heading.textContent.trim()) {
203
+ heading.hidden = false;
204
+ } else {
205
+ heading.hidden = true;
206
+ }
207
+
208
+ const badge = this.querySelector(':scope [data-count-badge]');
209
+ if (!badge) return;
210
+ const explicit = this.count?.toString().trim();
211
+ const fallback = this.#target?.data?.length;
212
+ const value = explicit || (Number.isFinite(fallback) ? String(fallback) : '');
213
+ if (value) {
214
+ badge.setAttribute('text', value);
215
+ badge.hidden = false;
216
+ } else {
217
+ badge.hidden = true;
218
+ }
219
+ }
220
+
221
+ #updateControlVisibility() {
222
+ const root = this.querySelector(':scope > [data-toolbar]');
223
+ if (!root) return;
224
+
225
+ const setHidden = (sel, hidden) => {
226
+ const el = root.querySelector(sel);
227
+ if (el) el.hidden = hidden;
228
+ };
229
+
230
+ setHidden('[data-toolbar-btn="filter"]', !this.filterable);
231
+ setHidden('[data-toolbar-btn="sort"]', !this.sortable);
232
+ setHidden('[data-toolbar-btn="columns"]', !this.columns);
233
+ setHidden('[data-search]', !this.searchable);
234
+ }
235
+
236
+ // Re-run on attribute changes for boolean flags.
237
+ attributeChanged(name) {
238
+ if (['filterable', 'sortable', 'columns', 'searchable'].includes(name)) {
239
+ this.#updateControlVisibility();
240
+ }
241
+ if (name === 'placeholder') {
242
+ const search = this.querySelector(':scope [data-search]');
243
+ search?.setAttribute('placeholder', this.placeholder);
244
+ }
245
+ }
246
+
247
+ // ── Search ───────────────────────────────────────────────────────────────
248
+
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
+ }));
256
+ };
257
+
258
+ // ── Sync from target (initial paint) ─────────────────────────────────────
259
+
260
+ #syncFromTarget() {
261
+ if (!this.#target) return;
262
+ const search = this.querySelector(':scope [data-search]');
263
+ if (search && this.#target.search) {
264
+ search.value = this.#target.search;
265
+ }
266
+ this.#updateTitle();
267
+ }
268
+
269
+ // ── Popovers ─────────────────────────────────────────────────────────────
270
+
271
+ #togglePopover(kind, btn) {
272
+ if (this.#activePopover?.kind === kind) {
273
+ this.#closePopover();
274
+ return;
275
+ }
276
+ this.#closePopover();
277
+
278
+ const panel = document.createElement('div');
279
+ panel.setAttribute('data-toolbar-popover', kind);
280
+ panel.setAttribute('popover', 'manual');
281
+ panel.setAttribute('role', 'menu');
282
+
283
+ if (kind === 'filter') this.#fillFilterPanel(panel);
284
+ if (kind === 'sort') this.#fillSortPanel(panel);
285
+ if (kind === 'columns') this.#fillColumnsPanel(panel);
286
+
287
+ document.body.appendChild(panel);
288
+
289
+ try { panel.showPopover(); } catch { /* popover API unavailable */ }
290
+ const cleanup = anchorPopover(btn, panel, { placement: 'bottom-start', gap: 4 });
291
+
292
+ this.#activePopover = { kind, btn, panel, cleanup };
293
+
294
+ if (!this.#docListenersBound) {
295
+ this.#docListenersBound = true;
296
+ requestAnimationFrame(() => {
297
+ document.addEventListener('pointerdown', this.#onDocDown, true);
298
+ document.addEventListener('keydown', this.#onDocKey, true);
299
+ });
300
+ }
301
+ }
302
+
303
+ #closePopover() {
304
+ const ap = this.#activePopover;
305
+ if (!ap) return;
306
+ ap.cleanup?.();
307
+ if (ap.panel?.matches?.(':popover-open')) {
308
+ try { ap.panel.hidePopover(); } catch { /* noop */ }
309
+ }
310
+ ap.panel?.remove();
311
+ this.#activePopover = null;
312
+ if (this.#docListenersBound) {
313
+ this.#docListenersBound = false;
314
+ document.removeEventListener('pointerdown', this.#onDocDown, true);
315
+ document.removeEventListener('keydown', this.#onDocKey, true);
316
+ }
317
+ }
318
+
319
+ #onDocDown = (e) => {
320
+ const ap = this.#activePopover;
321
+ if (!ap) return;
322
+ if (ap.btn.contains(e.target)) return;
323
+ if (ap.panel.contains(e.target)) return;
324
+ this.#closePopover();
325
+ };
326
+
327
+ #onDocKey = (e) => {
328
+ if (e.key !== 'Escape') return;
329
+ e.stopPropagation();
330
+ this.#closePopover();
331
+ this.#activePopover?.btn?.focus?.({ preventScroll: true });
332
+ };
333
+
334
+ // ── Filter panel ─────────────────────────────────────────────────────────
335
+
336
+ #fillFilterPanel(panel) {
337
+ const target = this.#target;
338
+ if (!target?.columns?.length) {
339
+ panel.appendChild(emptyHint('No filterable columns'));
340
+ return;
341
+ }
342
+ const filters = target.filters || {};
343
+
344
+ panel.appendChild(popoverHead('Filter rows'));
345
+
346
+ const list = document.createElement('div');
347
+ list.setAttribute('data-popover-list', '');
348
+
349
+ const data = target.data || [];
350
+ for (const col of target.columns) {
351
+ if (col.hidden) continue;
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');
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
+ }
415
+
416
+ list.appendChild(row);
417
+ }
418
+
419
+ panel.appendChild(list);
420
+
421
+ if (Object.keys(filters).length) {
422
+ const clear = this.#mkPopoverAction('Clear all filters', () => {
423
+ target.clearFilters();
424
+ this.dispatchEvent(new CustomEvent('filter-change', {
425
+ bubbles: true,
426
+ detail: { filters: {} },
427
+ }));
428
+ this.#refreshFilterPanel();
429
+ });
430
+ panel.appendChild(clear);
431
+ }
432
+ }
433
+
434
+ #refreshFilterPanel() {
435
+ const ap = this.#activePopover;
436
+ if (!ap || ap.kind !== 'filter') return;
437
+ ap.panel.replaceChildren();
438
+ this.#fillFilterPanel(ap.panel);
439
+ }
440
+
441
+ // ── Sort panel ───────────────────────────────────────────────────────────
442
+
443
+ #fillSortPanel(panel) {
444
+ const target = this.#target;
445
+ if (!target?.columns?.length) {
446
+ panel.appendChild(emptyHint('No sortable columns'));
447
+ return;
448
+ }
449
+
450
+ panel.appendChild(popoverHead('Sort by'));
451
+
452
+ const sortState = target.sortState || [];
453
+ const dirByKey = new Map(sortState.map((s) => [s.key, s.dir]));
454
+
455
+ const list = document.createElement('div');
456
+ list.setAttribute('data-popover-list', '');
457
+
458
+ for (const col of target.columns) {
459
+ if (col.hidden) continue;
460
+ if (col.sortable === false) continue;
461
+
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');
468
+ row.setAttribute('data-sort-row', '');
469
+ row.setAttribute('text', col.label || col.key);
470
+ row.dataset.key = col.key;
471
+ if (dir) row.dataset.active = dir;
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
+ });
482
+
483
+ row.addEventListener('click', (e) => {
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.
486
+ const headerCell = target.querySelector(`:scope > [data-header] [data-sort-key="${col.key}"]`);
487
+ if (!headerCell) return;
488
+ const evt = new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: e.shiftKey });
489
+ headerCell.dispatchEvent(evt);
490
+ this.dispatchEvent(new CustomEvent('sort-change', {
491
+ bubbles: true,
492
+ detail: { sortState: target.sortState },
493
+ }));
494
+ this.#refreshSortPanel();
495
+ });
496
+
497
+ list.appendChild(row);
498
+ }
499
+
500
+ panel.appendChild(list);
501
+
502
+ if (sortState.length) {
503
+ const clear = this.#mkPopoverAction('Clear sort', () => {
504
+ // Simulate cycle-through clicks until empty — but the table only stores
505
+ // one sort entry per key, and a non-shift click on an already-active key
506
+ // cycles to the opposite dir before clearing. Cleanest: clone columns and
507
+ // clear via a microtask using clicks on each active key until empty.
508
+ for (const s of [...sortState]) {
509
+ const headerCell = target.querySelector(`:scope > [data-header] [data-sort-key="${s.key}"]`);
510
+ if (!headerCell) continue;
511
+ // Two clicks toggle through asc → desc → clear when single-sort. With
512
+ // multi-sort entries we shift-click to remove individually.
513
+ const fire = (shift) => headerCell.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: shift }));
514
+ if (sortState.length > 1) {
515
+ // Shift-clicking through default→opposite→remove.
516
+ fire(true); fire(true);
517
+ } else {
518
+ // Single-sort: click twice non-shift to cycle through both dirs and clear.
519
+ fire(false); fire(false);
520
+ }
521
+ }
522
+ this.dispatchEvent(new CustomEvent('sort-change', {
523
+ bubbles: true,
524
+ detail: { sortState: target.sortState },
525
+ }));
526
+ this.#refreshSortPanel();
527
+ });
528
+ panel.appendChild(clear);
529
+ }
530
+ }
531
+
532
+ #refreshSortPanel() {
533
+ const ap = this.#activePopover;
534
+ if (!ap || ap.kind !== 'sort') return;
535
+ ap.panel.replaceChildren();
536
+ this.#fillSortPanel(ap.panel);
537
+ }
538
+
539
+ // ── Columns panel ────────────────────────────────────────────────────────
540
+
541
+ #fillColumnsPanel(panel) {
542
+ const target = this.#target;
543
+ if (!target?.columns?.length) {
544
+ panel.appendChild(emptyHint('No columns'));
545
+ return;
546
+ }
547
+
548
+ panel.appendChild(popoverHead('Visible columns'));
549
+
550
+ const list = document.createElement('div');
551
+ list.setAttribute('data-popover-list', '');
552
+
553
+ for (const col of target.columns) {
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');
558
+ row.setAttribute('data-columns-row', '');
559
+ row.setAttribute('inline', '');
560
+ row.setAttribute('label', col.label || col.key);
561
+
562
+ const check = document.createElement('check-ui');
563
+ if (!col.hidden) check.setAttribute('checked', '');
564
+ check.dataset.key = col.key;
565
+ check.addEventListener('change', () => {
566
+ const next = target.columns.map((c) => (
567
+ c.key === col.key ? { ...c, hidden: !check.hasAttribute('checked') } : { ...c }
568
+ ));
569
+ target.columns = next;
570
+ this.dispatchEvent(new CustomEvent('columns-change', {
571
+ bubbles: true,
572
+ detail: { hiddenColumns: next.filter((c) => c.hidden).map((c) => c.key) },
573
+ }));
574
+ });
575
+ row.appendChild(check);
576
+
577
+ list.appendChild(row);
578
+ }
579
+
580
+ panel.appendChild(list);
581
+ }
582
+ }
583
+
584
+ function emptyHint(text) {
585
+ const el = document.createElement('text-ui');
586
+ el.setAttribute('data-popover-empty', '');
587
+ el.setAttribute('color', 'subtle');
588
+ el.setAttribute('variant', 'caption');
589
+ el.textContent = text;
590
+ return el;
591
+ }
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
+
683
+ customElements.define('table-toolbar-ui', AdiaTableToolbar);
684
+
685
+ export { AdiaTableToolbar };
@@ -0,0 +1,165 @@
1
+ $schema: ../../../../scripts/schemas/component.yaml.schema.json
2
+ name: AdiaTableToolbar
3
+ tag: table-toolbar-ui
4
+ component: TableToolbar
5
+ category: agent
6
+ version: 1
7
+ description: >-
8
+ Header / companion bar for a sibling table-ui. Renders title + count badge,
9
+ filter / sort / columns popovers, and a search input — all wired to the
10
+ target table via an [for] id-ref. Modeled on chart-legend-ui's [for] binding
11
+ pattern. Drop next to (or above) any table-ui to add the standard data-grid
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.
18
+ props:
19
+ for:
20
+ description: id-ref of the table-ui to control. Falls back to the first sibling table-ui within the same parent when omitted.
21
+ type: string
22
+ default: ""
23
+ reflect: true
24
+ text:
25
+ description: Title text shown on the left. Alternative to slotted heading content.
26
+ type: string
27
+ default: ""
28
+ count:
29
+ description: Optional count badge value shown next to the title. When unset, falls back to the row count of the bound table.
30
+ type: string
31
+ default: ""
32
+ filterable:
33
+ description: Show the Filter popover button.
34
+ type: boolean
35
+ default: true
36
+ reflect: true
37
+ sortable:
38
+ description: Show the Sort popover button.
39
+ type: boolean
40
+ default: true
41
+ reflect: true
42
+ columns:
43
+ description: Show the Columns visibility popover button.
44
+ type: boolean
45
+ default: true
46
+ reflect: true
47
+ searchable:
48
+ description: Show the search input.
49
+ type: boolean
50
+ default: true
51
+ reflect: true
52
+ placeholder:
53
+ description: Placeholder text for the search input.
54
+ type: string
55
+ default: Search...
56
+ variant:
57
+ description: Toolbar visual variant. `default` renders bare on parent surface; `card` adds the same chrome as a card-ui header.
58
+ type: string
59
+ default: default
60
+ enum:
61
+ - default
62
+ - card
63
+ reflect: true
64
+ events:
65
+ search:
66
+ description: "Debounced search query change. Detail: { value }."
67
+ filter-change:
68
+ description: "Filter set changed. Detail: { filters }."
69
+ sort-change:
70
+ description: "Sort state changed. Detail: { sortState }."
71
+ columns-change:
72
+ description: "Column visibility changed. Detail: { hiddenColumns }."
73
+ slots:
74
+ default:
75
+ description: Optional title content. Used when [text] is empty.
76
+ actions:
77
+ description: Trailing action area — primary buttons (e.g. "New row") rendered after the search input.
78
+ states:
79
+ - name: idle
80
+ description: Default, ready for interaction.
81
+ traits: []
82
+ tokens:
83
+ --table-toolbar-gap:
84
+ description: Gap between toolbar clusters
85
+ --table-toolbar-py:
86
+ description: Vertical padding
87
+ --table-toolbar-px:
88
+ description: Horizontal padding
89
+ --table-toolbar-bg:
90
+ description: Toolbar background (variant=card)
91
+ --table-toolbar-border:
92
+ description: Toolbar border color (variant=card)
93
+ --table-toolbar-radius:
94
+ description: Toolbar corner radius (variant=card)
95
+ --table-toolbar-title-fg:
96
+ description: Title text color
97
+ --table-toolbar-title-size:
98
+ description: Title font size
99
+ --table-toolbar-title-weight:
100
+ description: Title font weight
101
+ --table-toolbar-popover-bg:
102
+ description: Popover background
103
+ --table-toolbar-popover-fg:
104
+ description: Popover text color
105
+ --table-toolbar-popover-border:
106
+ description: Popover border
107
+ --table-toolbar-popover-radius:
108
+ description: Popover radius
109
+ --table-toolbar-popover-shadow:
110
+ description: Popover shadow
111
+ --table-toolbar-popover-pad:
112
+ description: Popover padding
113
+ --table-toolbar-popover-gap:
114
+ description: Popover content gap
115
+ --table-toolbar-popover-min:
116
+ description: Popover minimum width
117
+ a2ui:
118
+ rules: []
119
+ anti_patterns: []
120
+ examples:
121
+ - name: members-toolbar
122
+ description: Members table with filter/sort/columns/search wired to a sibling table-ui.
123
+ a2ui: >-
124
+ [
125
+ {"id": "root", "component": "Column", "gap": "3", "children": ["bar", "card"]},
126
+ {"id": "bar", "component": "TableToolbar", "for": "members", "text": "All Employees", "count": "32"},
127
+ {"id": "card", "component": "Card", "children": ["sec"]},
128
+ {"id": "sec", "component": "Section", "bleed": true, "children": ["tbl"]},
129
+ {"id": "tbl", "component": "Table", "id": "members", "sortable": true, "raw": true}
130
+ ]
131
+ keywords:
132
+ - table-toolbar
133
+ - data-grid
134
+ - data-grid-toolbar
135
+ - filter
136
+ - sort
137
+ - columns
138
+ - search
139
+ - directory
140
+ - admin
141
+ - backoffice
142
+ - listing
143
+ - records
144
+ synonyms:
145
+ data-grid:
146
+ - table-toolbar
147
+ - table
148
+ data-grid-toolbar:
149
+ - table-toolbar
150
+ - table
151
+ filter:
152
+ - table-toolbar
153
+ - table
154
+ sort:
155
+ - table-toolbar
156
+ - table
157
+ columns:
158
+ - table-toolbar
159
+ - table
160
+ related:
161
+ - table
162
+ - search
163
+ - button
164
+ - badge
165
+ - popover
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adia-ai/web-components",
3
- "version": "0.0.17",
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": {
@@ -39,6 +39,7 @@
39
39
  @import "../components/color-picker/color-picker.css";
40
40
  @import "../components/noodles/noodles.css";
41
41
  @import "../components/table/table.css";
42
+ @import "../components/table-toolbar/table-toolbar.css";
42
43
  @import "../components/timeline/timeline.css";
43
44
  @import "../components/upload/upload.css";
44
45
  @import "../components/card/card.css";