@adia-ai/web-components 0.8.45 → 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 (36) hide show
  1. package/CHANGELOG.md +9 -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/field/field.css +24 -2
  10. package/components/index.js +1 -0
  11. package/components/input/input.css +7 -0
  12. package/components/table/table.a2ui.json +19 -4
  13. package/components/table/table.class.js +163 -12
  14. package/components/table/table.d.ts +9 -3
  15. package/components/table/table.yaml +109 -8
  16. package/components/table-footer/table-footer.a2ui.json +150 -0
  17. package/components/table-footer/table-footer.class.js +391 -0
  18. package/components/table-footer/table-footer.css +64 -0
  19. package/components/table-footer/table-footer.d.ts +39 -0
  20. package/components/table-footer/table-footer.examples.md +46 -0
  21. package/components/table-footer/table-footer.js +17 -0
  22. package/components/table-footer/table-footer.yaml +219 -0
  23. package/components/table-toolbar/table-toolbar.yaml +17 -6
  24. package/custom-elements.json +138 -4
  25. package/dist/theme-provider.min.js +1 -1
  26. package/dist/web-components.min.css +1 -1
  27. package/dist/web-components.min.js +87 -87
  28. package/dist/web-components.sheet.js +1 -1
  29. package/package.json +1 -1
  30. package/patterns/chart-in-card/chart-in-card.examples.html +36 -9
  31. package/patterns/new-enrollments/new-enrollments.examples.html +140 -0
  32. package/patterns/new-enrollments/new-enrollments.html +54 -0
  33. package/patterns/table-in-card/table-in-card.examples.html +168 -0
  34. package/patterns/table-in-card/table-in-card.examples.js +139 -0
  35. package/patterns/table-in-card/table-in-card.html +86 -0
  36. 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 };