@adia-ai/web-components 0.8.45 → 0.8.47
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 +18 -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/field/field.css +24 -2
- package/components/index.js +1 -0
- package/components/input/input.css +7 -0
- package/components/nav/nav.css +7 -3
- package/components/nav-group/nav-group.css +38 -41
- package/components/nav-item/nav-item.css +11 -5
- package/components/preview/preview.class.js +12 -2
- package/components/table/table.a2ui.json +19 -4
- package/components/table/table.class.js +163 -12
- package/components/table/table.d.ts +9 -3
- package/components/table/table.yaml +109 -8
- 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 +10 -0
- package/components/table-toolbar/table-toolbar.class.js +70 -7
- package/components/table-toolbar/table-toolbar.css +100 -3
- package/components/table-toolbar/table-toolbar.d.ts +2 -0
- package/components/table-toolbar/table-toolbar.examples.md +41 -41
- package/components/table-toolbar/table-toolbar.yaml +31 -6
- package/core/anchor.js +21 -1
- package/custom-elements.json +145 -4
- package/dist/host.min.css +1 -1
- package/dist/host.sheet.js +1 -1
- package/dist/theme-provider.min.js +2 -2
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +87 -87
- 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/colors/material-static.css +276 -234
- package/styles/components.css +1 -0
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-side-effect class export for `<table-footer-ui>`.
|
|
3
|
+
*
|
|
4
|
+
* Importing this file gives you the class(es) without auto-registering the tag.
|
|
5
|
+
* Useful for test isolation, subclassing with tag-name override, or selective
|
|
6
|
+
* composition.
|
|
7
|
+
*
|
|
8
|
+
* The auto-register path stays at `@adia-ai/web-components/components/table-footer`
|
|
9
|
+
* (which imports this file + calls `defineIfFree()`).
|
|
10
|
+
*
|
|
11
|
+
* @see ../../USAGE.md#registration--auto-vs-explicit
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* <table-footer-ui for="tests" page="2" page-size="10" range-total="128"></table-footer-ui>
|
|
16
|
+
*
|
|
17
|
+
* Companion / footer bar for a sibling table-ui (gh#1807, spec-table-structural-system,
|
|
18
|
+
* ADR-0080). Renders two regions on one wrapping flex row: a "Showing X–Y of N"
|
|
19
|
+
* range label (inline-start) and a composed `<pagination-ui>` (inline-end).
|
|
20
|
+
* The pager itself is never re-implemented — table-footer-ui owns only the
|
|
21
|
+
* derivation math (pages / range window) and the range label's text + a11y.
|
|
22
|
+
*
|
|
23
|
+
* [for] resolution mirrors table-toolbar-ui/chart-legend-ui — the footer
|
|
24
|
+
* resolves an element by id, then wires state via events. When [for] is
|
|
25
|
+
* absent, falls back to the first table-ui sibling under the same parent.
|
|
26
|
+
*
|
|
27
|
+
* Derivation model, not explicit endpoints (REQ-F-002) — attributes are
|
|
28
|
+
* `page` / `page-size` / `range-total`; everything else (total pages, the
|
|
29
|
+
* range window) derives:
|
|
30
|
+
* pages = max(1, ceil(range-total / page-size))
|
|
31
|
+
* range-start = (page − 1) × page-size + 1
|
|
32
|
+
* range-end = min(page × page-size, range-total)
|
|
33
|
+
*
|
|
34
|
+
* Degradation (REQ-S):
|
|
35
|
+
* - range-total ABSENT → indeterminate/loading: both regions hidden.
|
|
36
|
+
* - range-total EXPLICIT "0" → pager hidden; [slot="empty"] renders in
|
|
37
|
+
* the range region if supplied, else hides.
|
|
38
|
+
* - page-size ABSENT or "0" → count-only label ("128 items"), no pager
|
|
39
|
+
* (REQ-S-005 — the ordinary DEFAULT state,
|
|
40
|
+
* since page-size defaults to 0).
|
|
41
|
+
* - derived pages ≤ 1 → pager hidden, range label still renders.
|
|
42
|
+
*
|
|
43
|
+
* Target derivation (REQ-W-006, amended 0.2.0/ADR-0082) — a two-branch
|
|
44
|
+
* ladder when range-total is unset, per-attribute:
|
|
45
|
+
* 1. the target's own [range-total] attribute, when present — table-
|
|
46
|
+
* authoritative SERVER mode (REQ-D-003, gh#1754). The identity rule:
|
|
47
|
+
* the footer's range-total derives from the table's range-total, never
|
|
48
|
+
* the table's filteredCount. Takes precedence over branch 2 even when
|
|
49
|
+
* the target's [paginate] is also > 0 (server mode has a real page
|
|
50
|
+
* size, it just isn't the source of the row total).
|
|
51
|
+
* 2. otherwise, when the resolved target has [paginate] > 0, CLIENT mode
|
|
52
|
+
* (the original 0.1.0 behavior): page-size derives from
|
|
53
|
+
* `target.paginate`, range-total from `target.filteredCount` (a
|
|
54
|
+
* read-only getter on table-ui — search+filter applied,
|
|
55
|
+
* pre-pagination).
|
|
56
|
+
* `page-size` derives from `target.paginate` in EITHER branch above. `page`
|
|
57
|
+
* has no live read on the target (no public getter for its private page
|
|
58
|
+
* state); the default (1) is correct at bind time for a freshly-connected
|
|
59
|
+
* table, and any later internal page move resyncs via the target's `page`
|
|
60
|
+
* event (below). Explicit attributes always win over derivation,
|
|
61
|
+
* per-attribute. Widened staleness edge: a programmatic `.data` swap — or a
|
|
62
|
+
* `range-total` update on the target without a page move — emits no event
|
|
63
|
+
* either.
|
|
64
|
+
*
|
|
65
|
+
* State flow — events-only interaction contract (ADR-0079, extended by
|
|
66
|
+
* ADR-0080 to a second sender). The footer never writes a property or calls
|
|
67
|
+
* a method on the bound table directly:
|
|
68
|
+
* pager click → `footer-page` { page } (1-based; bubbles, composed),
|
|
69
|
+
* dispatched directly at the resolved [for] target.
|
|
70
|
+
* table.class.js listens for it in connected()/disconnected()
|
|
71
|
+
* and applies it only when [paginate] > 0 (clamped; a
|
|
72
|
+
* non-numeric/NaN detail is a no-op; server-mode is a
|
|
73
|
+
* documented no-op).
|
|
74
|
+
* pager click → the footer's own bubbling `page-change` { page }
|
|
75
|
+
* notification (fires in both modes — the server-mode
|
|
76
|
+
* fetch hook).
|
|
77
|
+
* table → footer (read direction, REQ-W-004) — the footer listens on the
|
|
78
|
+
* resolved target for its `page` notification to resync an
|
|
79
|
+
* internally-caused move (a search reset to page 1, an
|
|
80
|
+
* internal pager click with [no-pager] unset). Basing
|
|
81
|
+
* translation (gh#1808, SPEC O-1): table.class.js dispatches
|
|
82
|
+
* `page` with its internal 0-based index, and table.yaml
|
|
83
|
+
* documents it as such — this listener translates that
|
|
84
|
+
* 0-based detail to this footer's own 1-based `page`. The
|
|
85
|
+
* footer's own surfaces (`page` attribute, `footer-page`,
|
|
86
|
+
* `page-change`) are all 1-based, always.
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
import { UIElement } from '../../core/element.js';
|
|
90
|
+
|
|
91
|
+
export class UITableFooter extends UIElement {
|
|
92
|
+
static properties = {
|
|
93
|
+
for: { type: String, default: '', reflect: true },
|
|
94
|
+
page: { type: Number, default: 1, reflect: true },
|
|
95
|
+
pageSize: { type: Number, default: 0, reflect: true, attribute: 'page-size' },
|
|
96
|
+
rangeTotal: { type: Number, default: 0, reflect: true, attribute: 'range-total' },
|
|
97
|
+
// Pass-throughs to the composed pagination-ui (REQ-F-002) — the
|
|
98
|
+
// defaults below deliberately mirror pagination-ui's own defaults, so
|
|
99
|
+
// an unset pass-through is a no-op mirror, not a silent override.
|
|
100
|
+
siblings: { type: Number, default: 1, reflect: true },
|
|
101
|
+
size: { type: String, default: 'md', reflect: true },
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
static template = () => null;
|
|
105
|
+
|
|
106
|
+
#target = null;
|
|
107
|
+
|
|
108
|
+
// ── Lifecycle ────────────────────────────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
connected() {
|
|
111
|
+
this.#stamp();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
render() {
|
|
115
|
+
// [for] / page / page-size / range-total / siblings / size changes come
|
|
116
|
+
// through here (the framework's own reactive effect calls render()
|
|
117
|
+
// once right after connected(), then again on every tracked-property
|
|
118
|
+
// change).
|
|
119
|
+
this.#resolveTarget();
|
|
120
|
+
this.#draw();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
disconnected() {
|
|
124
|
+
this.#detachTarget();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ADR-0076 REQ-E-001's own gotcha, ported verbatim (table-toolbar.class.js) —
|
|
128
|
+
// rangeTotal's default is already 0, so setting range-total="0" from an
|
|
129
|
+
// unset start parses to the SAME Number(0); the base class's property
|
|
130
|
+
// setter's Object.is short-circuit swallows the signal write, and render()
|
|
131
|
+
// never re-runs from the reactive effect alone. This is fundamentally an
|
|
132
|
+
// ATTRIBUTE-PRESENCE concern (REQ-S-001 vs REQ-S-002), not a coerced-value
|
|
133
|
+
// one, so it needs the raw attribute callback, not the signal diff.
|
|
134
|
+
attributeChangedCallback(name, oldVal, newVal) {
|
|
135
|
+
super.attributeChangedCallback(name, oldVal, newVal);
|
|
136
|
+
if (name === 'range-total') this.#draw();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ── DOM stamp ────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
#stamp() {
|
|
142
|
+
if (this.querySelector(':scope > [data-row]')) return;
|
|
143
|
+
|
|
144
|
+
const row = document.createElement('div');
|
|
145
|
+
row.setAttribute('data-row', '');
|
|
146
|
+
|
|
147
|
+
// REQ-X-002 — the range region announces page navigation without
|
|
148
|
+
// stealing focus; the `empty` slot's content (REQ-F-004) renders inside
|
|
149
|
+
// this SAME region, so a 0-result transition announces too.
|
|
150
|
+
const range = document.createElement('div');
|
|
151
|
+
range.setAttribute('data-range', '');
|
|
152
|
+
range.setAttribute('aria-live', 'polite');
|
|
153
|
+
range.setAttribute('aria-atomic', 'true');
|
|
154
|
+
|
|
155
|
+
const rangeText = document.createElement('span');
|
|
156
|
+
rangeText.setAttribute('data-range-text', '');
|
|
157
|
+
range.appendChild(rangeText);
|
|
158
|
+
|
|
159
|
+
// Empty-state slot (REQ-F-004) — real insertion point (ADR-0067), same
|
|
160
|
+
// move-pre-existing-children pattern as table-toolbar-ui's own [slot="empty"].
|
|
161
|
+
const rangeEmpty = document.createElement('div');
|
|
162
|
+
rangeEmpty.setAttribute('data-range-empty', '');
|
|
163
|
+
rangeEmpty.hidden = true;
|
|
164
|
+
for (const node of [...this.children]) {
|
|
165
|
+
if (node === row) continue;
|
|
166
|
+
if (node.getAttribute?.('slot') === 'empty') {
|
|
167
|
+
rangeEmpty.appendChild(node);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
range.appendChild(rangeEmpty);
|
|
171
|
+
|
|
172
|
+
// Pager region (REQ-F-003) — composes <pagination-ui> per ADR-0027
|
|
173
|
+
// (listed in table-footer.yaml's `composes:`, never auto-imported).
|
|
174
|
+
const pagerWrap = document.createElement('div');
|
|
175
|
+
pagerWrap.setAttribute('data-pager', '');
|
|
176
|
+
|
|
177
|
+
const pag = document.createElement('pagination-ui');
|
|
178
|
+
pag.setAttribute('data-footer-pager', '');
|
|
179
|
+
pag.addEventListener('page-change', this.#onPagerChange);
|
|
180
|
+
pagerWrap.appendChild(pag);
|
|
181
|
+
|
|
182
|
+
row.appendChild(range);
|
|
183
|
+
row.appendChild(pagerWrap);
|
|
184
|
+
this.appendChild(row);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Pager interaction (footer → table, REQ-W-001/003) ───────────────────
|
|
188
|
+
|
|
189
|
+
#onPagerChange = (e) => {
|
|
190
|
+
// The composed pagination-ui's own `page-change` also bubbles (it sets
|
|
191
|
+
// `bubbles: true`) — stop it here so an ancestor listening generically
|
|
192
|
+
// for `page-change` never receives BOTH the raw inner event and the
|
|
193
|
+
// footer's own re-dispatch of the same name below (code-checker
|
|
194
|
+
// finding, 2026-08-21).
|
|
195
|
+
e.stopPropagation();
|
|
196
|
+
const page = Number(e.detail?.page);
|
|
197
|
+
if (!Number.isFinite(page)) return;
|
|
198
|
+
// REQ-D-002 (CodeRabbit review, PR #1811): server-mode (an explicit
|
|
199
|
+
// [range-total]) never optimistically advances `this.page` — the
|
|
200
|
+
// consumer's write-back after a successful fetch is the state
|
|
201
|
+
// authority, so a failed fetch never needs an undo. Client-mode DOES
|
|
202
|
+
// advance immediately for a responsive pager; REQ-W-004's #onTargetPage
|
|
203
|
+
// resync listener then confirms/corrects it from the table's own
|
|
204
|
+
// `page` echo once the move actually lands.
|
|
205
|
+
if (!this.hasAttribute('range-total')) {
|
|
206
|
+
this.page = page;
|
|
207
|
+
}
|
|
208
|
+
if (this.#target) {
|
|
209
|
+
this.#target.dispatchEvent(new CustomEvent('footer-page', {
|
|
210
|
+
bubbles: true,
|
|
211
|
+
composed: true,
|
|
212
|
+
detail: { page },
|
|
213
|
+
}));
|
|
214
|
+
}
|
|
215
|
+
// REQ-W-003 — the footer's own consumer-facing notification, a
|
|
216
|
+
// distinct name from the inbound `footer-page` command, fires in both
|
|
217
|
+
// client- and server-mode (the server-mode fetch hook, REQ-D-002).
|
|
218
|
+
// Fires even with no resolved target (REQ-S-004 — a footer can drive a
|
|
219
|
+
// non-table surface).
|
|
220
|
+
this.dispatchEvent(new CustomEvent('page-change', {
|
|
221
|
+
bubbles: true,
|
|
222
|
+
detail: { page },
|
|
223
|
+
}));
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
// ── Target resolution (mirrors table-toolbar-ui's [for] pattern) ────────
|
|
227
|
+
|
|
228
|
+
#resolveTarget() {
|
|
229
|
+
const next = this.#findTarget();
|
|
230
|
+
if (next === this.#target) return;
|
|
231
|
+
this.#detachTarget();
|
|
232
|
+
if (!next) return;
|
|
233
|
+
this.#target = next;
|
|
234
|
+
next.addEventListener('page', this.#onTargetPage);
|
|
235
|
+
next.addEventListener('filter-change', this.#onTargetRefresh);
|
|
236
|
+
next.addEventListener('sort', this.#onTargetRefresh);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
#findTarget() {
|
|
240
|
+
if (this.for) {
|
|
241
|
+
const root = this.getRootNode?.();
|
|
242
|
+
const byId = root?.getElementById?.(this.for) || document.getElementById(this.for);
|
|
243
|
+
if (byId && byId.tagName?.toLowerCase() === 'table-ui') return byId;
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
// Fallback — first sibling table-ui in the same parent (a direct
|
|
247
|
+
// child, never a deeper descendant — a "sibling" is a peer, not
|
|
248
|
+
// anything nested arbitrarily deep inside another peer's own subtree).
|
|
249
|
+
const parent = this.parentElement;
|
|
250
|
+
if (!parent) return null;
|
|
251
|
+
return parent.querySelector(':scope > table-ui') || null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
#detachTarget() {
|
|
255
|
+
if (this.#target) {
|
|
256
|
+
this.#target.removeEventListener('page', this.#onTargetPage);
|
|
257
|
+
this.#target.removeEventListener('filter-change', this.#onTargetRefresh);
|
|
258
|
+
this.#target.removeEventListener('sort', this.#onTargetRefresh);
|
|
259
|
+
}
|
|
260
|
+
this.#target = null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ── Read direction (table → footer, REQ-W-004) ──────────────────────────
|
|
264
|
+
|
|
265
|
+
#onTargetPage = (e) => {
|
|
266
|
+
const raw = Number(e.detail?.page);
|
|
267
|
+
if (!Number.isFinite(raw)) return;
|
|
268
|
+
// Basing translation (gh#1808, SPEC O-1) — see the class-doc comment
|
|
269
|
+
// above. table-ui's `page` event detail is 0-based by contract
|
|
270
|
+
// (table.yaml); this footer's own surfaces are all 1-based.
|
|
271
|
+
this.page = raw + 1;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
#onTargetRefresh = () => {
|
|
275
|
+
// filter-change / sort don't move the target's own private page state,
|
|
276
|
+
// but they DO change the filtered row count a client-mode derivation
|
|
277
|
+
// (REQ-W-006) reads — re-derive and redraw without touching `page`.
|
|
278
|
+
this.#draw();
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
// ── Render ───────────────────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
#draw() {
|
|
284
|
+
const row = this.querySelector(':scope > [data-row]');
|
|
285
|
+
if (!row) return;
|
|
286
|
+
|
|
287
|
+
const rangeEl = row.querySelector(':scope > [data-range]');
|
|
288
|
+
const rangeText = rangeEl?.querySelector(':scope > [data-range-text]');
|
|
289
|
+
const rangeEmpty = rangeEl?.querySelector(':scope > [data-range-empty]');
|
|
290
|
+
const pagerWrap = row.querySelector(':scope > [data-pager]');
|
|
291
|
+
const pag = pagerWrap?.querySelector(':scope > pagination-ui');
|
|
292
|
+
|
|
293
|
+
const target = this.#target;
|
|
294
|
+
// gh#1754/ADR-0082 REQ-W-006 (amended 0.2.0) — the derivation ladder
|
|
295
|
+
// gains a server branch. targetServer (branch 2) takes precedence over
|
|
296
|
+
// the filteredCount fallback (branch 3) even when the target's
|
|
297
|
+
// [paginate] is also > 0 — a table-authoritative server-mode table
|
|
298
|
+
// (REQ-D-003) still has paginate > 0, so clientMode is narrowed to
|
|
299
|
+
// exclude it (the identity rule: the footer's range-total derives from
|
|
300
|
+
// the table's range-total, never its loaded-page filteredCount).
|
|
301
|
+
const targetServer = !!target && target.hasAttribute('range-total');
|
|
302
|
+
const clientMode = !!target && !targetServer && Number(target.paginate) > 0;
|
|
303
|
+
|
|
304
|
+
// REQ-F-002 — explicit attributes always win over derivation,
|
|
305
|
+
// per-attribute (not all-or-nothing).
|
|
306
|
+
const hasRangeTotal = this.hasAttribute('range-total');
|
|
307
|
+
const hasPageSize = this.hasAttribute('page-size');
|
|
308
|
+
|
|
309
|
+
const totalKnown = hasRangeTotal || targetServer || clientMode;
|
|
310
|
+
// gh#1754/ADR-0082 REQ-D-003 (CodeRabbit, PR #1828) — a raw `Number(x)
|
|
311
|
+
// || 0` accepts negative and non-finite author-set values verbatim
|
|
312
|
+
// (both are truthy), which can drive the range text negative/infinite
|
|
313
|
+
// (e.g. "Showing 1–-5 of -5"). Clamped to a finite, non-negative
|
|
314
|
+
// integer for both the local attribute and the server-mode target's.
|
|
315
|
+
const safeRangeTotal = (raw) => {
|
|
316
|
+
const n = Number(raw);
|
|
317
|
+
return Number.isFinite(n) && n > 0 ? Math.trunc(n) : 0;
|
|
318
|
+
};
|
|
319
|
+
const rangeTotal = hasRangeTotal
|
|
320
|
+
? safeRangeTotal(this.rangeTotal)
|
|
321
|
+
: targetServer
|
|
322
|
+
? safeRangeTotal(target.rangeTotal)
|
|
323
|
+
: (clientMode ? (Number(target.filteredCount) || 0) : 0);
|
|
324
|
+
// page-size still derives from target.paginate in EITHER derived mode
|
|
325
|
+
// (server or client) — unchanged behavior, just widened to cover the
|
|
326
|
+
// targetServer branch too (a server-mode target's paginate is its
|
|
327
|
+
// page-size, same as a client-mode target's).
|
|
328
|
+
const pageSize = hasPageSize
|
|
329
|
+
? (Number(this.pageSize) || 0)
|
|
330
|
+
: ((targetServer || clientMode) ? (Number(target.paginate) || 0) : (Number(this.pageSize) || 0));
|
|
331
|
+
|
|
332
|
+
if (!rangeEl) return;
|
|
333
|
+
|
|
334
|
+
if (!totalKnown) {
|
|
335
|
+
// REQ-S-001 — indeterminate (loading). Absence means "not yet known",
|
|
336
|
+
// never "zero" — the hasAttribute discipline above prevents any
|
|
337
|
+
// empty-state flash while data fetches (AC-006).
|
|
338
|
+
rangeEl.hidden = true;
|
|
339
|
+
if (pagerWrap) pagerWrap.hidden = true;
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
rangeEl.hidden = false;
|
|
344
|
+
|
|
345
|
+
if (rangeTotal === 0) {
|
|
346
|
+
// REQ-S-002 — confirmed-empty. The empty slot always wins over the
|
|
347
|
+
// range text whenever range-total is confirmed zero, mutually
|
|
348
|
+
// exclusive with the normal-range branch below.
|
|
349
|
+
const hasEmptyContent = !!rangeEmpty && rangeEmpty.childNodes.length > 0;
|
|
350
|
+
if (rangeText) rangeText.hidden = true;
|
|
351
|
+
if (rangeEmpty) rangeEmpty.hidden = !hasEmptyContent;
|
|
352
|
+
if (pagerWrap) pagerWrap.hidden = true;
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (rangeEmpty) rangeEmpty.hidden = true;
|
|
357
|
+
|
|
358
|
+
if (pageSize <= 0) {
|
|
359
|
+
// REQ-S-005 — unknown page size, the ORDINARY default state
|
|
360
|
+
// (page-size defaults to 0). Count-only label; never the undefined
|
|
361
|
+
// "Showing 1–0 of N" math an unbounded pager would otherwise need.
|
|
362
|
+
if (rangeText) {
|
|
363
|
+
rangeText.hidden = false;
|
|
364
|
+
rangeText.textContent = `${rangeTotal} items`;
|
|
365
|
+
}
|
|
366
|
+
if (pagerWrap) pagerWrap.hidden = true;
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const pages = Math.max(1, Math.ceil(rangeTotal / pageSize));
|
|
371
|
+
const page = Math.max(1, Math.min(Number(this.page) || 1, pages));
|
|
372
|
+
const start = (page - 1) * pageSize + 1;
|
|
373
|
+
const end = Math.min(page * pageSize, rangeTotal);
|
|
374
|
+
|
|
375
|
+
if (rangeText) {
|
|
376
|
+
rangeText.hidden = false;
|
|
377
|
+
rangeText.textContent = `Showing ${start}–${end} of ${rangeTotal}`;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (pagerWrap) {
|
|
381
|
+
// REQ-S-003 — a single page is still information, just no pager.
|
|
382
|
+
pagerWrap.hidden = pages <= 1;
|
|
383
|
+
}
|
|
384
|
+
if (pag) {
|
|
385
|
+
pag.setAttribute('page', String(page));
|
|
386
|
+
pag.setAttribute('total', String(pages));
|
|
387
|
+
pag.setAttribute('siblings', String(Math.max(0, Number(this.siblings) || 0)));
|
|
388
|
+
pag.setAttribute('size', this.size || 'md');
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
@scope (table-footer-ui) {
|
|
2
|
+
:where(:scope) {
|
|
3
|
+
/* ── Layout ── */
|
|
4
|
+
--table-footer-gap: var(--a-space-3);
|
|
5
|
+
--table-footer-py: var(--a-space-2);
|
|
6
|
+
--table-footer-px: 0;
|
|
7
|
+
|
|
8
|
+
/* ── Range label ("Showing X–Y of N" / "N items") ── */
|
|
9
|
+
--table-footer-summary-fg: var(--md-sys-color-neutral-on-surface-variant);
|
|
10
|
+
--table-footer-summary-size: var(--a-ui-size);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* ═══════ Base ═══════ */
|
|
14
|
+
|
|
15
|
+
:scope {
|
|
16
|
+
box-sizing: border-box;
|
|
17
|
+
display: block;
|
|
18
|
+
color: var(--md-sys-color-neutral-on-surface);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
[data-row] {
|
|
22
|
+
display: flex;
|
|
23
|
+
flex-direction: row;
|
|
24
|
+
flex-wrap: wrap;
|
|
25
|
+
justify-content: space-between;
|
|
26
|
+
align-items: center;
|
|
27
|
+
gap: var(--table-footer-gap);
|
|
28
|
+
padding: var(--table-footer-py) var(--table-footer-px);
|
|
29
|
+
min-width: 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* ═══════ Range label ═══════ */
|
|
33
|
+
|
|
34
|
+
[data-range] {
|
|
35
|
+
flex: 0 0 auto;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
[data-range][hidden] { display: none; }
|
|
39
|
+
|
|
40
|
+
[data-range-text] {
|
|
41
|
+
font-size: var(--table-footer-summary-size);
|
|
42
|
+
color: var(--table-footer-summary-fg);
|
|
43
|
+
line-height: var(--a-font-leading-snug);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
[data-range-text][hidden] { display: none; }
|
|
47
|
+
|
|
48
|
+
/* ═══════ Empty-state slot ═══════ (REQ-F-004 — renders in [data-range]'s
|
|
49
|
+
own position, alongside [data-range-text]; only one of the two is ever
|
|
50
|
+
visible at a time, per #draw(). Plain author-supplied markup — no
|
|
51
|
+
forced typography here, unlike [data-range-text] above, which styles
|
|
52
|
+
OUR OWN generated text. */
|
|
53
|
+
|
|
54
|
+
[data-range-empty][hidden] { display: none; }
|
|
55
|
+
|
|
56
|
+
/* ═══════ Pager region ═══════ */
|
|
57
|
+
|
|
58
|
+
[data-pager] {
|
|
59
|
+
flex: 0 0 auto;
|
|
60
|
+
margin-inline-start: auto;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
[data-pager][hidden] { display: none; }
|
|
64
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<table-footer-ui>` — Footer / companion bar for a sibling table-ui (gh#1807, ADR-0080) — a "Showing X–Y of N" range label paired with a composed pagination-ui. Derivation-model API: set `page` / `page-size` / `range-total` and the footer derives total pages and the range window itself, so it never drifts out of sync with its own pager. Wired to the target table via a [for] id-ref and CustomEvents only (ADR-0079/ADR-0080) — the footer never writes a property or calls a method on the bound table. Drop below (or inside a card-ui footer alongside) any table-ui to add the standard data-grid pager + range summary without re-implementing pagination-ui's ellipsis truncation, keyboard handling, or ARIA.
|
|
3
|
+
*
|
|
4
|
+
* @see https://ui-kit.exe.xyz/site/components/table-footer
|
|
5
|
+
*
|
|
6
|
+
* Type declarations generated by scripts/build/dts-codegen.mjs from
|
|
7
|
+
* the component's `.a2ui.json` sidecar(s). Edit the source `.yaml`,
|
|
8
|
+
* run `npm run build:components`, then `npm run codegen:dts` to
|
|
9
|
+
* regenerate; or hand-author this file fully if rich event types are
|
|
10
|
+
* needed beyond what the yaml `events:` block can express.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { UIElement } from '../../core/element.js';
|
|
14
|
+
|
|
15
|
+
export type TableFooterFooterPageEvent = CustomEvent<unknown>;
|
|
16
|
+
export type TableFooterPageChangeEvent = CustomEvent<unknown>;
|
|
17
|
+
|
|
18
|
+
export class UITableFooter extends UIElement {
|
|
19
|
+
/** id-ref of the table-ui to control. Falls back to the first sibling table-ui within the same parent when omitted. */
|
|
20
|
+
for: string;
|
|
21
|
+
/** Current page, 1-based. Explicit attributes always win over derivation (per-attribute, not all-or-nothing) — see page-size/range-total. */
|
|
22
|
+
page: number;
|
|
23
|
+
/** Rows per page. Default 0 (unknown) is the ORDINARY default state, not an exotic edge case: with page-size absent/0 and range-total present, the range label renders in count-only form ("128 items") and the pager does not render — the derivation math (pages = range-total / page-size) is undefined without a page size. In a client-mode composition (the resolved target has [paginate] > 0) and this attribute is unset, the footer derives page-size from the target's own [paginate] value. */
|
|
24
|
+
pageSize: number;
|
|
25
|
+
/** Total ROWS across all pages. Absent (the default) means "not yet known" (loading) — never "zero"; both the range label and the pager stay hidden until this is set. Explicit range-total="0" is the confirmed-empty state (pager hidden; the `empty` slot renders in the range label's position when supplied). Named identically to table-toolbar-ui's [range-total] and table-ui's own [range-total] (gh#1754, ADR-0082) and deliberately NOT `total` (pagination-ui's `total` is total PAGES — the ADR-0063 B5 collision rule). When this attribute is unset, the footer derives it from the resolved target via a two-branch ladder (REQ-W-006, amended 0.2.0/ADR-0082): (1) the target's own [range-total] attribute, when present — the table-authoritative SERVER mode (gh#1754 Shape 1's second lawful composition); the identity rule: the footer's range-total derives from the table's range-total, never the table's filteredCount, and this branch wins even when the target's [paginate] is also > 0; (2) otherwise, when the target has [paginate] > 0, CLIENT mode: the target's filtered row count (search + column filters applied, before pagination). Refreshes on the target's `page` / `filter-change` / `sort` events in either branch. Widened named staleness edge: a programmatic `.data` swap on the target — or a `range-total` update on the target without an accompanying page move — emits no event; either re-set this footer's own attributes after such a change, or accept staleness until the target's next event. */
|
|
26
|
+
rangeTotal: number;
|
|
27
|
+
/** Passed through to the composed pagination-ui's own [siblings] (page-number window size on each side of the current page). */
|
|
28
|
+
siblings: number;
|
|
29
|
+
/** Passed through to the composed pagination-ui's own [size] (sm | md | lg, the universal size system). */
|
|
30
|
+
size: string;
|
|
31
|
+
|
|
32
|
+
addEventListener(type: 'footer-page', listener: (ev: TableFooterFooterPageEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
33
|
+
addEventListener(type: 'page-change', listener: (ev: TableFooterPageChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
|
|
34
|
+
addEventListener<K extends keyof HTMLElementEventMap>(
|
|
35
|
+
type: K,
|
|
36
|
+
listener: (this: UITableFooter, ev: HTMLElementEventMap[K]) => unknown,
|
|
37
|
+
options?: boolean | AddEventListenerOptions,
|
|
38
|
+
): void;
|
|
39
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# table-footer — Examples
|
|
2
|
+
|
|
3
|
+
## Wiring
|
|
4
|
+
|
|
5
|
+
```html
|
|
6
|
+
<card-ui>
|
|
7
|
+
<section bleed>
|
|
8
|
+
<table-ui id="tests" paginate="10" no-pager>…</table-ui>
|
|
9
|
+
</section>
|
|
10
|
+
<footer divider>
|
|
11
|
+
<table-footer-ui for="tests"></table-footer-ui>
|
|
12
|
+
</footer>
|
|
13
|
+
</card-ui>
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Rendered demos
|
|
17
|
+
|
|
18
|
+
```html
|
|
19
|
+
<col-ui gap="0">
|
|
20
|
+
<table-ui id="demo-footer-server" raw>
|
|
21
|
+
<table>
|
|
22
|
+
<thead>
|
|
23
|
+
<tr>
|
|
24
|
+
<th>Name</th>
|
|
25
|
+
<th>Status</th>
|
|
26
|
+
</tr>
|
|
27
|
+
</thead>
|
|
28
|
+
<tbody>
|
|
29
|
+
<tr>
|
|
30
|
+
<td>Metabolic panel — A. Chen</td>
|
|
31
|
+
<td>
|
|
32
|
+
<tag-ui variant="success">Complete</tag-ui>
|
|
33
|
+
</td>
|
|
34
|
+
</tr>
|
|
35
|
+
<tr>
|
|
36
|
+
<td>Metabolic panel — J. Park</td>
|
|
37
|
+
<td>
|
|
38
|
+
<tag-ui variant="warning">Pending</tag-ui>
|
|
39
|
+
</td>
|
|
40
|
+
</tr>
|
|
41
|
+
</tbody>
|
|
42
|
+
</table>
|
|
43
|
+
</table-ui>
|
|
44
|
+
<table-footer-ui for="demo-footer-server" page="2" page-size="10" range-total="128"></table-footer-ui>
|
|
45
|
+
</col-ui>
|
|
46
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<table-footer-ui>` — auto-registers the tag on import.
|
|
3
|
+
*
|
|
4
|
+
* For non-side-effect class import (test isolation, tag override), use
|
|
5
|
+
* the `class` subpath:
|
|
6
|
+
*
|
|
7
|
+
* import { UITableFooter } from '@adia-ai/web-components/components/table-footer/class';
|
|
8
|
+
*
|
|
9
|
+
* @see ../../USAGE.md#registration--auto-vs-explicit
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { defineIfFree } from '../../core/register.js';
|
|
13
|
+
import { UITableFooter } from './table-footer.class.js';
|
|
14
|
+
|
|
15
|
+
defineIfFree('table-footer-ui', UITableFooter);
|
|
16
|
+
|
|
17
|
+
export { UITableFooter };
|