@adia-ai/web-components 0.0.17 → 0.0.18
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/components/index.js +1 -0
- package/components/noodles/noodles.js +19 -5
- package/components/table-toolbar/table-toolbar.a2ui.json +212 -0
- package/components/table-toolbar/table-toolbar.css +269 -0
- package/components/table-toolbar/table-toolbar.js +565 -0
- package/components/table-toolbar/table-toolbar.yaml +160 -0
- package/package.json +1 -1
- package/styles/components.css +1 -0
package/components/index.js
CHANGED
|
@@ -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
|
|
247
|
-
const
|
|
248
|
-
const
|
|
249
|
-
|
|
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.",
|
|
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,269 @@
|
|
|
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
|
+
[data-toolbar-popover] [data-popover-head] {
|
|
154
|
+
font-size: var(--a-ui-tiny);
|
|
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));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
[data-toolbar-popover] [data-popover-list] {
|
|
162
|
+
display: flex;
|
|
163
|
+
flex-direction: column;
|
|
164
|
+
gap: var(--a-space-1);
|
|
165
|
+
max-height: 22rem;
|
|
166
|
+
overflow-y: auto;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
[data-toolbar-popover] [data-popover-empty] {
|
|
170
|
+
display: block;
|
|
171
|
+
padding: var(--a-space-3) var(--a-space-2);
|
|
172
|
+
text-align: center;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
[data-toolbar-popover] [data-popover-action] {
|
|
176
|
+
margin-top: var(--a-space-1);
|
|
177
|
+
border-top: 1px solid var(--a-border-subtle);
|
|
178
|
+
padding-top: var(--a-space-1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/* ── Filter rows ── */
|
|
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);
|
|
188
|
+
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
|
+
}
|
|
208
|
+
|
|
209
|
+
[data-toolbar-popover] [data-filter-input] {
|
|
210
|
+
min-width: 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/* ── Sort rows ── */
|
|
214
|
+
|
|
215
|
+
[data-toolbar-popover] [data-sort-row] {
|
|
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
|
+
|
|
236
|
+
[data-toolbar-popover] [data-sort-row][data-active] {
|
|
237
|
+
color: var(--a-fg);
|
|
238
|
+
background: var(--a-bg-selected);
|
|
239
|
+
font-weight: var(--a-ui-weight, var(--a-weight-medium));
|
|
240
|
+
}
|
|
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
|
+
}
|
|
@@ -0,0 +1,565 @@
|
|
|
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
|
+
#searchTimer = null;
|
|
47
|
+
#activePopover = null; // { btn, panel, cleanup }
|
|
48
|
+
#docListenersBound = false;
|
|
49
|
+
|
|
50
|
+
// ── Lifecycle ────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
connected() {
|
|
53
|
+
this.setAttribute('role', 'toolbar');
|
|
54
|
+
this.#stamp();
|
|
55
|
+
this.#resolveTarget();
|
|
56
|
+
this.#syncFromTarget();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
disconnected() {
|
|
60
|
+
clearTimeout(this.#searchTimer);
|
|
61
|
+
this.#searchTimer = null;
|
|
62
|
+
this.#closePopover();
|
|
63
|
+
this.#detachTarget();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
render() {
|
|
67
|
+
// [for] / count / text changes come through here.
|
|
68
|
+
this.#resolveTarget();
|
|
69
|
+
this.#syncFromTarget();
|
|
70
|
+
this.#updateTitle();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Target resolution ────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
#resolveTarget() {
|
|
76
|
+
const next = this.#findTarget();
|
|
77
|
+
if (next === this.#target) return;
|
|
78
|
+
this.#detachTarget();
|
|
79
|
+
if (!next) return;
|
|
80
|
+
this.#target = next;
|
|
81
|
+
|
|
82
|
+
const onSort = () => this.#refreshSortPanel();
|
|
83
|
+
const onFilter = () => this.#refreshFilterPanel();
|
|
84
|
+
next.addEventListener('sort', onSort);
|
|
85
|
+
next.addEventListener('filter-change', onFilter);
|
|
86
|
+
this.#targetListeners.push(['sort', onSort], ['filter-change', onFilter]);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#findTarget() {
|
|
90
|
+
if (this.for) {
|
|
91
|
+
const root = this.getRootNode?.();
|
|
92
|
+
const byId = root?.getElementById?.(this.for) || document.getElementById(this.for);
|
|
93
|
+
if (byId && byId.tagName?.toLowerCase() === 'table-ui') return byId;
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
// Fallback — first sibling table-ui in the same parent.
|
|
97
|
+
const parent = this.parentElement;
|
|
98
|
+
if (!parent) return null;
|
|
99
|
+
return parent.querySelector(':scope table-ui') || null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
#detachTarget() {
|
|
103
|
+
if (this.#target) {
|
|
104
|
+
for (const [evt, fn] of this.#targetListeners) {
|
|
105
|
+
this.#target.removeEventListener(evt, fn);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
this.#targetListeners = [];
|
|
109
|
+
this.#target = null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── DOM stamp ────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
#stamp() {
|
|
115
|
+
if (this.querySelector(':scope > [data-toolbar]')) return;
|
|
116
|
+
|
|
117
|
+
const root = document.createElement('div');
|
|
118
|
+
root.setAttribute('data-toolbar', '');
|
|
119
|
+
|
|
120
|
+
// Title cluster
|
|
121
|
+
const title = document.createElement('div');
|
|
122
|
+
title.setAttribute('data-title', '');
|
|
123
|
+
|
|
124
|
+
const heading = document.createElement('span');
|
|
125
|
+
heading.setAttribute('data-heading', '');
|
|
126
|
+
title.appendChild(heading);
|
|
127
|
+
|
|
128
|
+
const badge = document.createElement('badge-ui');
|
|
129
|
+
badge.setAttribute('data-count-badge', '');
|
|
130
|
+
badge.setAttribute('size', 'sm');
|
|
131
|
+
badge.setAttribute('variant', 'muted');
|
|
132
|
+
badge.hidden = true;
|
|
133
|
+
title.appendChild(badge);
|
|
134
|
+
|
|
135
|
+
// Controls cluster
|
|
136
|
+
const controls = document.createElement('div');
|
|
137
|
+
controls.setAttribute('data-controls', '');
|
|
138
|
+
controls.appendChild(this.#mkButton('filter', 'Filter', 'funnel-simple'));
|
|
139
|
+
controls.appendChild(this.#mkButton('sort', 'Sort', 'arrows-down-up'));
|
|
140
|
+
controls.appendChild(this.#mkButton('columns', 'Columns', 'columns'));
|
|
141
|
+
|
|
142
|
+
// Search
|
|
143
|
+
const search = document.createElement('input-ui');
|
|
144
|
+
search.setAttribute('data-search', '');
|
|
145
|
+
search.setAttribute('type', 'search');
|
|
146
|
+
search.setAttribute('prefix', 'magnifying-glass');
|
|
147
|
+
search.setAttribute('placeholder', this.placeholder);
|
|
148
|
+
search.addEventListener('input', this.#onSearchInput);
|
|
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
|
+
#onSearchInput = (e) => {
|
|
250
|
+
const value = e.target?.value ?? '';
|
|
251
|
+
clearTimeout(this.#searchTimer);
|
|
252
|
+
this.#searchTimer = setTimeout(() => {
|
|
253
|
+
if (this.#target) this.#target.search = value;
|
|
254
|
+
this.dispatchEvent(new CustomEvent('search', {
|
|
255
|
+
bubbles: true,
|
|
256
|
+
detail: { value },
|
|
257
|
+
}));
|
|
258
|
+
}, SEARCH_DEBOUNCE);
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// ── Sync from target (initial paint) ─────────────────────────────────────
|
|
262
|
+
|
|
263
|
+
#syncFromTarget() {
|
|
264
|
+
if (!this.#target) return;
|
|
265
|
+
const search = this.querySelector(':scope [data-search]');
|
|
266
|
+
if (search && this.#target.search) {
|
|
267
|
+
search.value = this.#target.search;
|
|
268
|
+
}
|
|
269
|
+
this.#updateTitle();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ── Popovers ─────────────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
#togglePopover(kind, btn) {
|
|
275
|
+
if (this.#activePopover?.kind === kind) {
|
|
276
|
+
this.#closePopover();
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
this.#closePopover();
|
|
280
|
+
|
|
281
|
+
const panel = document.createElement('div');
|
|
282
|
+
panel.setAttribute('data-toolbar-popover', kind);
|
|
283
|
+
panel.setAttribute('popover', 'manual');
|
|
284
|
+
panel.setAttribute('role', 'menu');
|
|
285
|
+
|
|
286
|
+
if (kind === 'filter') this.#fillFilterPanel(panel);
|
|
287
|
+
if (kind === 'sort') this.#fillSortPanel(panel);
|
|
288
|
+
if (kind === 'columns') this.#fillColumnsPanel(panel);
|
|
289
|
+
|
|
290
|
+
document.body.appendChild(panel);
|
|
291
|
+
|
|
292
|
+
try { panel.showPopover(); } catch { /* popover API unavailable */ }
|
|
293
|
+
const cleanup = anchorPopover(btn, panel, { placement: 'bottom-start', gap: 4 });
|
|
294
|
+
|
|
295
|
+
this.#activePopover = { kind, btn, panel, cleanup };
|
|
296
|
+
|
|
297
|
+
if (!this.#docListenersBound) {
|
|
298
|
+
this.#docListenersBound = true;
|
|
299
|
+
requestAnimationFrame(() => {
|
|
300
|
+
document.addEventListener('pointerdown', this.#onDocDown, true);
|
|
301
|
+
document.addEventListener('keydown', this.#onDocKey, true);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
#closePopover() {
|
|
307
|
+
const ap = this.#activePopover;
|
|
308
|
+
if (!ap) return;
|
|
309
|
+
ap.cleanup?.();
|
|
310
|
+
if (ap.panel?.matches?.(':popover-open')) {
|
|
311
|
+
try { ap.panel.hidePopover(); } catch { /* noop */ }
|
|
312
|
+
}
|
|
313
|
+
ap.panel?.remove();
|
|
314
|
+
this.#activePopover = null;
|
|
315
|
+
if (this.#docListenersBound) {
|
|
316
|
+
this.#docListenersBound = false;
|
|
317
|
+
document.removeEventListener('pointerdown', this.#onDocDown, true);
|
|
318
|
+
document.removeEventListener('keydown', this.#onDocKey, true);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
#onDocDown = (e) => {
|
|
323
|
+
const ap = this.#activePopover;
|
|
324
|
+
if (!ap) return;
|
|
325
|
+
if (ap.btn.contains(e.target)) return;
|
|
326
|
+
if (ap.panel.contains(e.target)) return;
|
|
327
|
+
this.#closePopover();
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
#onDocKey = (e) => {
|
|
331
|
+
if (e.key !== 'Escape') return;
|
|
332
|
+
e.stopPropagation();
|
|
333
|
+
this.#closePopover();
|
|
334
|
+
this.#activePopover?.btn?.focus?.({ preventScroll: true });
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// ── Filter panel ─────────────────────────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
#fillFilterPanel(panel) {
|
|
340
|
+
const target = this.#target;
|
|
341
|
+
if (!target?.columns?.length) {
|
|
342
|
+
panel.appendChild(emptyHint('No filterable columns'));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const filters = target.filters || {};
|
|
346
|
+
|
|
347
|
+
const head = document.createElement('div');
|
|
348
|
+
head.setAttribute('data-popover-head', '');
|
|
349
|
+
head.textContent = 'Filter rows';
|
|
350
|
+
panel.appendChild(head);
|
|
351
|
+
|
|
352
|
+
const list = document.createElement('div');
|
|
353
|
+
list.setAttribute('data-popover-list', '');
|
|
354
|
+
|
|
355
|
+
for (const col of target.columns) {
|
|
356
|
+
if (col.hidden) continue;
|
|
357
|
+
const row = document.createElement('label');
|
|
358
|
+
row.setAttribute('data-filter-row', '');
|
|
359
|
+
|
|
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
|
+
list.appendChild(row);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
panel.appendChild(list);
|
|
385
|
+
|
|
386
|
+
if (Object.keys(filters).length) {
|
|
387
|
+
const clear = this.#mkPopoverAction('Clear all filters', () => {
|
|
388
|
+
target.clearFilters();
|
|
389
|
+
this.dispatchEvent(new CustomEvent('filter-change', {
|
|
390
|
+
bubbles: true,
|
|
391
|
+
detail: { filters: {} },
|
|
392
|
+
}));
|
|
393
|
+
this.#refreshFilterPanel();
|
|
394
|
+
});
|
|
395
|
+
panel.appendChild(clear);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#refreshFilterPanel() {
|
|
400
|
+
const ap = this.#activePopover;
|
|
401
|
+
if (!ap || ap.kind !== 'filter') return;
|
|
402
|
+
ap.panel.replaceChildren();
|
|
403
|
+
this.#fillFilterPanel(ap.panel);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// ── Sort panel ───────────────────────────────────────────────────────────
|
|
407
|
+
|
|
408
|
+
#fillSortPanel(panel) {
|
|
409
|
+
const target = this.#target;
|
|
410
|
+
if (!target?.columns?.length) {
|
|
411
|
+
panel.appendChild(emptyHint('No sortable columns'));
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const head = document.createElement('div');
|
|
416
|
+
head.setAttribute('data-popover-head', '');
|
|
417
|
+
head.textContent = 'Sort by';
|
|
418
|
+
panel.appendChild(head);
|
|
419
|
+
|
|
420
|
+
const sortState = target.sortState || [];
|
|
421
|
+
const dirByKey = new Map(sortState.map((s) => [s.key, s.dir]));
|
|
422
|
+
|
|
423
|
+
const list = document.createElement('div');
|
|
424
|
+
list.setAttribute('data-popover-list', '');
|
|
425
|
+
|
|
426
|
+
for (const col of target.columns) {
|
|
427
|
+
if (col.hidden) continue;
|
|
428
|
+
if (col.sortable === false) continue;
|
|
429
|
+
|
|
430
|
+
const row = document.createElement('button');
|
|
431
|
+
row.type = 'button';
|
|
432
|
+
row.setAttribute('data-sort-row', '');
|
|
433
|
+
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
|
+
if (dir) row.dataset.active = dir;
|
|
446
|
+
row.appendChild(indicator);
|
|
447
|
+
|
|
448
|
+
row.addEventListener('click', (e) => {
|
|
449
|
+
// Multi-sort with shift: forward to the table by simulating a header click
|
|
450
|
+
// (the table already owns the asc / desc / clear cycle). Falls back to no-op
|
|
451
|
+
// when the table is not present — same defensive shape as chart-legend's
|
|
452
|
+
// [for] target check.
|
|
453
|
+
const headerCell = target.querySelector(`:scope > [data-header] [data-sort-key="${col.key}"]`);
|
|
454
|
+
if (!headerCell) return;
|
|
455
|
+
const evt = new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: e.shiftKey });
|
|
456
|
+
headerCell.dispatchEvent(evt);
|
|
457
|
+
this.dispatchEvent(new CustomEvent('sort-change', {
|
|
458
|
+
bubbles: true,
|
|
459
|
+
detail: { sortState: target.sortState },
|
|
460
|
+
}));
|
|
461
|
+
this.#refreshSortPanel();
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
list.appendChild(row);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
panel.appendChild(list);
|
|
468
|
+
|
|
469
|
+
if (sortState.length) {
|
|
470
|
+
const clear = this.#mkPopoverAction('Clear sort', () => {
|
|
471
|
+
// Simulate cycle-through clicks until empty — but the table only stores
|
|
472
|
+
// one sort entry per key, and a non-shift click on an already-active key
|
|
473
|
+
// cycles to the opposite dir before clearing. Cleanest: clone columns and
|
|
474
|
+
// clear via a microtask using clicks on each active key until empty.
|
|
475
|
+
for (const s of [...sortState]) {
|
|
476
|
+
const headerCell = target.querySelector(`:scope > [data-header] [data-sort-key="${s.key}"]`);
|
|
477
|
+
if (!headerCell) continue;
|
|
478
|
+
// Two clicks toggle through asc → desc → clear when single-sort. With
|
|
479
|
+
// multi-sort entries we shift-click to remove individually.
|
|
480
|
+
const fire = (shift) => headerCell.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, shiftKey: shift }));
|
|
481
|
+
if (sortState.length > 1) {
|
|
482
|
+
// Shift-clicking through default→opposite→remove.
|
|
483
|
+
fire(true); fire(true);
|
|
484
|
+
} else {
|
|
485
|
+
// Single-sort: click twice non-shift to cycle through both dirs and clear.
|
|
486
|
+
fire(false); fire(false);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
this.dispatchEvent(new CustomEvent('sort-change', {
|
|
490
|
+
bubbles: true,
|
|
491
|
+
detail: { sortState: target.sortState },
|
|
492
|
+
}));
|
|
493
|
+
this.#refreshSortPanel();
|
|
494
|
+
});
|
|
495
|
+
panel.appendChild(clear);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
#refreshSortPanel() {
|
|
500
|
+
const ap = this.#activePopover;
|
|
501
|
+
if (!ap || ap.kind !== 'sort') return;
|
|
502
|
+
ap.panel.replaceChildren();
|
|
503
|
+
this.#fillSortPanel(ap.panel);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ── Columns panel ────────────────────────────────────────────────────────
|
|
507
|
+
|
|
508
|
+
#fillColumnsPanel(panel) {
|
|
509
|
+
const target = this.#target;
|
|
510
|
+
if (!target?.columns?.length) {
|
|
511
|
+
panel.appendChild(emptyHint('No columns'));
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const head = document.createElement('div');
|
|
516
|
+
head.setAttribute('data-popover-head', '');
|
|
517
|
+
head.textContent = 'Visible columns';
|
|
518
|
+
panel.appendChild(head);
|
|
519
|
+
|
|
520
|
+
const list = document.createElement('div');
|
|
521
|
+
list.setAttribute('data-popover-list', '');
|
|
522
|
+
|
|
523
|
+
for (const col of target.columns) {
|
|
524
|
+
const row = document.createElement('label');
|
|
525
|
+
row.setAttribute('data-columns-row', '');
|
|
526
|
+
|
|
527
|
+
const check = document.createElement('check-ui');
|
|
528
|
+
if (!col.hidden) check.setAttribute('checked', '');
|
|
529
|
+
check.dataset.key = col.key;
|
|
530
|
+
check.addEventListener('change', () => {
|
|
531
|
+
const next = target.columns.map((c) => (
|
|
532
|
+
c.key === col.key ? { ...c, hidden: !check.hasAttribute('checked') } : { ...c }
|
|
533
|
+
));
|
|
534
|
+
target.columns = next;
|
|
535
|
+
this.dispatchEvent(new CustomEvent('columns-change', {
|
|
536
|
+
bubbles: true,
|
|
537
|
+
detail: { hiddenColumns: next.filter((c) => c.hidden).map((c) => c.key) },
|
|
538
|
+
}));
|
|
539
|
+
});
|
|
540
|
+
row.appendChild(check);
|
|
541
|
+
|
|
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
|
+
list.appendChild(row);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
panel.appendChild(list);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function emptyHint(text) {
|
|
555
|
+
const el = document.createElement('text-ui');
|
|
556
|
+
el.setAttribute('data-popover-empty', '');
|
|
557
|
+
el.setAttribute('color', 'subtle');
|
|
558
|
+
el.setAttribute('variant', 'caption');
|
|
559
|
+
el.textContent = text;
|
|
560
|
+
return el;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
customElements.define('table-toolbar-ui', AdiaTableToolbar);
|
|
564
|
+
|
|
565
|
+
export { AdiaTableToolbar };
|
|
@@ -0,0 +1,160 @@
|
|
|
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
|
+
props:
|
|
14
|
+
for:
|
|
15
|
+
description: id-ref of the table-ui to control. Falls back to the first sibling table-ui within the same parent when omitted.
|
|
16
|
+
type: string
|
|
17
|
+
default: ""
|
|
18
|
+
reflect: true
|
|
19
|
+
text:
|
|
20
|
+
description: Title text shown on the left. Alternative to slotted heading content.
|
|
21
|
+
type: string
|
|
22
|
+
default: ""
|
|
23
|
+
count:
|
|
24
|
+
description: Optional count badge value shown next to the title. When unset, falls back to the row count of the bound table.
|
|
25
|
+
type: string
|
|
26
|
+
default: ""
|
|
27
|
+
filterable:
|
|
28
|
+
description: Show the Filter popover button.
|
|
29
|
+
type: boolean
|
|
30
|
+
default: true
|
|
31
|
+
reflect: true
|
|
32
|
+
sortable:
|
|
33
|
+
description: Show the Sort popover button.
|
|
34
|
+
type: boolean
|
|
35
|
+
default: true
|
|
36
|
+
reflect: true
|
|
37
|
+
columns:
|
|
38
|
+
description: Show the Columns visibility popover button.
|
|
39
|
+
type: boolean
|
|
40
|
+
default: true
|
|
41
|
+
reflect: true
|
|
42
|
+
searchable:
|
|
43
|
+
description: Show the search input.
|
|
44
|
+
type: boolean
|
|
45
|
+
default: true
|
|
46
|
+
reflect: true
|
|
47
|
+
placeholder:
|
|
48
|
+
description: Placeholder text for the search input.
|
|
49
|
+
type: string
|
|
50
|
+
default: Search...
|
|
51
|
+
variant:
|
|
52
|
+
description: Toolbar visual variant. `default` renders bare on parent surface; `card` adds the same chrome as a card-ui header.
|
|
53
|
+
type: string
|
|
54
|
+
default: default
|
|
55
|
+
enum:
|
|
56
|
+
- default
|
|
57
|
+
- card
|
|
58
|
+
reflect: true
|
|
59
|
+
events:
|
|
60
|
+
search:
|
|
61
|
+
description: "Debounced search query change. Detail: { value }."
|
|
62
|
+
filter-change:
|
|
63
|
+
description: "Filter set changed. Detail: { filters }."
|
|
64
|
+
sort-change:
|
|
65
|
+
description: "Sort state changed. Detail: { sortState }."
|
|
66
|
+
columns-change:
|
|
67
|
+
description: "Column visibility changed. Detail: { hiddenColumns }."
|
|
68
|
+
slots:
|
|
69
|
+
default:
|
|
70
|
+
description: Optional title content. Used when [text] is empty.
|
|
71
|
+
actions:
|
|
72
|
+
description: Trailing action area — primary buttons (e.g. "New row") rendered after the search input.
|
|
73
|
+
states:
|
|
74
|
+
- name: idle
|
|
75
|
+
description: Default, ready for interaction.
|
|
76
|
+
traits: []
|
|
77
|
+
tokens:
|
|
78
|
+
--table-toolbar-gap:
|
|
79
|
+
description: Gap between toolbar clusters
|
|
80
|
+
--table-toolbar-py:
|
|
81
|
+
description: Vertical padding
|
|
82
|
+
--table-toolbar-px:
|
|
83
|
+
description: Horizontal padding
|
|
84
|
+
--table-toolbar-bg:
|
|
85
|
+
description: Toolbar background (variant=card)
|
|
86
|
+
--table-toolbar-border:
|
|
87
|
+
description: Toolbar border color (variant=card)
|
|
88
|
+
--table-toolbar-radius:
|
|
89
|
+
description: Toolbar corner radius (variant=card)
|
|
90
|
+
--table-toolbar-title-fg:
|
|
91
|
+
description: Title text color
|
|
92
|
+
--table-toolbar-title-size:
|
|
93
|
+
description: Title font size
|
|
94
|
+
--table-toolbar-title-weight:
|
|
95
|
+
description: Title font weight
|
|
96
|
+
--table-toolbar-popover-bg:
|
|
97
|
+
description: Popover background
|
|
98
|
+
--table-toolbar-popover-fg:
|
|
99
|
+
description: Popover text color
|
|
100
|
+
--table-toolbar-popover-border:
|
|
101
|
+
description: Popover border
|
|
102
|
+
--table-toolbar-popover-radius:
|
|
103
|
+
description: Popover radius
|
|
104
|
+
--table-toolbar-popover-shadow:
|
|
105
|
+
description: Popover shadow
|
|
106
|
+
--table-toolbar-popover-pad:
|
|
107
|
+
description: Popover padding
|
|
108
|
+
--table-toolbar-popover-gap:
|
|
109
|
+
description: Popover content gap
|
|
110
|
+
--table-toolbar-popover-min:
|
|
111
|
+
description: Popover minimum width
|
|
112
|
+
a2ui:
|
|
113
|
+
rules: []
|
|
114
|
+
anti_patterns: []
|
|
115
|
+
examples:
|
|
116
|
+
- name: members-toolbar
|
|
117
|
+
description: Members table with filter/sort/columns/search wired to a sibling table-ui.
|
|
118
|
+
a2ui: >-
|
|
119
|
+
[
|
|
120
|
+
{"id": "root", "component": "Column", "gap": "3", "children": ["bar", "card"]},
|
|
121
|
+
{"id": "bar", "component": "TableToolbar", "for": "members", "text": "All Employees", "count": "32"},
|
|
122
|
+
{"id": "card", "component": "Card", "children": ["sec"]},
|
|
123
|
+
{"id": "sec", "component": "Section", "bleed": true, "children": ["tbl"]},
|
|
124
|
+
{"id": "tbl", "component": "Table", "id": "members", "sortable": true, "raw": true}
|
|
125
|
+
]
|
|
126
|
+
keywords:
|
|
127
|
+
- table-toolbar
|
|
128
|
+
- data-grid
|
|
129
|
+
- data-grid-toolbar
|
|
130
|
+
- filter
|
|
131
|
+
- sort
|
|
132
|
+
- columns
|
|
133
|
+
- search
|
|
134
|
+
- directory
|
|
135
|
+
- admin
|
|
136
|
+
- backoffice
|
|
137
|
+
- listing
|
|
138
|
+
- records
|
|
139
|
+
synonyms:
|
|
140
|
+
data-grid:
|
|
141
|
+
- table-toolbar
|
|
142
|
+
- table
|
|
143
|
+
data-grid-toolbar:
|
|
144
|
+
- table-toolbar
|
|
145
|
+
- table
|
|
146
|
+
filter:
|
|
147
|
+
- table-toolbar
|
|
148
|
+
- table
|
|
149
|
+
sort:
|
|
150
|
+
- table-toolbar
|
|
151
|
+
- table
|
|
152
|
+
columns:
|
|
153
|
+
- table-toolbar
|
|
154
|
+
- table
|
|
155
|
+
related:
|
|
156
|
+
- table
|
|
157
|
+
- search
|
|
158
|
+
- button
|
|
159
|
+
- badge
|
|
160
|
+
- popover
|
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.18",
|
|
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": {
|
package/styles/components.css
CHANGED
|
@@ -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";
|