@adia-ai/web-components 0.8.44 → 0.8.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/components/card/card.css +12 -23
- package/components/card/card.yaml +11 -0
- package/components/chart/chart.a2ui.json +16 -1
- package/components/chart/chart.class.js +611 -41
- package/components/chart/chart.css +174 -0
- package/components/chart/chart.d.ts +5 -1
- package/components/chart/chart.yaml +45 -1
- package/components/context-menu/context-menu.a2ui.json +8 -3
- package/components/context-menu/context-menu.class.js +46 -5
- package/components/context-menu/context-menu.d.ts +6 -3
- package/components/context-menu/context-menu.examples.md +2 -2
- package/components/context-menu/context-menu.yaml +22 -5
- package/components/field/field.css +24 -2
- package/components/index.js +1 -0
- package/components/input/input.css +7 -0
- package/components/nav/nav.a2ui.json +2 -2
- package/components/nav/nav.css +1 -1
- package/components/nav/nav.d.ts +1 -1
- package/components/nav/nav.yaml +14 -3
- package/components/nav-group/nav-group.css +37 -3
- package/components/pagination/pagination.class.js +52 -22
- package/components/search/search.class.js +39 -5
- package/components/select/select.a2ui.json +5 -0
- package/components/select/select.class.js +20 -0
- package/components/select/select.css +25 -0
- package/components/select/select.d.ts +2 -0
- package/components/select/select.yaml +12 -0
- package/components/table/cell-types.js +9 -0
- package/components/table/table.a2ui.json +19 -4
- package/components/table/table.class.js +410 -45
- package/components/table/table.css +7 -4
- package/components/table/table.d.ts +9 -3
- package/components/table/table.yaml +115 -9
- package/components/table-footer/table-footer.a2ui.json +150 -0
- package/components/table-footer/table-footer.class.js +391 -0
- package/components/table-footer/table-footer.css +64 -0
- package/components/table-footer/table-footer.d.ts +39 -0
- package/components/table-footer/table-footer.examples.md +46 -0
- package/components/table-footer/table-footer.js +17 -0
- package/components/table-footer/table-footer.yaml +219 -0
- package/components/table-toolbar/table-toolbar.a2ui.json +15 -0
- package/components/table-toolbar/table-toolbar.class.js +61 -17
- package/components/table-toolbar/table-toolbar.css +34 -0
- package/components/table-toolbar/table-toolbar.d.ts +10 -0
- package/components/table-toolbar/table-toolbar.yaml +58 -15
- package/core/data-stream.js +37 -2
- package/core/index.d.ts +1 -0
- package/core/index.js +1 -0
- package/core/provider.d.ts +9 -13
- package/core/provider.js +9 -113
- package/core/store.d.ts +46 -0
- package/core/store.js +89 -0
- package/custom-elements.json +192 -8
- package/dist/host.min.css +1 -1
- package/dist/host.sheet.js +1 -1
- package/dist/theme-provider.min.js +1 -1
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +96 -96
- package/dist/web-components.sheet.js +1 -1
- package/package.json +1 -1
- package/patterns/chart-in-card/chart-in-card.examples.html +36 -9
- package/patterns/new-enrollments/new-enrollments.examples.html +140 -0
- package/patterns/new-enrollments/new-enrollments.html +54 -0
- package/patterns/table-in-card/table-in-card.examples.html +168 -0
- package/patterns/table-in-card/table-in-card.examples.js +139 -0
- package/patterns/table-in-card/table-in-card.html +86 -0
- package/styles/api/sizing.css +46 -0
- package/styles/components.css +1 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
$schema: ../../../../scripts/schemas/component.yaml.schema.json
|
|
2
|
+
name: UITableFooter
|
|
3
|
+
tag: table-footer-ui
|
|
4
|
+
status: beta
|
|
5
|
+
component: TableFooter
|
|
6
|
+
category: agent
|
|
7
|
+
version: 1
|
|
8
|
+
description: >-
|
|
9
|
+
Footer / companion bar for a sibling table-ui (gh#1807, ADR-0080) — a
|
|
10
|
+
"Showing X–Y of N" range label paired with a composed pagination-ui.
|
|
11
|
+
Derivation-model API: set `page` / `page-size` / `range-total` and the
|
|
12
|
+
footer derives total pages and the range window itself, so it never
|
|
13
|
+
drifts out of sync with its own pager. Wired to the target table via a
|
|
14
|
+
[for] id-ref and CustomEvents only (ADR-0079/ADR-0080) — the footer never
|
|
15
|
+
writes a property or calls a method on the bound table. Drop below (or
|
|
16
|
+
inside a card-ui footer alongside) any table-ui to add the standard
|
|
17
|
+
data-grid pager + range summary without re-implementing pagination-ui's
|
|
18
|
+
ellipsis truncation, keyboard handling, or ARIA.
|
|
19
|
+
# Per ADR-0027 — primitives that programmatically create other primitives
|
|
20
|
+
# do NOT auto-import them. Consumer (or demo shell) must explicitly import.
|
|
21
|
+
composes:
|
|
22
|
+
- pagination-ui
|
|
23
|
+
props:
|
|
24
|
+
for:
|
|
25
|
+
description: id-ref of the table-ui to control. Falls back to the first sibling table-ui within the same parent when omitted.
|
|
26
|
+
type: string
|
|
27
|
+
default: ""
|
|
28
|
+
reflect: true
|
|
29
|
+
page:
|
|
30
|
+
description: >-
|
|
31
|
+
Current page, 1-based. Explicit attributes always win over derivation
|
|
32
|
+
(per-attribute, not all-or-nothing) — see page-size/range-total.
|
|
33
|
+
type: number
|
|
34
|
+
default: 1
|
|
35
|
+
reflect: true
|
|
36
|
+
page-size:
|
|
37
|
+
description: >-
|
|
38
|
+
Rows per page. Default 0 (unknown) is the ORDINARY default state, not
|
|
39
|
+
an exotic edge case: with page-size absent/0 and range-total present,
|
|
40
|
+
the range label renders in count-only form ("128 items") and the
|
|
41
|
+
pager does not render — the derivation math (pages = range-total /
|
|
42
|
+
page-size) is undefined without a page size. In a client-mode
|
|
43
|
+
composition (the resolved target has [paginate] > 0) and this
|
|
44
|
+
attribute is unset, the footer derives page-size from the target's
|
|
45
|
+
own [paginate] value.
|
|
46
|
+
type: number
|
|
47
|
+
default: 0
|
|
48
|
+
reflect: true
|
|
49
|
+
range-total:
|
|
50
|
+
description: >-
|
|
51
|
+
Total ROWS across all pages. Absent (the default) means "not yet
|
|
52
|
+
known" (loading) — never "zero"; both the range label and the pager
|
|
53
|
+
stay hidden until this is set. Explicit range-total="0" is the
|
|
54
|
+
confirmed-empty state (pager hidden; the `empty` slot renders in the
|
|
55
|
+
range label's position when supplied). Named identically to
|
|
56
|
+
table-toolbar-ui's [range-total] and table-ui's own [range-total]
|
|
57
|
+
(gh#1754, ADR-0082) and deliberately NOT `total` (pagination-ui's
|
|
58
|
+
`total` is total PAGES — the ADR-0063 B5 collision rule). When this
|
|
59
|
+
attribute is unset, the footer derives it from the resolved target
|
|
60
|
+
via a two-branch ladder (REQ-W-006, amended 0.2.0/ADR-0082):
|
|
61
|
+
(1) the target's own [range-total] attribute, when present — the
|
|
62
|
+
table-authoritative SERVER mode (gh#1754 Shape 1's second lawful
|
|
63
|
+
composition); the identity rule: the footer's range-total derives
|
|
64
|
+
from the table's range-total, never the table's filteredCount, and
|
|
65
|
+
this branch wins even when the target's [paginate] is also > 0;
|
|
66
|
+
(2) otherwise, when the target has [paginate] > 0, CLIENT mode: the
|
|
67
|
+
target's filtered row count (search + column filters applied, before
|
|
68
|
+
pagination). Refreshes on the target's `page` / `filter-change` /
|
|
69
|
+
`sort` events in either branch. Widened named staleness edge: a
|
|
70
|
+
programmatic `.data` swap on the target — or a `range-total` update
|
|
71
|
+
on the target without an accompanying page move — emits no event;
|
|
72
|
+
either re-set this footer's own attributes after such a change, or
|
|
73
|
+
accept staleness until the target's next event.
|
|
74
|
+
type: number
|
|
75
|
+
default: 0
|
|
76
|
+
reflect: true
|
|
77
|
+
siblings:
|
|
78
|
+
description: Passed through to the composed pagination-ui's own [siblings] (page-number window size on each side of the current page).
|
|
79
|
+
type: number
|
|
80
|
+
default: 1
|
|
81
|
+
reflect: true
|
|
82
|
+
size:
|
|
83
|
+
description: Passed through to the composed pagination-ui's own [size] (sm | md | lg, the universal size system).
|
|
84
|
+
type: string
|
|
85
|
+
default: md
|
|
86
|
+
reflect: true
|
|
87
|
+
events:
|
|
88
|
+
footer-page:
|
|
89
|
+
description: >-
|
|
90
|
+
gh#1807, ADR-0080 (events-only interaction contract, extending
|
|
91
|
+
ADR-0079's sender-prefixed toolbar-* precedent to a second sender) —
|
|
92
|
+
dispatched directly at the resolved [for] target on every pager
|
|
93
|
+
interaction. Detail: { page } (1-based). table-ui listens for this on
|
|
94
|
+
itself, applied only when [paginate] > 0 (clamped to the valid page
|
|
95
|
+
range; a non-numeric/NaN detail.page is a no-op); at [paginate="0"]
|
|
96
|
+
the command is a documented no-op on the table. Additive: any
|
|
97
|
+
consumer, not only table-footer-ui, may dispatch this at a table-ui.
|
|
98
|
+
page-change:
|
|
99
|
+
description: >-
|
|
100
|
+
The footer's own consumer-facing notification — a distinct name from
|
|
101
|
+
the inbound footer-page command (ADR-0079's command≠notification
|
|
102
|
+
separation). Fires on every pager interaction, in both client- and
|
|
103
|
+
server-mode. Detail: { page } (1-based). This is the server-mode
|
|
104
|
+
fetch hook: a consumer listens for it, fetches the new page, then
|
|
105
|
+
sets `table.data` and the footer's own `page` (and `range-total` if
|
|
106
|
+
the server's total moved) — the footer never optimistically advances
|
|
107
|
+
its own state in server-mode.
|
|
108
|
+
slots:
|
|
109
|
+
empty:
|
|
110
|
+
description: >-
|
|
111
|
+
Renders in place of the range label — the same [data-range] region —
|
|
112
|
+
when [range-total] is EXPLICITLY present and equal to "0" (checked
|
|
113
|
+
via hasAttribute, never the computed value alone, so an unset
|
|
114
|
+
range-total during loading never flashes this content). Mutually
|
|
115
|
+
exclusive with a positive [range-total]: the normal "Showing X–Y of N"
|
|
116
|
+
(or count-only) text always wins whenever range-total resolves
|
|
117
|
+
positive. Plain author-supplied markup — table-footer-ui does not
|
|
118
|
+
template or constrain its shape. Naming matches table-toolbar-ui's own
|
|
119
|
+
`empty` slot convention.
|
|
120
|
+
states:
|
|
121
|
+
- name: idle
|
|
122
|
+
description: Default, ready for interaction.
|
|
123
|
+
traits: []
|
|
124
|
+
tokens:
|
|
125
|
+
--table-footer-gap:
|
|
126
|
+
description: Gap between the range label and the pager
|
|
127
|
+
--table-footer-py:
|
|
128
|
+
description: Vertical padding
|
|
129
|
+
--table-footer-px:
|
|
130
|
+
description: Horizontal padding
|
|
131
|
+
--table-footer-summary-fg:
|
|
132
|
+
description: Range-label text color
|
|
133
|
+
--table-footer-summary-size:
|
|
134
|
+
description: Range-label font size
|
|
135
|
+
a2ui:
|
|
136
|
+
rules:
|
|
137
|
+
- >-
|
|
138
|
+
Pair <table-footer-ui> with <table-ui> via [for="<table-id>"] (or
|
|
139
|
+
rely on first-sibling fallback when both are inside the same parent).
|
|
140
|
+
Client-mode composition (a [paginate]-sliced table): set [no-pager]
|
|
141
|
+
on the table-ui so its own internal pagination bar doesn't render
|
|
142
|
+
alongside the footer's composed one — table-footer-ui never
|
|
143
|
+
auto-suppresses it for you.
|
|
144
|
+
- >-
|
|
145
|
+
Server-mode composition (gh#1754 Shape 1 — the server returns the
|
|
146
|
+
page's rows plus the total row count): set [range-total] to the
|
|
147
|
+
server's total, [page] / [page-size] from your own request state, and
|
|
148
|
+
listen for the footer's `page-change` event to fetch the next page —
|
|
149
|
+
the footer does not optimistically advance its own [page] in
|
|
150
|
+
server-mode; write it back after the fetch resolves.
|
|
151
|
+
- >-
|
|
152
|
+
The toolbar's [count] badge and this footer's range label coexist by
|
|
153
|
+
design — they answer different questions (the collection summary vs.
|
|
154
|
+
the page-window position) and neither replaces the other. In a
|
|
155
|
+
composition that includes a table-footer-ui, leave the toolbar's
|
|
156
|
+
[range-start]/[range-end]/[range-total] unset — the range summary's
|
|
157
|
+
home is the footer.
|
|
158
|
+
- >-
|
|
159
|
+
Canonical composition: card-ui header (title + primary action) →
|
|
160
|
+
a plain <section> wrapping a title-less <table-toolbar-ui for> → a
|
|
161
|
+
<section bleed> wrapping the <table-ui id> → a <footer divider>
|
|
162
|
+
wrapping this <table-footer-ui for> — see the table-in-card pattern.
|
|
163
|
+
- >-
|
|
164
|
+
Use <span slot="empty"> (or any markup) inside a table-footer-ui that
|
|
165
|
+
also sets [range-total="0"] to show a "no results" message in the
|
|
166
|
+
range label's own position, without a layout shift versus the normal
|
|
167
|
+
range text. Only fires on an EXPLICIT range-total="0" — omitting
|
|
168
|
+
range-total entirely (loading) never shows it.
|
|
169
|
+
anti_patterns:
|
|
170
|
+
- wrong: '<table-footer-ui range-start="1" range-end="10" range-total="128"></table-footer-ui>'
|
|
171
|
+
why: >-
|
|
172
|
+
table-footer-ui is derivation-only (REQ-F-002) — there is no
|
|
173
|
+
[range-start]/[range-end] attribute pair (that shape belongs to
|
|
174
|
+
table-toolbar-ui, which has no page/page-size knowledge of its own).
|
|
175
|
+
Setting [page] + [page-size] + [range-total] derives the range window
|
|
176
|
+
instead; a permanent three-way drift hazard is exactly what the
|
|
177
|
+
derivation model exists to avoid.
|
|
178
|
+
fix: '<table-footer-ui page="1" page-size="10" range-total="128"></table-footer-ui>'
|
|
179
|
+
examples:
|
|
180
|
+
- name: table-footer-server-mode
|
|
181
|
+
description: Server-mode footer (gh#1754 Shape 1) paired with a for=-bound table-ui.
|
|
182
|
+
a2ui: >-
|
|
183
|
+
[
|
|
184
|
+
{"id": "root", "component": "Column", "gap": "0", "children": ["tests", "ftr"]},
|
|
185
|
+
{"id": "tests", "component": "Table", "raw": true},
|
|
186
|
+
{"id": "ftr", "component": "TableFooter", "for": "tests", "page": 1, "page-size": 10, "range-total": 128}
|
|
187
|
+
]
|
|
188
|
+
- name: table-footer-client-mode
|
|
189
|
+
description: Client-mode footer bound to a [paginate]d table-ui with its own internal pager suppressed via [no-pager].
|
|
190
|
+
a2ui: >-
|
|
191
|
+
[
|
|
192
|
+
{"id": "root", "component": "Column", "gap": "0", "children": ["tests-c", "ftr"]},
|
|
193
|
+
{"id": "tests-c", "component": "Table", "paginate": 10, "no-pager": true, "raw": true},
|
|
194
|
+
{"id": "ftr", "component": "TableFooter", "for": "tests-c"}
|
|
195
|
+
]
|
|
196
|
+
keywords:
|
|
197
|
+
- table-footer
|
|
198
|
+
- pagination
|
|
199
|
+
- pager
|
|
200
|
+
- range
|
|
201
|
+
- showing
|
|
202
|
+
- data-grid
|
|
203
|
+
- directory
|
|
204
|
+
- admin
|
|
205
|
+
- backoffice
|
|
206
|
+
- listing
|
|
207
|
+
- records
|
|
208
|
+
synonyms:
|
|
209
|
+
pagination:
|
|
210
|
+
- table-footer
|
|
211
|
+
- pagination
|
|
212
|
+
pager:
|
|
213
|
+
- table-footer
|
|
214
|
+
- pagination
|
|
215
|
+
related:
|
|
216
|
+
- table
|
|
217
|
+
- table-toolbar
|
|
218
|
+
- pagination
|
|
219
|
+
- card
|
|
@@ -142,6 +142,21 @@
|
|
|
142
142
|
},
|
|
143
143
|
"sort-change": {
|
|
144
144
|
"description": "Sort state changed. Detail: { sortState }."
|
|
145
|
+
},
|
|
146
|
+
"toolbar-columns-set": {
|
|
147
|
+
"description": "gh#1764/#1780, ADR-0079 — dispatched directly at the resolved [for] target in place of the pre-#1780 direct `target.columns =` write. table-ui listens for this on itself. Detail: { columns }."
|
|
148
|
+
},
|
|
149
|
+
"toolbar-filter-clear": {
|
|
150
|
+
"description": "gh#1764/#1780, ADR-0079 — dispatched directly at the resolved [for] target in place of the pre-#1780 direct `target.clearFilters()` call. table-ui listens for this on itself. No detail."
|
|
151
|
+
},
|
|
152
|
+
"toolbar-filter-set": {
|
|
153
|
+
"description": "gh#1764/#1780, ADR-0079 — dispatched directly at the resolved [for] target in place of the pre-#1780 direct `target.setFilter()` call. table-ui listens for this on itself. Detail: { key, value, op }; a null `value` clears that one column's filter."
|
|
154
|
+
},
|
|
155
|
+
"toolbar-paginate": {
|
|
156
|
+
"description": "gh#1764/#1780, ADR-0079 — dispatched directly at the resolved [for] target in place of the pre-#1780 direct `target.paginate =` write. table-ui listens for this on itself. Detail: { pageSize }."
|
|
157
|
+
},
|
|
158
|
+
"toolbar-search": {
|
|
159
|
+
"description": "gh#1764/#1780, ADR-0079 (events-only interaction contract) — dispatched directly at the resolved [for] target (not bubbled from this element) in place of the pre-#1780 direct `.search =` write. table-ui listens for this on itself. Detail: { value }."
|
|
145
160
|
}
|
|
146
161
|
},
|
|
147
162
|
"examples": [
|
|
@@ -29,11 +29,22 @@
|
|
|
29
29
|
* Popovers use the platform Popover API + core/anchor.js, the same primitives
|
|
30
30
|
* that menu-ui / popover-ui / toolbar-ui already use in this package.
|
|
31
31
|
*
|
|
32
|
-
* State flow
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
32
|
+
* State flow — events-only interaction contract (gh#1764/#1780, ADR-0079;
|
|
33
|
+
* mirrors chart-legend-ui↔chart-ui's own bubbling-CustomEvent model). The
|
|
34
|
+
* toolbar never writes a property or calls a method on the bound table
|
|
35
|
+
* directly — every command is a `toolbar-*` CustomEvent dispatched AT the
|
|
36
|
+
* resolved target, which table.class.js listens for in its own
|
|
37
|
+
* connected()/disconnected() and applies via its existing public surface:
|
|
38
|
+
* search → `toolbar-search` { value } → table.search =
|
|
39
|
+
* filters → `toolbar-filter-set` { key, value, op } → table.setFilter()
|
|
40
|
+
* filters clear → `toolbar-filter-clear` {} → table.clearFilters()
|
|
41
|
+
* sort → simulated click on table's [data-sort-key] header (already
|
|
42
|
+
* event-shaped — a real DOM event dispatched at the target,
|
|
43
|
+
* not a property write; left as-is, see gh#1780's PR notes)
|
|
44
|
+
* column hidden → `toolbar-columns-set` { columns } → table.columns =
|
|
45
|
+
* page size → `toolbar-paginate` { pageSize } → table.paginate =
|
|
46
|
+
* The table→toolbar direction (`sort`, `filter-change` listened on #target)
|
|
47
|
+
* was already events-only and is unchanged.
|
|
37
48
|
*/
|
|
38
49
|
|
|
39
50
|
import { UIElement } from '../../core/element.js';
|
|
@@ -846,11 +857,16 @@ export class UITableToolbar extends UIElement {
|
|
|
846
857
|
#onPageSizeChange = (e) => {
|
|
847
858
|
const pageSize = Number(e.detail?.value ?? this.querySelector(':scope [data-page-size]')?.value) || 0;
|
|
848
859
|
this.pageSize = pageSize;
|
|
849
|
-
//
|
|
850
|
-
//
|
|
851
|
-
//
|
|
852
|
-
//
|
|
853
|
-
if (this.#target)
|
|
860
|
+
// gh#1764/#1780 (events-only interaction contract, ADR-0079) — the
|
|
861
|
+
// bound table applies its own [paginate] through the `toolbar-paginate`
|
|
862
|
+
// command event rather than a direct property write; table.class.js
|
|
863
|
+
// listens for it in connected() and sets `this.paginate` itself.
|
|
864
|
+
if (this.#target) {
|
|
865
|
+
this.#target.dispatchEvent(new CustomEvent('toolbar-paginate', {
|
|
866
|
+
bubbles: true,
|
|
867
|
+
detail: { pageSize },
|
|
868
|
+
}));
|
|
869
|
+
}
|
|
854
870
|
this.dispatchEvent(new CustomEvent('page-size-change', {
|
|
855
871
|
bubbles: true,
|
|
856
872
|
detail: { pageSize },
|
|
@@ -902,7 +918,15 @@ export class UITableToolbar extends UIElement {
|
|
|
902
918
|
|
|
903
919
|
#onSearch = (e) => {
|
|
904
920
|
const value = e.detail?.value ?? '';
|
|
905
|
-
|
|
921
|
+
// gh#1764/#1780 (events-only interaction contract, ADR-0079) —
|
|
922
|
+
// `toolbar-search` replaces the direct `.search =` write; table.class.js
|
|
923
|
+
// listens for it in connected() and sets `this.search` itself.
|
|
924
|
+
if (this.#target) {
|
|
925
|
+
this.#target.dispatchEvent(new CustomEvent('toolbar-search', {
|
|
926
|
+
bubbles: true,
|
|
927
|
+
detail: { value },
|
|
928
|
+
}));
|
|
929
|
+
}
|
|
906
930
|
this.dispatchEvent(new CustomEvent('search', {
|
|
907
931
|
bubbles: true,
|
|
908
932
|
detail: { value },
|
|
@@ -1044,8 +1068,14 @@ export class UITableToolbar extends UIElement {
|
|
|
1044
1068
|
}
|
|
1045
1069
|
sel.addEventListener('change', () => {
|
|
1046
1070
|
const v = sel.value || '';
|
|
1047
|
-
|
|
1048
|
-
|
|
1071
|
+
// gh#1764/#1780 (events-only interaction contract, ADR-0079) —
|
|
1072
|
+
// `toolbar-filter-set` replaces the direct target.setFilter()
|
|
1073
|
+
// call; table.class.js applies it synchronously in its own
|
|
1074
|
+
// listener, so `target.filters` below already reflects it.
|
|
1075
|
+
target.dispatchEvent(new CustomEvent('toolbar-filter-set', {
|
|
1076
|
+
bubbles: true,
|
|
1077
|
+
detail: { key: col.key, value: v || null, op: 'select' },
|
|
1078
|
+
}));
|
|
1049
1079
|
this.dispatchEvent(new CustomEvent('filter-change', {
|
|
1050
1080
|
bubbles: true,
|
|
1051
1081
|
detail: { filters: target.filters },
|
|
@@ -1061,8 +1091,12 @@ export class UITableToolbar extends UIElement {
|
|
|
1061
1091
|
if (current?.op === 'contains') input.value = current.value ?? '';
|
|
1062
1092
|
input.addEventListener('input', () => {
|
|
1063
1093
|
const v = input.value;
|
|
1064
|
-
|
|
1065
|
-
|
|
1094
|
+
// gh#1764/#1780 (events-only interaction contract, ADR-0079) —
|
|
1095
|
+
// see the select-filter handler above for the same normalization.
|
|
1096
|
+
target.dispatchEvent(new CustomEvent('toolbar-filter-set', {
|
|
1097
|
+
bubbles: true,
|
|
1098
|
+
detail: { key: col.key, value: v || null, op: 'contains' },
|
|
1099
|
+
}));
|
|
1066
1100
|
this.dispatchEvent(new CustomEvent('filter-change', {
|
|
1067
1101
|
bubbles: true,
|
|
1068
1102
|
detail: { filters: target.filters },
|
|
@@ -1078,7 +1112,11 @@ export class UITableToolbar extends UIElement {
|
|
|
1078
1112
|
|
|
1079
1113
|
if (Object.keys(filters).length) {
|
|
1080
1114
|
const clear = this.#mkPopoverAction('Clear all filters', () => {
|
|
1081
|
-
|
|
1115
|
+
// gh#1764/#1780 (events-only interaction contract, ADR-0079) —
|
|
1116
|
+
// `toolbar-filter-clear` replaces the direct target.clearFilters()
|
|
1117
|
+
// call; table.class.js applies it synchronously in its own
|
|
1118
|
+
// listener before #refreshFilterPanel() re-reads target.filters.
|
|
1119
|
+
target.dispatchEvent(new CustomEvent('toolbar-filter-clear', { bubbles: true }));
|
|
1082
1120
|
this.dispatchEvent(new CustomEvent('filter-change', {
|
|
1083
1121
|
bubbles: true,
|
|
1084
1122
|
detail: { filters: {} },
|
|
@@ -1227,7 +1265,13 @@ export class UITableToolbar extends UIElement {
|
|
|
1227
1265
|
const next = target.columns.map((c) => (
|
|
1228
1266
|
c.key === col.key ? { ...c, hidden: !check.hasAttribute('checked') } : { ...c }
|
|
1229
1267
|
));
|
|
1230
|
-
|
|
1268
|
+
// gh#1764/#1780 (events-only interaction contract, ADR-0079) —
|
|
1269
|
+
// `toolbar-columns-set` replaces the direct target.columns= write;
|
|
1270
|
+
// table.class.js applies it synchronously in its own listener.
|
|
1271
|
+
target.dispatchEvent(new CustomEvent('toolbar-columns-set', {
|
|
1272
|
+
bubbles: true,
|
|
1273
|
+
detail: { columns: next },
|
|
1274
|
+
}));
|
|
1231
1275
|
this.dispatchEvent(new CustomEvent('columns-change', {
|
|
1232
1276
|
bubbles: true,
|
|
1233
1277
|
detail: { hiddenColumns: next.filter((c) => c.hidden).map((c) => c.key) },
|
|
@@ -163,6 +163,40 @@
|
|
|
163
163
|
width: var(--button-height);
|
|
164
164
|
}
|
|
165
165
|
|
|
166
|
+
/* ═══════ Actions-leading icon-only reduction ═══════ (gh#1748) — extends
|
|
167
|
+
the SAME icon-only collapse above to a plain <button-ui> a consumer
|
|
168
|
+
slots into [slot="actions-leading"] (gh#1649, e.g. the app-owned
|
|
169
|
+
Filter/Columns triggers on the ratified adiav2 row), so it reads as
|
|
170
|
+
part of the same toolbar affordance cluster rather than staying
|
|
171
|
+
full-width while its native siblings compact. A separate rule block
|
|
172
|
+
(rather than folding into [data-toolbar-btn]'s own selector list above)
|
|
173
|
+
keeps that block's literal text byte-identical for its own
|
|
174
|
+
source-assertion tests. Same breakpoint/pin pair, same
|
|
175
|
+
aria-label-survives guarantee (button-ui's own REQ-X-001 stamp,
|
|
176
|
+
unaffected here) — light-DOM real insertion point (no shadow
|
|
177
|
+
boundary), so a plain descendant tag selector reaches it directly, no
|
|
178
|
+
`::slotted()` needed. Literal `40rem` bound to
|
|
179
|
+
--table-toolbar-bp-icon-only's default, same as the native block. */
|
|
180
|
+
@container table-toolbar (max-width: 40rem) {
|
|
181
|
+
:scope:is(:not([stage]), [stage=""]) [data-actions-leading] button-ui[text]::after {
|
|
182
|
+
content: none;
|
|
183
|
+
}
|
|
184
|
+
:scope:is(:not([stage]), [stage=""]) [data-actions-leading] button-ui {
|
|
185
|
+
--button-px: 0;
|
|
186
|
+
|
|
187
|
+
width: var(--button-height);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
:scope:is([stage="icon-only"], [stage="overflow"]) [data-actions-leading] button-ui[text]::after {
|
|
192
|
+
content: none;
|
|
193
|
+
}
|
|
194
|
+
:scope:is([stage="icon-only"], [stage="overflow"]) [data-actions-leading] button-ui {
|
|
195
|
+
--button-px: 0;
|
|
196
|
+
|
|
197
|
+
width: var(--button-height);
|
|
198
|
+
}
|
|
199
|
+
|
|
166
200
|
/* ═══════ Range-summary conditional compact/hide ═══════ (ADR-0076
|
|
167
201
|
REQ-S-003, step 4) — at the SAME icon-only breakpoint as above:
|
|
168
202
|
- [data-page-size] present AND visible (no [hidden]) → its own
|
|
@@ -17,6 +17,11 @@ export type TableToolbarFilterChangeEvent = CustomEvent<unknown>;
|
|
|
17
17
|
export type TableToolbarPageSizeChangeEvent = CustomEvent<unknown>;
|
|
18
18
|
export type TableToolbarSearchEvent = CustomEvent<unknown>;
|
|
19
19
|
export type TableToolbarSortChangeEvent = CustomEvent<unknown>;
|
|
20
|
+
export type TableToolbarToolbarColumnsSetEvent = CustomEvent<unknown>;
|
|
21
|
+
export type TableToolbarToolbarFilterClearEvent = CustomEvent<unknown>;
|
|
22
|
+
export type TableToolbarToolbarFilterSetEvent = CustomEvent<unknown>;
|
|
23
|
+
export type TableToolbarToolbarPaginateEvent = CustomEvent<unknown>;
|
|
24
|
+
export type TableToolbarToolbarSearchEvent = CustomEvent<unknown>;
|
|
20
25
|
|
|
21
26
|
export class UITableToolbar extends UIElement {
|
|
22
27
|
/** Suppress all four native controls/search at once (filter, sort, columns, search) as additive sugar over noFilter/noSort/noColumns/ noSearch (ADR-0076, ADIA2-9123). Precedence is pure, absolute OR — while set, all four stay off regardless of any individual no-* attribute's own value, with no partial re-enable path; to re-enable one control, remove chrome-only entirely and set the other three no-* attributes explicitly instead. The four granular attributes are not deprecated or removed — they remain the independently-addressable shipped API; chrome-only never replaces them, it's a convenience preset on top. */
|
|
@@ -57,6 +62,11 @@ export class UITableToolbar extends UIElement {
|
|
|
57
62
|
addEventListener(type: 'page-size-change', listener: (ev: TableToolbarPageSizeChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
58
63
|
addEventListener(type: 'search', listener: (ev: TableToolbarSearchEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
59
64
|
addEventListener(type: 'sort-change', listener: (ev: TableToolbarSortChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
65
|
+
addEventListener(type: 'toolbar-columns-set', listener: (ev: TableToolbarToolbarColumnsSetEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
66
|
+
addEventListener(type: 'toolbar-filter-clear', listener: (ev: TableToolbarToolbarFilterClearEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
67
|
+
addEventListener(type: 'toolbar-filter-set', listener: (ev: TableToolbarToolbarFilterSetEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
68
|
+
addEventListener(type: 'toolbar-paginate', listener: (ev: TableToolbarToolbarPaginateEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
69
|
+
addEventListener(type: 'toolbar-search', listener: (ev: TableToolbarToolbarSearchEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
60
70
|
addEventListener<K extends keyof HTMLElementEventMap>(
|
|
61
71
|
type: K,
|
|
62
72
|
listener: (this: UITableToolbar, ev: HTMLElementEventMap[K]) => unknown,
|
|
@@ -199,6 +199,33 @@ events:
|
|
|
199
199
|
description: "Column visibility changed. Detail: { hiddenColumns }."
|
|
200
200
|
page-size-change:
|
|
201
201
|
description: "Page-size select changed. Detail: { pageSize }."
|
|
202
|
+
toolbar-search:
|
|
203
|
+
description: >-
|
|
204
|
+
gh#1764/#1780, ADR-0079 (events-only interaction contract) — dispatched
|
|
205
|
+
directly at the resolved [for] target (not bubbled from this element)
|
|
206
|
+
in place of the pre-#1780 direct `.search =` write. table-ui listens
|
|
207
|
+
for this on itself. Detail: { value }.
|
|
208
|
+
toolbar-filter-set:
|
|
209
|
+
description: >-
|
|
210
|
+
gh#1764/#1780, ADR-0079 — dispatched directly at the resolved [for]
|
|
211
|
+
target in place of the pre-#1780 direct `target.setFilter()` call.
|
|
212
|
+
table-ui listens for this on itself. Detail: { key, value, op }; a
|
|
213
|
+
null `value` clears that one column's filter.
|
|
214
|
+
toolbar-filter-clear:
|
|
215
|
+
description: >-
|
|
216
|
+
gh#1764/#1780, ADR-0079 — dispatched directly at the resolved [for]
|
|
217
|
+
target in place of the pre-#1780 direct `target.clearFilters()` call.
|
|
218
|
+
table-ui listens for this on itself. No detail.
|
|
219
|
+
toolbar-columns-set:
|
|
220
|
+
description: >-
|
|
221
|
+
gh#1764/#1780, ADR-0079 — dispatched directly at the resolved [for]
|
|
222
|
+
target in place of the pre-#1780 direct `target.columns =` write.
|
|
223
|
+
table-ui listens for this on itself. Detail: { columns }.
|
|
224
|
+
toolbar-paginate:
|
|
225
|
+
description: >-
|
|
226
|
+
gh#1764/#1780, ADR-0079 — dispatched directly at the resolved [for]
|
|
227
|
+
target in place of the pre-#1780 direct `target.paginate =` write.
|
|
228
|
+
table-ui listens for this on itself. Detail: { pageSize }.
|
|
202
229
|
slots:
|
|
203
230
|
scope:
|
|
204
231
|
description: Leading region rendered BEFORE the [text]/[count] title cluster — e.g. a scope/view-switcher menu (an org's teams, a saved view). Positioning is CSS by DOM order, mirroring how [slot="actions"] is a real, author-fillable insertion point rather than template-stamped content.
|
|
@@ -266,18 +293,29 @@ a2ui:
|
|
|
266
293
|
- >-
|
|
267
294
|
Pair <table-toolbar-ui> with <table-ui> via [for="<table-id>"]
|
|
268
295
|
(or rely on first-sibling fallback when both are inside the
|
|
269
|
-
same parent). One toolbar per table.
|
|
270
|
-
|
|
271
|
-
|
|
296
|
+
same parent). One toolbar per table. The doubled-chrome hazard is
|
|
297
|
+
a TITLE COLLISION, not a blanket card-header ban (gh#1807, SPEC
|
|
298
|
+
REQ-C-004): a toolbar carrying [text] must not share a card with
|
|
299
|
+
a heading-slotted <card-ui> <header> — that pairing doubles the
|
|
300
|
+
title row. A title-less toolbar (no [text]) inside a card whose
|
|
301
|
+
<header> owns the title is the canonical table-in-card
|
|
302
|
+
composition (see the table-in-card pattern) and is NOT doubled
|
|
303
|
+
chrome — the toolbar renders only its count/search/filter/sort/
|
|
304
|
+
columns cluster, with the title living in the card header alone.
|
|
272
305
|
- >-
|
|
273
306
|
All four affordances (search, filter, sort, columns) default ON.
|
|
274
307
|
Opt out individually via [no-search] / [no-filter] / [no-sort]
|
|
275
308
|
/ [no-columns]. The previous [searchable] / [filterable]
|
|
276
309
|
attributes are deprecated — do NOT emit them.
|
|
277
310
|
- >-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
311
|
+
Three legitimate placements: ABOVE the <card-ui> containing the
|
|
312
|
+
table-ui; standing alone outside a card-ui parent with
|
|
313
|
+
[variant="card"] (wraps the toolbar in card-style chrome); or
|
|
314
|
+
INSIDE the card, in its own plain (non-bleed) <section> between
|
|
315
|
+
the card header and the bleed table section — the canonical
|
|
316
|
+
table-in-card shape (gh#1807) — title-less (no [text]) since the
|
|
317
|
+
card header already owns the title (see the title-collision rule
|
|
318
|
+
above).
|
|
281
319
|
- >-
|
|
282
320
|
Use slot="action" (or [slot="actions"]) for trailing primary
|
|
283
321
|
buttons (Invite, Export, +New). Use [text] / [count] props for
|
|
@@ -322,15 +360,20 @@ a2ui:
|
|
|
322
360
|
EXPLICIT range-total="0" — omitting range-* entirely (loading) never
|
|
323
361
|
shows it.
|
|
324
362
|
- >-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
363
|
+
A plain <button-ui> slotted into [slot="actions-leading"] (e.g. the
|
|
364
|
+
app-owned Filter/Columns triggers above) already collapses to
|
|
365
|
+
icon-only at the SAME breakpoint as the native Filter/Sort/Columns
|
|
366
|
+
buttons (gh#1748) — no consumer JS/CSS required. Read the host's
|
|
367
|
+
[data-stage-resolved] attribute (ADR-0076 REQ-M-005, ADIA2-9123
|
|
368
|
+
S4-ii) only for compaction BEYOND plain icon-only reduction — e.g.
|
|
369
|
+
hiding a slotted button entirely at the overflow stage, or reacting
|
|
370
|
+
to the stage in a consumer's own drawer/menu — without re-deriving
|
|
371
|
+
the same breakpoints yourself. Informational only — mirrors the
|
|
372
|
+
CSS-decided compaction stage (full | search-tight | icon-only |
|
|
373
|
+
overflow), never drives table-toolbar's own rendering. Reports the
|
|
374
|
+
pinned [stage] value when set, the live width classification
|
|
375
|
+
otherwise; updates on both a live resize and a [stage] attribute
|
|
376
|
+
change.
|
|
334
377
|
anti_patterns: []
|
|
335
378
|
examples:
|
|
336
379
|
- name: members-toolbar
|
package/core/data-stream.js
CHANGED
|
@@ -37,6 +37,14 @@
|
|
|
37
37
|
* stream-load — first signal value received for this element
|
|
38
38
|
* stream-update — each subsequent value, detail.data = the new value
|
|
39
39
|
* stream-error — transport-level error, detail.error = message
|
|
40
|
+
*
|
|
41
|
+
* Opting out (gh#1760):
|
|
42
|
+
* `data-stream-managed="false"` tells the document-level observer to
|
|
43
|
+
* never claim this element, even though it carries `data-stream-src`.
|
|
44
|
+
* For elements that own their own imperative fetch (e.g. the billing
|
|
45
|
+
* composites' `refresh()`) and must not also be claimed by this module —
|
|
46
|
+
* never mix an imperative fetch with an unmanaged `data-stream-src` on
|
|
47
|
+
* the same element (data-stream-protocol.md §10.1).
|
|
40
48
|
*/
|
|
41
49
|
|
|
42
50
|
import { signal, effect, untracked } from './signals.js';
|
|
@@ -54,6 +62,11 @@ const ATTRS = {
|
|
|
54
62
|
merge: 'data-stream-merge',
|
|
55
63
|
format: 'data-stream-format',
|
|
56
64
|
id: 'data-stream-id',
|
|
65
|
+
// gh#1760 — opt-out for elements that carry `data-stream-src` but manage
|
|
66
|
+
// their own fetch (e.g. the billing composites' hand-rolled `refresh()`).
|
|
67
|
+
// `"false"` tells the document-level observer to never claim this element,
|
|
68
|
+
// so exactly one owner (the element's own imperative fetch) ever runs.
|
|
69
|
+
managed: 'data-stream-managed',
|
|
57
70
|
};
|
|
58
71
|
|
|
59
72
|
const STREAMS = new Map(); /* streamId → { signal, refs, transport, opts } */
|
|
@@ -386,6 +399,7 @@ function applyData(el, raw, opts) {
|
|
|
386
399
|
export function start(el) {
|
|
387
400
|
stop(el);
|
|
388
401
|
if (!el.isConnected) return;
|
|
402
|
+
if (attr(el, 'managed') === 'false') return; // gh#1760 — self-managed opt-out
|
|
389
403
|
const src = attr(el, 'src');
|
|
390
404
|
if (!src) return;
|
|
391
405
|
|
|
@@ -442,7 +456,8 @@ export function stop(el) {
|
|
|
442
456
|
const ATTR_FILTER = Object.values(ATTRS);
|
|
443
457
|
|
|
444
458
|
function isStreamingEl(node) {
|
|
445
|
-
return node && node.nodeType === 1 && node.hasAttribute && node.hasAttribute(ATTRS.src)
|
|
459
|
+
return !!(node && node.nodeType === 1 && node.hasAttribute && node.hasAttribute(ATTRS.src)
|
|
460
|
+
&& node.getAttribute(ATTRS.managed) !== 'false');
|
|
446
461
|
}
|
|
447
462
|
|
|
448
463
|
function visitSubtree(root, fn) {
|
|
@@ -476,11 +491,31 @@ const observer = typeof MutationObserver !== 'undefined'
|
|
|
476
491
|
|
|
477
492
|
function bootstrap() {
|
|
478
493
|
if (typeof document === 'undefined') return;
|
|
494
|
+
// Register the observer synchronously — no live mutation is missed.
|
|
479
495
|
observer?.observe(document.documentElement, {
|
|
480
496
|
childList: true, subtree: true,
|
|
481
497
|
attributes: true, attributeFilter: ATTR_FILTER,
|
|
482
498
|
});
|
|
483
|
-
|
|
499
|
+
// gh#1760 — defer the INITIAL sweep of already-present markup by one
|
|
500
|
+
// microtask. Without this, a page whose entry module imports the core
|
|
501
|
+
// barrel (this module) BEFORE a self-managed composite's own module
|
|
502
|
+
// (which sets `data-stream-managed="false"` synchronously from its
|
|
503
|
+
// upgrade-triggered `connectedCallback`) races: this sweep would claim
|
|
504
|
+
// pre-existing SSR markup before the composite ever gets a chance to
|
|
505
|
+
// opt out. Deferring to a microtask lets the rest of the SAME
|
|
506
|
+
// synchronous module-evaluation phase — including every sibling
|
|
507
|
+
// module's own top-level side effects — finish first, so by the time
|
|
508
|
+
// this sweep runs, every already-loaded composite has already set its
|
|
509
|
+
// marker. (A composite loaded asynchronously — a dynamic `import()` —
|
|
510
|
+
// after this microtask has already fired is not covered by this
|
|
511
|
+
// deferral alone; its own `connected()` marker-set plus the observer's
|
|
512
|
+
// live attribute-mutation handling still tears down a wrongly-started
|
|
513
|
+
// stream, bounding the damage to at most one extra fetch.)
|
|
514
|
+
queueMicrotask(() => {
|
|
515
|
+
document.querySelectorAll(`[${ATTRS.src}]`).forEach((el) => {
|
|
516
|
+
if (isStreamingEl(el)) start(el);
|
|
517
|
+
});
|
|
518
|
+
});
|
|
484
519
|
}
|
|
485
520
|
|
|
486
521
|
if (typeof document !== 'undefined') {
|
package/core/index.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from './template.js';
|
|
|
9
9
|
export * from './element.js';
|
|
10
10
|
export * from './form.js';
|
|
11
11
|
export * from './register.js';
|
|
12
|
+
export * from './store.js';
|
|
12
13
|
// Mirror the explicit re-export in core/index.js — `streams` + `whenStream`
|
|
13
14
|
// (the public surface of the data-stream attribute-driven ingestion module).
|
|
14
15
|
// `export *` from data-stream.js would also expose internals; keep the
|
package/core/index.js
CHANGED