@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.
Files changed (69) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/components/card/card.css +12 -23
  3. package/components/card/card.yaml +11 -0
  4. package/components/chart/chart.a2ui.json +16 -1
  5. package/components/chart/chart.class.js +611 -41
  6. package/components/chart/chart.css +174 -0
  7. package/components/chart/chart.d.ts +5 -1
  8. package/components/chart/chart.yaml +45 -1
  9. package/components/context-menu/context-menu.a2ui.json +8 -3
  10. package/components/context-menu/context-menu.class.js +46 -5
  11. package/components/context-menu/context-menu.d.ts +6 -3
  12. package/components/context-menu/context-menu.examples.md +2 -2
  13. package/components/context-menu/context-menu.yaml +22 -5
  14. package/components/field/field.css +24 -2
  15. package/components/index.js +1 -0
  16. package/components/input/input.css +7 -0
  17. package/components/nav/nav.a2ui.json +2 -2
  18. package/components/nav/nav.css +1 -1
  19. package/components/nav/nav.d.ts +1 -1
  20. package/components/nav/nav.yaml +14 -3
  21. package/components/nav-group/nav-group.css +37 -3
  22. package/components/pagination/pagination.class.js +52 -22
  23. package/components/search/search.class.js +39 -5
  24. package/components/select/select.a2ui.json +5 -0
  25. package/components/select/select.class.js +20 -0
  26. package/components/select/select.css +25 -0
  27. package/components/select/select.d.ts +2 -0
  28. package/components/select/select.yaml +12 -0
  29. package/components/table/cell-types.js +9 -0
  30. package/components/table/table.a2ui.json +19 -4
  31. package/components/table/table.class.js +410 -45
  32. package/components/table/table.css +7 -4
  33. package/components/table/table.d.ts +9 -3
  34. package/components/table/table.yaml +115 -9
  35. package/components/table-footer/table-footer.a2ui.json +150 -0
  36. package/components/table-footer/table-footer.class.js +391 -0
  37. package/components/table-footer/table-footer.css +64 -0
  38. package/components/table-footer/table-footer.d.ts +39 -0
  39. package/components/table-footer/table-footer.examples.md +46 -0
  40. package/components/table-footer/table-footer.js +17 -0
  41. package/components/table-footer/table-footer.yaml +219 -0
  42. package/components/table-toolbar/table-toolbar.a2ui.json +15 -0
  43. package/components/table-toolbar/table-toolbar.class.js +61 -17
  44. package/components/table-toolbar/table-toolbar.css +34 -0
  45. package/components/table-toolbar/table-toolbar.d.ts +10 -0
  46. package/components/table-toolbar/table-toolbar.yaml +58 -15
  47. package/core/data-stream.js +37 -2
  48. package/core/index.d.ts +1 -0
  49. package/core/index.js +1 -0
  50. package/core/provider.d.ts +9 -13
  51. package/core/provider.js +9 -113
  52. package/core/store.d.ts +46 -0
  53. package/core/store.js +89 -0
  54. package/custom-elements.json +192 -8
  55. package/dist/host.min.css +1 -1
  56. package/dist/host.sheet.js +1 -1
  57. package/dist/theme-provider.min.js +1 -1
  58. package/dist/web-components.min.css +1 -1
  59. package/dist/web-components.min.js +96 -96
  60. package/dist/web-components.sheet.js +1 -1
  61. package/package.json +1 -1
  62. package/patterns/chart-in-card/chart-in-card.examples.html +36 -9
  63. package/patterns/new-enrollments/new-enrollments.examples.html +140 -0
  64. package/patterns/new-enrollments/new-enrollments.html +54 -0
  65. package/patterns/table-in-card/table-in-card.examples.html +168 -0
  66. package/patterns/table-in-card/table-in-card.examples.js +139 -0
  67. package/patterns/table-in-card/table-in-card.html +86 -0
  68. package/styles/api/sizing.css +46 -0
  69. 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
- * search → table.search (string property)
34
- * filters → table.setFilter() (per-key)
35
- * sort → simulated click on table's [data-sort-key] header
36
- * column hidden table.columns = (clone with hidden flag flipped)
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
- // Applied directly to the bound table's own [paginate] (rows-per-page)
850
- // prop the same "mirror to URL/analytics via the event, but the
851
- // toolbar already wires the change into the target" pattern
852
- // columns-change/sort-change already use.
853
- if (this.#target) this.#target.paginate = pageSize;
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
- if (this.#target) this.#target.search = value;
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
- if (v) target.setFilter(col.key, v, 'select');
1048
- else target.setFilter(col.key, null);
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
- if (v) target.setFilter(col.key, v, 'contains');
1065
- else target.setFilter(col.key, null);
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
- target.clearFilters();
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
- target.columns = next;
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. Do NOT also use
270
- <card-ui>'s <header> on the same card that produces a doubled
271
- chrome row.
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
- Place the toolbar ABOVE the <card-ui> containing the table-ui,
279
- or use [variant="card"] when standing alone outside a card-ui
280
- parent (the variant wraps the toolbar in card-style chrome).
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
- Read the host's [data-stage-resolved] attribute (ADR-0076 REQ-M-005,
326
- ADIA2-9123 S4-ii) to compact a consumer's own [slot="actions-leading"]
327
- content in sympathy with table-toolbar's native regions, without
328
- re-deriving the same breakpoints yourself. Informational only
329
- mirrors the CSS-decided compaction stage (full | search-tight |
330
- icon-only | overflow), never drives table-toolbar's own rendering.
331
- Reports the pinned [stage] value when set, the live width
332
- classification otherwise; updates on both a live resize and a
333
- [stage] attribute change.
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
@@ -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
- document.querySelectorAll(`[${ATTRS.src}]`).forEach(start);
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
@@ -18,6 +18,7 @@ export * from './signals.js';
18
18
  export * from './template.js';
19
19
  export * from './register.js';
20
20
  export * from './controller.js';
21
+ export * from './store.js';
21
22
  export * from './provider.js';
22
23
  export * from './anchor.js';
23
24
  export * from './icons.js';