@adia-ai/web-components 0.8.47 → 0.8.48
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 +34 -0
- package/components/card/card.css +25 -2
- package/components/chart/chart.a2ui.json +1 -1
- package/components/chart/chart.d.ts +1 -1
- package/components/chart/chart.yaml +6 -3
- package/components/link/link.css +3 -2
- package/components/richtext/richtext.css +2 -2
- package/components/table/table.a2ui.json +5 -2
- package/components/table/table.class.js +36 -1
- package/components/table/table.d.ts +2 -2
- package/components/table/table.yaml +17 -1
- package/components/table-footer/table-footer.a2ui.json +20 -2
- package/components/table-footer/table-footer.class.js +94 -8
- package/components/table-footer/table-footer.d.ts +6 -2
- package/components/table-footer/table-footer.yaml +57 -6
- package/components/table-toolbar/table-toolbar.a2ui.json +25 -2
- package/components/table-toolbar/table-toolbar.class.js +47 -2
- package/components/table-toolbar/table-toolbar.css +6 -2
- package/components/table-toolbar/table-toolbar.d.ts +8 -2
- package/components/table-toolbar/table-toolbar.examples.md +6 -4
- package/components/table-toolbar/table-toolbar.yaml +83 -9
- package/custom-elements.json +43 -5
- package/dist/host.min.css +1 -1
- package/dist/host.sheet.js +1 -1
- package/dist/theme-provider.min.js +8 -8
- package/dist/themes.min.css +1 -1
- package/dist/themes.sheet.js +1 -1
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +49 -49
- package/dist/web-components.sheet.js +1 -1
- package/package.json +1 -1
- package/styles/colors/material-static.css +34 -12
- package/styles/themes.css +2676 -0
- package/styles/type/elements.css +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,41 @@
|
|
|
1
1
|
# Changelog — @adia-ai/web-components
|
|
2
2
|
|
|
3
|
+
## [0.8.48] — 2026-08-23
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **`table-ui`/`table-footer-ui`/`table-toolbar-ui` range grammar — `open-total`, `range-of`, `range-noun` (gh#1877), ADR-0082 Amendment.** Server-side pagination's range summary ("1–20 of 200") gets three composable authoring knobs: `open-total` drops the known-total suffix for unbounded/streamed counts, `range-of` swaps the connector word, `range-noun` supplies the counted-thing label — wired through `components/table/table.css`, `components/table-footer/table-footer.js`, and `components/table-toolbar/table-toolbar.js` (#1890).
|
|
7
|
+
- **`table-toolbar-ui[title-always]` opt-out for the scope-reconciliation title hide (gh#1879).** The `:has([data-scope]:not(:empty)) [data-title]` reconciliation rule (landed in 0.8.47) drops the title whenever a scope chip is present; `title-always` opts a toolbar out so the title survives alongside scope chips (#1880).
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- **`link-ui`/`anchor-ui`-bearing surfaces get hover tokens, transitions, and a new `--link-fg-disabled` (gh#1881).** Every anchor-bearing component now declares `--link-fg-hover`/`--link-fg-active` plus a `color`/`text-decoration-color` transition instead of relying on inherited defaults; `--link-fg-disabled` fills the missing disabled-state role (#1887).
|
|
11
|
+
- **`[prose]`/`[verse]` live usages swept to `scale=` (gh#1822).** Component demos and yaml examples using the deprecated `[prose]`/`[verse]` attribute pair migrated to the `scale=` contract ratified in an earlier cut (#1874).
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- **`card-ui` preserves footer top inset in content-sized cards (gh#1866).** A content-sized (non-fixed-height) card's footer lost its top inset because the inset was conditioned on a fixed-height layout branch that content-sized cards never take; the inset now applies unconditionally (`components/card/card.css`) (#1875).
|
|
15
|
+
- **A2UI runtime stops stamping synthetic `.id` onto elements with no authored id.** `resolve-node.js`'s synthetic-id path was firing for every unauthored element, polluting the DOM with runtime-only ids that leaked into snapshot tests and event targeting; now only elements that need an id for event wiring get one (#1872).
|
|
16
|
+
- **A2UI runtime reflects `JS_PROPS` as JSON attributes alongside property writes (gh#1858).** `prop-apply.js` wrote JS property values but skipped the matching attribute reflection for `JS_PROPS`-classified props, so attribute-based selectors and SSR snapshots saw a stale value; both now update together (#1870).
|
|
17
|
+
- **`themes.css`/`material-static.css` theme-scoped semantic alias resolution under descendant `[theme]` scopes (gh#1863).** A `--a-*` semantic alias resolved against the wrong `[theme]` ancestor when a themed subtree nested inside another themed subtree; two new gates guard the resolution order (#1873).
|
|
18
|
+
|
|
19
|
+
### Docs
|
|
20
|
+
- **`site/components/table-toolbar` canonical sample composes the toolbar as a `card-ui` header (#1884).** Replaces the prior bare-toolbar sample with the pattern most consumers actually use.
|
|
21
|
+
- **`components/chart/chart.yaml` `ratio=` comment repaired** — the per-type wave landed in an earlier cut; the yaml comment still described the old single-ratio contract (#1871).
|
|
22
|
+
|
|
23
|
+
### Maintenance
|
|
24
|
+
- **`dist/` bundles rebuilt** in this cut's window — regenerated from the source changes described above, not independent edits.
|
|
25
|
+
- **`styles/` touched in this release window** (`colors/material-static.css`, `themes.css`) — carried by the theme-scoped alias fix above (gh#1863, #1873).
|
|
26
|
+
|
|
3
27
|
## [0.8.47] — 2026-08-22
|
|
4
28
|
|
|
29
|
+
### Changed
|
|
30
|
+
- **`nav-group-ui`/`nav-item-ui`/`nav-ui` token simplification (gh#1740, gh#459, gh#1736).** `nav-group-ui` header text settles on one unconditional NEUTRAL-HIGH (`--nav-group-fg-active` deleted; `--nav-group-fg-selected` kept for the popover's current-option row); `nav-ui[data-nav-label]` moves from `--a-fg-muted` to plain NEUTRAL with retuned padding (top 16 / sides 4 / bottom 8). Dev server: `vite.config.js` gets an explicit `server.fs.allow`/`deny` — fixes vite's default `**/.git/**` deny blocking its own `/@vite/client` after resolving pnpm symlinks into a bootstrapped worktree's shared store (#1853).
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
- **`material-static.css`'s identical-leg `light-dark()` roles dropped; stale `themes.css` dead-code claim corrected (gh#1852).** ~224 solid-fill/hover/active/on-*/alpha-family `--md-sys-color-*` roles wrapped in a no-op `light-dark()` (both legs identical) — unwrapped. The header's second claim, that `themes.css` ships a dead divergent neutral ramp, was a false premise on investigation (a themed ancestor's override wins via ordinary custom-property inheritance, verified in Playwright) — corrected in place rather than deleting live theming infrastructure (#1854).
|
|
34
|
+
- **`table-toolbar-ui[size]` prop, overflow-trigger cascade tie, and three min-width floor fights.** New `size` prop wired via ambient CSS inheritance; the `[data-toolbar-overflow]` cascade tie with `menu-ui`'s base rule (redundant "More" trigger re-render) fixed; `:has([data-scope]:not(:empty)) [data-title]` reconciliation drops a duplicate title/count cluster; toolbar/input/select/menu min-width floors reconciled (#1855).
|
|
35
|
+
- **`core/anchor.js` popover-anchor reuse guard (all 12 consumers).** A trigger reused as the anchor for a second popover had its anchor-name clobbered by the first popover's async close-cleanup, landing the second popover at the viewport origin — fixed with an ownership guard in the cleanup closure (284 consumer tests green) (#1855).
|
|
36
|
+
|
|
37
|
+
### Docs
|
|
38
|
+
- **`site/components/table-toolbar` side-by-side docs demos.** `[data-preview-split]` opt-in pairs a demo's live render with its authored HTML side by side; table-toolbar visual goldens re-baselined after the verified layout fixes (#1855).
|
|
5
39
|
|
|
6
40
|
### Maintenance
|
|
7
41
|
- **`components/` touched in this release window** (12 file(s), e.g. `nav-group/nav-group.css`) — carried by the entries above.
|
package/components/card/card.css
CHANGED
|
@@ -76,8 +76,30 @@
|
|
|
76
76
|
display: flex;
|
|
77
77
|
flex-direction: column;
|
|
78
78
|
}
|
|
79
|
+
/* Explicit (not left to the base `& > footer` rule further down) so this
|
|
80
|
+
value is the one that wins regardless of declaration order — kept
|
|
81
|
+
alongside the bottom-pin spacer it replaces, gh#1866. */
|
|
79
82
|
:scope > footer {
|
|
80
|
-
margin-
|
|
83
|
+
margin-block-start: var(--card-inset);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/* Bottom-pin (gh#559), preserved WITHOUT sacrificing the footer's own
|
|
87
|
+
top inset (gh#1866). The old approach put `margin-top: auto` directly
|
|
88
|
+
on the footer: that absorbs FREE SPACE in an equal-height grid/flex
|
|
89
|
+
row (the pin), but a content-sized card has none to absorb, so the
|
|
90
|
+
auto margin resolved to 0 and the footer lost its --card-inset gap
|
|
91
|
+
entirely — every OTHER region kept its inset, only the footer didn't.
|
|
92
|
+
Moving the auto margin to the region immediately before the footer
|
|
93
|
+
keeps both behaviors distinct: that region still soaks up any
|
|
94
|
+
leftover height (still bottom-pinning the footer in an equal-height
|
|
95
|
+
card), while the footer's own margin-block-start above stays the
|
|
96
|
+
ordinary --card-inset in every card, content-sized included.
|
|
97
|
+
`:has(+ footer)` (not `:last-of-type`) so exactly the ONE region
|
|
98
|
+
directly adjacent to the footer gets the spacer — `:last-of-type` would
|
|
99
|
+
match the last header AND the last section independently when both are
|
|
100
|
+
present, splitting the auto margin across two elements instead of one. */
|
|
101
|
+
:scope:has(> footer) > :is(header, section):has(+ footer) {
|
|
102
|
+
margin-block-end: auto;
|
|
81
103
|
}
|
|
82
104
|
|
|
83
105
|
/* Flex containers never collapse adjacent margins the way block flow
|
|
@@ -87,7 +109,8 @@
|
|
|
87
109
|
single inset block flow would have collapsed them to. The leading
|
|
88
110
|
region's own margin-bottom already supplies that inset; zero the
|
|
89
111
|
trailing region's margin-top. footer is excluded — its margin-top is
|
|
90
|
-
handled above (
|
|
112
|
+
handled above (its own explicit --card-inset, gh#1866) or by
|
|
113
|
+
[divider] below. */
|
|
91
114
|
:scope:has(> footer) > :is(header, section) + :is(header, section) {
|
|
92
115
|
margin-block-start: 0;
|
|
93
116
|
}
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
"default": false
|
|
119
119
|
},
|
|
120
120
|
"ratio": {
|
|
121
|
-
"description": "Chart 2.0 aspect-ratio bucket (ADR-0074). Unset (default) auto-snaps to the nearest of the three studied ratios by comparing the chart's own live box aspect against two midpoint boundaries (5:4 between 3:2/1:1, 4:5 between 1:1/2:3) — never interpolated; the resolved bucket reflects onto the host as `data-ratio-resolved`. Setting `ratio` explicitly pins that bucket regardless of the box's actual aspect, overriding the auto-snap. Orthogonal to `size` (overall scale) and `type`/`color` (ADR-0064's family axis) —
|
|
121
|
+
"description": "Chart 2.0 aspect-ratio bucket (ADR-0074). Unset (default) auto-snaps to the nearest of the three studied ratios by comparing the chart's own live box aspect against two midpoint boundaries (5:4 between 3:2/1:1, 4:5 between 1:1/2:3) — never interpolated; the resolved bucket reflects onto the host as `data-ratio-resolved`. Setting `ratio` explicitly pins that bucket regardless of the box's actual aspect, overriding the auto-snap. Orthogonal to `size` (overall scale) and `type`/`color` (ADR-0064's family axis) — per-type studied renderings per bucket shipped across Waves A-C (gh#1624 plan steps 2-4: #1658 bar/line/area/pie/donut/sparkline, #1659 radar/scatter/ radial-bar/gauge/segments/stacked-bar/grouped-bar/multi-line, #1662 funnel/treemap/sankey/composed) — all 18 types have dedicated per-ratio render paths.",
|
|
122
122
|
"type": "string",
|
|
123
123
|
"enum": [
|
|
124
124
|
"3:2",
|
|
@@ -40,7 +40,7 @@ export class UIChart extends UIElement {
|
|
|
40
40
|
noGrid: boolean;
|
|
41
41
|
/** Hide value labels. Canonical spelling (ADR-0063, gh#1563) — the deprecated `hideValues` dual-read shim was cut in 0.8.43 (gh#1617). */
|
|
42
42
|
noValues: boolean;
|
|
43
|
-
/** Chart 2.0 aspect-ratio bucket (ADR-0074). Unset (default) auto-snaps to the nearest of the three studied ratios by comparing the chart's own live box aspect against two midpoint boundaries (5:4 between 3:2/1:1, 4:5 between 1:1/2:3) — never interpolated; the resolved bucket reflects onto the host as `data-ratio-resolved`. Setting `ratio` explicitly pins that bucket regardless of the box's actual aspect, overriding the auto-snap. Orthogonal to `size` (overall scale) and `type`/`color` (ADR-0064's family axis) —
|
|
43
|
+
/** Chart 2.0 aspect-ratio bucket (ADR-0074). Unset (default) auto-snaps to the nearest of the three studied ratios by comparing the chart's own live box aspect against two midpoint boundaries (5:4 between 3:2/1:1, 4:5 between 1:1/2:3) — never interpolated; the resolved bucket reflects onto the host as `data-ratio-resolved`. Setting `ratio` explicitly pins that bucket regardless of the box's actual aspect, overriding the auto-snap. Orthogonal to `size` (overall scale) and `type`/`color` (ADR-0064's family axis) — per-type studied renderings per bucket shipped across Waves A-C (gh#1624 plan steps 2-4: #1658 bar/line/area/pie/donut/sparkline, #1659 radar/scatter/ radial-bar/gauge/segments/stacked-bar/grouped-bar/multi-line, #1662 funnel/treemap/sankey/composed) — all 18 types have dedicated per-ratio render paths. */
|
|
44
44
|
ratio: '3:2' | '1:1' | '2:3';
|
|
45
45
|
/** Multi-line only. Names one series key (matching a `y` key) to render at full strength — area fill + full-opacity line. Every other series drops its area fill and renders line-only at --chart-deemphasized-opacity. Empty (default) applies full treatment to every series, unchanged from pre-gh#561 behavior. */
|
|
46
46
|
seriesEmphasis: string;
|
|
@@ -105,9 +105,12 @@ props:
|
|
|
105
105
|
bucket reflects onto the host as `data-ratio-resolved`. Setting
|
|
106
106
|
`ratio` explicitly pins that bucket regardless of the box's actual
|
|
107
107
|
aspect, overriding the auto-snap. Orthogonal to `size` (overall
|
|
108
|
-
scale) and `type`/`color` (ADR-0064's family axis) —
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
scale) and `type`/`color` (ADR-0064's family axis) — per-type studied
|
|
109
|
+
renderings per bucket shipped across Waves A-C (gh#1624 plan steps
|
|
110
|
+
2-4: #1658 bar/line/area/pie/donut/sparkline, #1659 radar/scatter/
|
|
111
|
+
radial-bar/gauge/segments/stacked-bar/grouped-bar/multi-line, #1662
|
|
112
|
+
funnel/treemap/sankey/composed) — all 18 types have dedicated
|
|
113
|
+
per-ratio render paths.
|
|
111
114
|
type: string
|
|
112
115
|
default: ""
|
|
113
116
|
enum:
|
package/components/link/link.css
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
--link-fg: var(--a-link);
|
|
11
11
|
--link-fg-hover: var(--a-link-hover);
|
|
12
12
|
--link-fg-visited: var(--a-link-visited);
|
|
13
|
+
--link-fg-disabled: var(--md-sys-color-neutral-on-surface-disabled);
|
|
13
14
|
--link-underline-offset: 2px;
|
|
14
15
|
--link-focus-ring: var(--a-focus-ring);
|
|
15
16
|
}
|
|
@@ -89,9 +90,9 @@
|
|
|
89
90
|
/* ── Disabled ── */
|
|
90
91
|
|
|
91
92
|
:scope[disabled] > a {
|
|
92
|
-
color: var(--
|
|
93
|
+
color: var(--link-fg-disabled);
|
|
93
94
|
cursor: not-allowed;
|
|
94
|
-
text-decoration-color: var(--
|
|
95
|
+
text-decoration-color: var(--link-fg-disabled);
|
|
95
96
|
pointer-events: none;
|
|
96
97
|
}
|
|
97
98
|
}
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
page and inverted inside cards. Same tier as --a-bg-muted/hover. */
|
|
22
22
|
--richtext-code-bg: var(--md-sys-color-neutral-container-low);
|
|
23
23
|
--richtext-border: var(--md-sys-color-neutral-outline-variant);
|
|
24
|
-
--richtext-link: var(--a-
|
|
25
|
-
--richtext-link-hover: var(--
|
|
24
|
+
--richtext-link: var(--a-link);
|
|
25
|
+
--richtext-link-hover: var(--a-link-hover);
|
|
26
26
|
|
|
27
27
|
/* ── Typography ── */
|
|
28
28
|
--richtext-font: var(--a-font-family);
|
|
@@ -60,8 +60,11 @@
|
|
|
60
60
|
"default": 0
|
|
61
61
|
},
|
|
62
62
|
"range-total": {
|
|
63
|
-
"description": "Total row count across ALL server pages (gh#1754, ADR-0082) — the table-authoritative server-mode data contract. Presence-gated: the attribute's PRESENCE, not its value, is the mode switch (checked via hasAttribute, never the coerced value alone — the same discipline table-footer-ui's own [range-total] ships). Absent (the default) means client mode: today's behavior exactly, [paginate] slices `.data` as always. Present with [paginate] > 0 means server mode: no local slicing (`.data` IS the current page and renders whole, after local search/sort/filter), the internal pager's page count becomes `max(1, ceil(range-total / paginate))` (one calc site, consolidated), and the internal page-reset sites (`data` set, `setFilter`, `clearFilters`) are suppressed so a fetch write-back never fights the pager back to page 0 — page state then moves only via pager interaction, the `footer-page` command, or `setState()`. The existing `page` event (0-based, unchanged) becomes the fetch trigger: the consumer listens for it, fetches that server page, and writes `.data` back. Explicit `range-total=\"0\"` is server-confirmed empty (pager hidden), never conflated with absent. With [paginate] absent/0, [range-total] is inert for this table's own rendering but stays readable by a bound table-footer-ui (REQ-W-006 branch 2) for its count-only label — see the footer-authoritative shape below, which remains lawful and is unaffected by this attribute. Named identically to table-toolbar-ui's and table-footer-ui's own [range-total] (rows, never `pagination-ui[total]`'s pages — the ADR-0063 B5 collision rule); a third instance of one name for one concept.",
|
|
64
|
-
"type":
|
|
63
|
+
"description": "Total row count across ALL server pages (gh#1754, ADR-0082) — the table-authoritative server-mode data contract. Presence-gated: the attribute's PRESENCE, not its value, is the mode switch (checked via hasAttribute, never the coerced value alone — the same discipline table-footer-ui's own [range-total] ships). Absent (the default) means client mode: today's behavior exactly, [paginate] slices `.data` as always. Present with [paginate] > 0 means server mode: no local slicing (`.data` IS the current page and renders whole, after local search/sort/filter), the internal pager's page count becomes `max(1, ceil(range-total / paginate))` (one calc site, consolidated), and the internal page-reset sites (`data` set, `setFilter`, `clearFilters`) are suppressed so a fetch write-back never fights the pager back to page 0 — page state then moves only via pager interaction, the `footer-page` command, or `setState()`. The existing `page` event (0-based, unchanged) becomes the fetch trigger: the consumer listens for it, fetches that server page, and writes `.data` back. Explicit `range-total=\"0\"` is server-confirmed empty (pager hidden), never conflated with absent. With [paginate] absent/0, [range-total] is inert for this table's own rendering but stays readable by a bound table-footer-ui (REQ-W-006 branch 2) for its count-only label — see the footer-authoritative shape below, which remains lawful and is unaffected by this attribute. Named identically to table-toolbar-ui's and table-footer-ui's own [range-total] (rows, never `pagination-ui[total]`'s pages — the ADR-0063 B5 collision rule); a third instance of one name for one concept. Explicit `range-total=\"?\"` (gh#1877, ADR-0082 Amendment) is the OPEN/unproven-total state — cursor/hasMore server paging with no known row count yet: still server mode (still presence-gated), but the internal pager's page count stays exactly one page ahead of wherever the pager currently sits (never a fixed ceil(...) total), keeping `next` enabled indefinitely instead of hiding the pager the way a confirmed `range-total=\"0\"` would. Resolves to the normal finite server-mode behavior the instant a real number replaces the \"?\" — every reader downstream (table-toolbar-ui, table-footer-ui) shares this same distinction. Note: since this prop is Number-typed and reflected, the framework's own attribute-reflection immediately rewrites the DOM attribute to the literal string \"NaN\" once \"?\" is parsed (`+\"?\"` → NaN, then stringified back onto the attribute) — the coerced numeric PROPERTY is what carries the open/unproven signal (non-finite), never a literal \"?\" surviving as the live DOM attribute value.",
|
|
64
|
+
"type": [
|
|
65
|
+
"number",
|
|
66
|
+
"string"
|
|
67
|
+
],
|
|
65
68
|
"default": 0
|
|
66
69
|
},
|
|
67
70
|
"raw": {
|
|
@@ -41,6 +41,16 @@
|
|
|
41
41
|
* conflated with absent. With [paginate] absent/0, [range-total] is
|
|
42
42
|
* inert for this table's own rendering but stays readable by a
|
|
43
43
|
* bound table-footer-ui for its count-only label.
|
|
44
|
+
* range-total="?" — gh#1877/ADR-0082 Amendment (2026-08-22) — the OPEN/
|
|
45
|
+
* unproven-total state: server mode with no known total yet
|
|
46
|
+
* (cursor/hasMore paging). Still presence-gated into server
|
|
47
|
+
* mode like any other [range-total] value; #pageCount stays
|
|
48
|
+
* one page ahead of the current page (never a fixed number)
|
|
49
|
+
* so the pager's `next` stays enabled indefinitely, and
|
|
50
|
+
* pagination visibility no longer requires a positive
|
|
51
|
+
* #safeRangeTotal() while open. Resolves to a normal finite
|
|
52
|
+
* server-mode table the moment a real range-total replaces
|
|
53
|
+
* the "?".
|
|
44
54
|
* loading — show loading overlay
|
|
45
55
|
* search — global search filter string
|
|
46
56
|
*
|
|
@@ -326,6 +336,20 @@ export class UITable extends UIElement {
|
|
|
326
336
|
// reset-suppression call sites for why that widened scope is safe).
|
|
327
337
|
get #serverMode() { return this.hasAttribute('range-total'); }
|
|
328
338
|
|
|
339
|
+
// gh#1877/ADR-0082 Amendment — the open/unproven-total state. NOT keyed
|
|
340
|
+
// on the raw attribute string: `rangeTotal` is a Number-typed reflected
|
|
341
|
+
// prop (installProps in core/element.js), so setting range-total="?" is
|
|
342
|
+
// parsed via `+"?"` → NaN and immediately reflected straight back onto
|
|
343
|
+
// the DOM attribute as the literal string "NaN" (reflect() in
|
|
344
|
+
// element.js stringifies any non-null coerced value) — by the time
|
|
345
|
+
// #draw()/render() runs, `getAttribute('range-total')` already reads
|
|
346
|
+
// "NaN", never the authored "?". The coerced PROPERTY still holds NaN
|
|
347
|
+
// stably, though (Object.is(NaN, NaN) is true, so the reflect's own
|
|
348
|
+
// re-parse never re-writes it) — that's the one signal that survives,
|
|
349
|
+
// and it's what #pageCount/showPagination below need to tell "no total
|
|
350
|
+
// yet" apart from "confirmed empty" (range-total="0", a finite 0).
|
|
351
|
+
get #rangeOpen() { return this.#serverMode && !Number.isFinite(Number(this.rangeTotal)); }
|
|
352
|
+
|
|
329
353
|
// gh#1754/ADR-0082 REQ-D-003 (CodeRabbit, PR #1828) — a raw
|
|
330
354
|
// `Number(x) || 0` accepts negative and non-finite values verbatim (both
|
|
331
355
|
// are truthy), which can drive #pageCount/the footer's own range text
|
|
@@ -933,6 +957,13 @@ export class UITable extends UIElement {
|
|
|
933
957
|
// derived, not the loaded page's own length (the exact dual-math drift
|
|
934
958
|
// gh#1754's evidence quoted — this is now the ONE calc site;
|
|
935
959
|
// #renderPagination reads this getter instead of recomputing).
|
|
960
|
+
// gh#1877/ADR-0082 Amendment — open/unproven total: no real total to
|
|
961
|
+
// derive from, so pageCount stays exactly one page ahead of wherever
|
|
962
|
+
// the pager currently is. That keeps `next` enabled indefinitely
|
|
963
|
+
// (never a false "last page") without inventing a fake total; the
|
|
964
|
+
// moment range-total resolves to a real number, this branch stops
|
|
965
|
+
// firing and the real ceil(...) math above takes over.
|
|
966
|
+
if (this.#rangeOpen) return this.#page + 2;
|
|
936
967
|
if (this.#serverMode) return Math.max(1, Math.ceil(this.#safeRangeTotal() / this.paginate));
|
|
937
968
|
// pageCount should reflect filtered data, not raw data
|
|
938
969
|
const filteredCount = this.#getProcessedIndices().length;
|
|
@@ -1054,8 +1085,12 @@ export class UITable extends UIElement {
|
|
|
1054
1085
|
// `data.length > 0` term: an in-flight fetch (momentarily empty `.data`)
|
|
1055
1086
|
// must not flicker the pager away, so visibility gates on `range-total`
|
|
1056
1087
|
// instead.
|
|
1088
|
+
// gh#1877/ADR-0082 Amendment — an open/unproven total (REQ-D-003
|
|
1089
|
+
// widened) always shows the pager while server mode is active: there's
|
|
1090
|
+
// no confirmed-zero signal to gate on yet, and hiding it would read as
|
|
1091
|
+
// "no more rows" when the real answer is "unknown, keep going".
|
|
1057
1092
|
const showPagination = this.paginate > 0 && !this.noPager
|
|
1058
|
-
&& (this.#serverMode ? this.#safeRangeTotal() > 0 : this.#data.length > 0);
|
|
1093
|
+
&& (this.#serverMode ? (this.#rangeOpen || this.#safeRangeTotal() > 0) : this.#data.length > 0);
|
|
1059
1094
|
let footer = this.querySelector(':scope > [data-footer]');
|
|
1060
1095
|
|
|
1061
1096
|
if (showPagination) {
|
|
@@ -104,8 +104,8 @@ export class UITable extends UIElement {
|
|
|
104
104
|
noPager: boolean;
|
|
105
105
|
/** Rows per page. 0 = show all rows without pagination. When > 0, renders the internal pagination bar below the table — UNLESS [no-pager] is also set (gh#1807, ADR-0080), which keeps [paginate]'s slicing/page-state/`page`-event/`footer-page`-listener behavior but suppresses only the bar itself (the anti-doubled-pager path for a table-footer-ui composition). When [range-total] is also set (gh#1754, ADR-0082), [paginate] becomes purely presentational — it supplies only the page-size for pager math; no local slicing runs. */
|
|
106
106
|
paginate: number;
|
|
107
|
-
/** Total row count across ALL server pages (gh#1754, ADR-0082) — the table-authoritative server-mode data contract. Presence-gated: the attribute's PRESENCE, not its value, is the mode switch (checked via hasAttribute, never the coerced value alone — the same discipline table-footer-ui's own [range-total] ships). Absent (the default) means client mode: today's behavior exactly, [paginate] slices `.data` as always. Present with [paginate] > 0 means server mode: no local slicing (`.data` IS the current page and renders whole, after local search/sort/filter), the internal pager's page count becomes `max(1, ceil(range-total / paginate))` (one calc site, consolidated), and the internal page-reset sites (`data` set, `setFilter`, `clearFilters`) are suppressed so a fetch write-back never fights the pager back to page 0 — page state then moves only via pager interaction, the `footer-page` command, or `setState()`. The existing `page` event (0-based, unchanged) becomes the fetch trigger: the consumer listens for it, fetches that server page, and writes `.data` back. Explicit `range-total="0"` is server-confirmed empty (pager hidden), never conflated with absent. With [paginate] absent/0, [range-total] is inert for this table's own rendering but stays readable by a bound table-footer-ui (REQ-W-006 branch 2) for its count-only label — see the footer-authoritative shape below, which remains lawful and is unaffected by this attribute. Named identically to table-toolbar-ui's and table-footer-ui's own [range-total] (rows, never `pagination-ui[total]`'s pages — the ADR-0063 B5 collision rule); a third instance of one name for one concept. */
|
|
108
|
-
rangeTotal: number;
|
|
107
|
+
/** Total row count across ALL server pages (gh#1754, ADR-0082) — the table-authoritative server-mode data contract. Presence-gated: the attribute's PRESENCE, not its value, is the mode switch (checked via hasAttribute, never the coerced value alone — the same discipline table-footer-ui's own [range-total] ships). Absent (the default) means client mode: today's behavior exactly, [paginate] slices `.data` as always. Present with [paginate] > 0 means server mode: no local slicing (`.data` IS the current page and renders whole, after local search/sort/filter), the internal pager's page count becomes `max(1, ceil(range-total / paginate))` (one calc site, consolidated), and the internal page-reset sites (`data` set, `setFilter`, `clearFilters`) are suppressed so a fetch write-back never fights the pager back to page 0 — page state then moves only via pager interaction, the `footer-page` command, or `setState()`. The existing `page` event (0-based, unchanged) becomes the fetch trigger: the consumer listens for it, fetches that server page, and writes `.data` back. Explicit `range-total="0"` is server-confirmed empty (pager hidden), never conflated with absent. With [paginate] absent/0, [range-total] is inert for this table's own rendering but stays readable by a bound table-footer-ui (REQ-W-006 branch 2) for its count-only label — see the footer-authoritative shape below, which remains lawful and is unaffected by this attribute. Named identically to table-toolbar-ui's and table-footer-ui's own [range-total] (rows, never `pagination-ui[total]`'s pages — the ADR-0063 B5 collision rule); a third instance of one name for one concept. Explicit `range-total="?"` (gh#1877, ADR-0082 Amendment) is the OPEN/unproven-total state — cursor/hasMore server paging with no known row count yet: still server mode (still presence-gated), but the internal pager's page count stays exactly one page ahead of wherever the pager currently sits (never a fixed ceil(...) total), keeping `next` enabled indefinitely instead of hiding the pager the way a confirmed `range-total="0"` would. Resolves to the normal finite server-mode behavior the instant a real number replaces the "?" — every reader downstream (table-toolbar-ui, table-footer-ui) shares this same distinction. Note: since this prop is Number-typed and reflected, the framework's own attribute-reflection immediately rewrites the DOM attribute to the literal string "NaN" once "?" is parsed (`+"?"` → NaN, then stringified back onto the attribute) — the coerced numeric PROPERTY is what carries the open/unproven signal (non-finite), never a literal "?" surviving as the live DOM attribute value. */
|
|
108
|
+
rangeTotal: number | string;
|
|
109
109
|
/** Visual-only passthrough — applies `<table-ui>`'s chrome reset (background / border / border-radius all transparent) AND short-circuits the data lifecycle entirely. The consumer owns the body shape: no header injection, no row reconciliation from `.data`, no empty-state / loading overlays, no aggregation or pagination footers. Use raw when embedding `<table-ui>` inside surfaces that supply their own chrome (e.g. `<card-ui><section bleed>`), or when wrapping a consumer-authored native `<table>` for design-token styling without the framework's data semantics. Note: a `<table-ui>` placed directly inside `<card-ui><section bleed>` drops the table's visual chrome automatically (card.css, gh#796) even without `raw` — `raw` is still required to also short-circuit the data lifecycle. Pre-v0.6.33 (FB-53 §2) `raw` was visual-only and the data lifecycle still ran — wrapping a `.data`-unset native table produced a phantom "No data" overlay. v0.6.33+ matches the documented contract. */
|
|
110
110
|
raw: boolean;
|
|
111
111
|
/** Global search/filter string. Filters visible rows across all columns using case-insensitive substring matching. Does NOT reset the current page (verified against source at the gh#1754/ADR-0082 build — unlike setFilter()/clearFilters(), no internal page reset ever ran for a search change; this description previously claimed "resets to page 1 on change," which the source never did). Aligning the doc to observed behavior here, not a behavior change — a page reset on search, if wanted, is a separate client-mode ticket. */
|
|
@@ -98,7 +98,23 @@ props:
|
|
|
98
98
|
to table-toolbar-ui's and table-footer-ui's own [range-total]
|
|
99
99
|
(rows, never `pagination-ui[total]`'s pages — the ADR-0063 B5
|
|
100
100
|
collision rule); a third instance of one name for one concept.
|
|
101
|
-
|
|
101
|
+
Explicit `range-total="?"` (gh#1877, ADR-0082 Amendment) is the
|
|
102
|
+
OPEN/unproven-total state — cursor/hasMore server paging with no
|
|
103
|
+
known row count yet: still server mode (still presence-gated), but
|
|
104
|
+
the internal pager's page count stays exactly one page ahead of
|
|
105
|
+
wherever the pager currently sits (never a fixed ceil(...) total),
|
|
106
|
+
keeping `next` enabled indefinitely instead of hiding the pager the
|
|
107
|
+
way a confirmed `range-total="0"` would. Resolves to the normal
|
|
108
|
+
finite server-mode behavior the instant a real number replaces the
|
|
109
|
+
"?" — every reader downstream (table-toolbar-ui, table-footer-ui)
|
|
110
|
+
shares this same distinction. Note: since this prop is Number-typed
|
|
111
|
+
and reflected, the framework's own attribute-reflection immediately
|
|
112
|
+
rewrites the DOM attribute to the literal string "NaN" once "?" is
|
|
113
|
+
parsed (`+"?"` → NaN, then stringified back onto the attribute) —
|
|
114
|
+
the coerced numeric PROPERTY is what carries the open/unproven
|
|
115
|
+
signal (non-finite), never a literal "?" surviving as the live DOM
|
|
116
|
+
attribute value.
|
|
117
|
+
type: [number, string]
|
|
102
118
|
default: 0
|
|
103
119
|
reflect: true
|
|
104
120
|
filteredCount:
|
|
@@ -31,11 +31,24 @@
|
|
|
31
31
|
"type": "number",
|
|
32
32
|
"default": 0
|
|
33
33
|
},
|
|
34
|
-
"range-
|
|
35
|
-
"description": "
|
|
34
|
+
"range-noun": {
|
|
35
|
+
"description": "gh#1877, ADR-0082 Amendment — optional per-slice noun appended after the range total (\"of 80 users\") so consumers stop hand-painting the noun themselves. Applies in both the normal finite range text and the open/unproven-total text (\"Showing 51–100 users\" with no \"of N\"). Unset (the default) changes nothing. Named identically on table-toolbar-ui (ADR-0082 §\"same name on both companions\").",
|
|
36
|
+
"type": "string",
|
|
37
|
+
"default": ""
|
|
38
|
+
},
|
|
39
|
+
"range-of": {
|
|
40
|
+
"description": "gh#1877, ADR-0082 Amendment — secondary whole-set total for the \"filtered-of-whole\" shape: a client-side facet narrows [range-total] (a filtered count) while range-of names the unfiltered set size, e.g. \"Showing 1–4 of 4 traversals (6 total)\". Appends \"(of <range-of> total)\" onto the normal finite range text; unset (the default) changes nothing. Never applies while [range-total] is absent (loading) or \"?\" (open/unproven) — there's no finite range text to append onto yet. Named identically on table-toolbar-ui (ADR-0082 §\"same name on both companions\").",
|
|
36
41
|
"type": "number",
|
|
37
42
|
"default": 0
|
|
38
43
|
},
|
|
44
|
+
"range-total": {
|
|
45
|
+
"description": "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). Explicit range-total=\"?\" (gh#1877, ADR-0082 Amendment) is the OPEN/unproven- total state — cursor/hasMore server paging with no known row count yet: the range label renders \"Showing X–Y\" with no \"of N\", and the pager stays in has-more mode (next enabled) rather than hiding — distinct from both \"loading\" (attribute absent) and \"confirmed empty\" (range-total=\"0\"). Resolves to the normal finite behavior the moment a real number replaces the \"?\". 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 target's own \"?\" carries through identically); 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.",
|
|
46
|
+
"type": [
|
|
47
|
+
"number",
|
|
48
|
+
"string"
|
|
49
|
+
],
|
|
50
|
+
"default": 0
|
|
51
|
+
},
|
|
39
52
|
"siblings": {
|
|
40
53
|
"description": "Passed through to the composed pagination-ui's own [siblings] (page-number window size on each side of the current page).",
|
|
41
54
|
"type": "number",
|
|
@@ -77,6 +90,11 @@
|
|
|
77
90
|
"a2ui": "[\n {\"id\": \"root\", \"component\": \"Column\", \"gap\": \"0\", \"children\": [\"tests\", \"ftr\"]},\n {\"id\": \"tests\", \"component\": \"Table\", \"raw\": true},\n {\"id\": \"ftr\", \"component\": \"TableFooter\", \"for\": \"tests\", \"page\": 1, \"page-size\": 10, \"range-total\": 128}\n]",
|
|
78
91
|
"name": "table-footer-server-mode"
|
|
79
92
|
},
|
|
93
|
+
{
|
|
94
|
+
"description": "gh#1877, ADR-0082 Amendment — cursor/hasMore server mode with no known row count yet: [range-total=\"?\"] renders \"Showing 1–50 subscriptions\" (via [range-noun]) with `next` kept enabled until a real total resolves.",
|
|
95
|
+
"a2ui": "[\n {\"id\": \"root\", \"component\": \"Column\", \"gap\": \"0\", \"children\": [\"subs\", \"ftr\"]},\n {\"id\": \"subs\", \"component\": \"Table\", \"raw\": true},\n {\"id\": \"ftr\", \"component\": \"TableFooter\", \"for\": \"subs\", \"page\": 1, \"page-size\": 50, \"range-total\": \"?\", \"range-noun\": \"subscriptions\"}\n]",
|
|
96
|
+
"name": "table-footer-open-total"
|
|
97
|
+
},
|
|
80
98
|
{
|
|
81
99
|
"description": "Client-mode footer bound to a [paginate]d table-ui with its own internal pager suppressed via [no-pager].",
|
|
82
100
|
"a2ui": "[\n {\"id\": \"root\", \"component\": \"Column\", \"gap\": \"0\", \"children\": [\"tests-c\", \"ftr\"]},\n {\"id\": \"tests-c\", \"component\": \"Table\", \"paginate\": 10, \"no-pager\": true, \"raw\": true},\n {\"id\": \"ftr\", \"component\": \"TableFooter\", \"for\": \"tests-c\"}\n]",
|
|
@@ -33,12 +33,23 @@
|
|
|
33
33
|
*
|
|
34
34
|
* Degradation (REQ-S):
|
|
35
35
|
* - range-total ABSENT → indeterminate/loading: both regions hidden.
|
|
36
|
+
* - range-total EXPLICIT "?" → gh#1877/ADR-0082 Amendment — OPEN/unproven
|
|
37
|
+
* total (cursor/hasMore paging): "Showing
|
|
38
|
+
* X–Y" with no "of N", pager stays in
|
|
39
|
+
* has-more mode (next enabled) until a
|
|
40
|
+
* real range-total resolves.
|
|
36
41
|
* - range-total EXPLICIT "0" → pager hidden; [slot="empty"] renders in
|
|
37
42
|
* the range region if supplied, else hides.
|
|
38
43
|
* - page-size ABSENT or "0" → count-only label ("128 items"), no pager
|
|
39
44
|
* (REQ-S-005 — the ordinary DEFAULT state,
|
|
40
45
|
* since page-size defaults to 0).
|
|
41
46
|
* - derived pages ≤ 1 → pager hidden, range label still renders.
|
|
47
|
+
* - range-of="<whole>" → gh#1877/ADR-0082 Amendment — secondary
|
|
48
|
+
* whole-set total, appends "(of <whole>
|
|
49
|
+
* total)"; unset changes nothing.
|
|
50
|
+
* - range-noun="<noun>" → gh#1877/ADR-0082 Amendment — appends the
|
|
51
|
+
* per-slice noun ("of 80 users"); unset
|
|
52
|
+
* changes nothing.
|
|
42
53
|
*
|
|
43
54
|
* Target derivation (REQ-W-006, amended 0.2.0/ADR-0082) — a two-branch
|
|
44
55
|
* ladder when range-total is unset, per-attribute:
|
|
@@ -94,6 +105,15 @@ export class UITableFooter extends UIElement {
|
|
|
94
105
|
page: { type: Number, default: 1, reflect: true },
|
|
95
106
|
pageSize: { type: Number, default: 0, reflect: true, attribute: 'page-size' },
|
|
96
107
|
rangeTotal: { type: Number, default: 0, reflect: true, attribute: 'range-total' },
|
|
108
|
+
// gh#1877/ADR-0082 Amendment — secondary whole-set total (the
|
|
109
|
+
// "filtered-of-whole" gap: a client-side-narrowed range-total plus the
|
|
110
|
+
// unfiltered set size), and the optional per-slice noun consumers used
|
|
111
|
+
// to hand-paint ("of 80 users"). Both presence-gated like range-total —
|
|
112
|
+
// hasAttribute decides whether the suffix renders, never the coerced
|
|
113
|
+
// value alone (an author-set range-of="0" is nonsensical and treated
|
|
114
|
+
// as absent by #draw()'s safeRangeTotal reuse).
|
|
115
|
+
rangeOf: { type: Number, default: 0, reflect: true, attribute: 'range-of' },
|
|
116
|
+
rangeNoun: { type: String, default: '', reflect: true, attribute: 'range-noun' },
|
|
97
117
|
// Pass-throughs to the composed pagination-ui (REQ-F-002) — the
|
|
98
118
|
// defaults below deliberately mirror pagination-ui's own defaults, so
|
|
99
119
|
// an unset pass-through is a no-op mirror, not a silent override.
|
|
@@ -133,7 +153,7 @@ export class UITableFooter extends UIElement {
|
|
|
133
153
|
// one, so it needs the raw attribute callback, not the signal diff.
|
|
134
154
|
attributeChangedCallback(name, oldVal, newVal) {
|
|
135
155
|
super.attributeChangedCallback(name, oldVal, newVal);
|
|
136
|
-
if (name === 'range-total') this.#draw();
|
|
156
|
+
if (name === 'range-total' || name === 'range-of' || name === 'range-noun') this.#draw();
|
|
137
157
|
}
|
|
138
158
|
|
|
139
159
|
// ── DOM stamp ────────────────────────────────────────────────────────────
|
|
@@ -316,11 +336,29 @@ export class UITableFooter extends UIElement {
|
|
|
316
336
|
const n = Number(raw);
|
|
317
337
|
return Number.isFinite(n) && n > 0 ? Math.trunc(n) : 0;
|
|
318
338
|
};
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
339
|
+
// gh#1877/ADR-0082 Amendment — the open/unproven-total state: the
|
|
340
|
+
// resolved source of range-total (local attribute if set, else the
|
|
341
|
+
// server-mode target's own) coerces to a non-finite number. NOT keyed
|
|
342
|
+
// on the raw attribute string — rangeTotal is a Number-typed reflected
|
|
343
|
+
// prop, so range-total="?" parses via `+"?"` → NaN and the property
|
|
344
|
+
// setter's own reflect() immediately rewrites the DOM attribute to the
|
|
345
|
+
// literal string "NaN" (core/element.js) before this ever runs;
|
|
346
|
+
// getAttribute('range-total') would read "NaN", never "?". The
|
|
347
|
+
// coerced PROPERTY still holds NaN stably (Object.is(NaN, NaN) is
|
|
348
|
+
// true, so the reflect's re-parse never re-writes it) — that's the
|
|
349
|
+
// one signal that survives, and what tells "unknown yet" apart from
|
|
350
|
+
// "confirmed zero" (a finite 0) once safeRangeTotal has clamped both
|
|
351
|
+
// to 0.
|
|
352
|
+
const rangeOpen = hasRangeTotal
|
|
353
|
+
? !Number.isFinite(Number(this.rangeTotal))
|
|
354
|
+
: (targetServer && !Number.isFinite(Number(target.rangeTotal)));
|
|
355
|
+
const rangeTotal = rangeOpen
|
|
356
|
+
? null
|
|
357
|
+
: hasRangeTotal
|
|
358
|
+
? safeRangeTotal(this.rangeTotal)
|
|
359
|
+
: targetServer
|
|
360
|
+
? safeRangeTotal(target.rangeTotal)
|
|
361
|
+
: (clientMode ? (Number(target.filteredCount) || 0) : 0);
|
|
324
362
|
// page-size still derives from target.paginate in EITHER derived mode
|
|
325
363
|
// (server or client) — unchanged behavior, just widened to cover the
|
|
326
364
|
// targetServer branch too (a server-mode target's paginate is its
|
|
@@ -329,6 +367,15 @@ export class UITableFooter extends UIElement {
|
|
|
329
367
|
? (Number(this.pageSize) || 0)
|
|
330
368
|
: ((targetServer || clientMode) ? (Number(target.paginate) || 0) : (Number(this.pageSize) || 0));
|
|
331
369
|
|
|
370
|
+
// gh#1877/ADR-0082 Amendment — range-of (secondary whole-set total) and
|
|
371
|
+
// range-noun (per-slice noun) are both additive suffixes onto the
|
|
372
|
+
// normal finite "of N" text; neither applies to the loading, confirmed-
|
|
373
|
+
// empty, or open-total branches below.
|
|
374
|
+
const hasRangeOf = this.hasAttribute('range-of');
|
|
375
|
+
const rangeOfVal = hasRangeOf ? safeRangeTotal(this.rangeOf) : 0;
|
|
376
|
+
const rangeOfSuffix = hasRangeOf && rangeOfVal > 0 ? ` (of ${rangeOfVal} total)` : '';
|
|
377
|
+
const nounSuffix = this.rangeNoun ? ` ${this.rangeNoun}` : '';
|
|
378
|
+
|
|
332
379
|
if (!rangeEl) return;
|
|
333
380
|
|
|
334
381
|
if (!totalKnown) {
|
|
@@ -342,6 +389,45 @@ export class UITableFooter extends UIElement {
|
|
|
342
389
|
|
|
343
390
|
rangeEl.hidden = false;
|
|
344
391
|
|
|
392
|
+
if (rangeOpen) {
|
|
393
|
+
// gh#1877/ADR-0082 Amendment REQ-open — "Showing X–Y" with no
|
|
394
|
+
// "of N", pager stays in has-more mode (next enabled) until a real
|
|
395
|
+
// range-total resolves. Deterministic window math off page/page-size
|
|
396
|
+
// alone, same shape as the finite branch below — no clamp against a
|
|
397
|
+
// total we don't have yet (adiav2 ehr-subscriptions-paging.ts).
|
|
398
|
+
if (rangeEmpty) rangeEmpty.hidden = true;
|
|
399
|
+
if (pageSize <= 0) {
|
|
400
|
+
// No page-size known yet either — nothing to derive a window from.
|
|
401
|
+
if (rangeText) {
|
|
402
|
+
rangeText.hidden = false;
|
|
403
|
+
rangeText.textContent = nounSuffix ? `Loading${nounSuffix}…` : 'Loading…';
|
|
404
|
+
}
|
|
405
|
+
if (pagerWrap) pagerWrap.hidden = true;
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
const page = Math.max(1, Number(this.page) || 1);
|
|
409
|
+
const start = (page - 1) * pageSize + 1;
|
|
410
|
+
const end = page * pageSize;
|
|
411
|
+
if (rangeText) {
|
|
412
|
+
rangeText.hidden = false;
|
|
413
|
+
rangeText.textContent = `Showing ${start}–${end}${nounSuffix}`;
|
|
414
|
+
}
|
|
415
|
+
if (pagerWrap) {
|
|
416
|
+
pagerWrap.hidden = false;
|
|
417
|
+
}
|
|
418
|
+
if (pag) {
|
|
419
|
+
// Keeps `next` enabled indefinitely (never a false "last page") —
|
|
420
|
+
// mirrors table.class.js's own #pageCount open-mode math
|
|
421
|
+
// (gh#1877/ADR-0082 Amendment): total is always one page ahead of
|
|
422
|
+
// wherever the pager currently sits.
|
|
423
|
+
pag.setAttribute('page', String(page));
|
|
424
|
+
pag.setAttribute('total', String(page + 1));
|
|
425
|
+
pag.setAttribute('siblings', String(Math.max(0, Number(this.siblings) || 0)));
|
|
426
|
+
pag.setAttribute('size', this.size || 'md');
|
|
427
|
+
}
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
345
431
|
if (rangeTotal === 0) {
|
|
346
432
|
// REQ-S-002 — confirmed-empty. The empty slot always wins over the
|
|
347
433
|
// range text whenever range-total is confirmed zero, mutually
|
|
@@ -361,7 +447,7 @@ export class UITableFooter extends UIElement {
|
|
|
361
447
|
// "Showing 1–0 of N" math an unbounded pager would otherwise need.
|
|
362
448
|
if (rangeText) {
|
|
363
449
|
rangeText.hidden = false;
|
|
364
|
-
rangeText.textContent = `${rangeTotal} items`;
|
|
450
|
+
rangeText.textContent = `${rangeTotal} items${rangeOfSuffix}`;
|
|
365
451
|
}
|
|
366
452
|
if (pagerWrap) pagerWrap.hidden = true;
|
|
367
453
|
return;
|
|
@@ -374,7 +460,7 @@ export class UITableFooter extends UIElement {
|
|
|
374
460
|
|
|
375
461
|
if (rangeText) {
|
|
376
462
|
rangeText.hidden = false;
|
|
377
|
-
rangeText.textContent = `Showing ${start}–${end} of ${rangeTotal}`;
|
|
463
|
+
rangeText.textContent = `Showing ${start}–${end} of ${rangeTotal}${nounSuffix}${rangeOfSuffix}`;
|
|
378
464
|
}
|
|
379
465
|
|
|
380
466
|
if (pagerWrap) {
|
|
@@ -22,8 +22,12 @@ export class UITableFooter extends UIElement {
|
|
|
22
22
|
page: number;
|
|
23
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
24
|
pageSize: number;
|
|
25
|
-
/**
|
|
26
|
-
|
|
25
|
+
/** gh#1877, ADR-0082 Amendment — optional per-slice noun appended after the range total ("of 80 users") so consumers stop hand-painting the noun themselves. Applies in both the normal finite range text and the open/unproven-total text ("Showing 51–100 users" with no "of N"). Unset (the default) changes nothing. Named identically on table-toolbar-ui (ADR-0082 §"same name on both companions"). */
|
|
26
|
+
rangeNoun: string;
|
|
27
|
+
/** gh#1877, ADR-0082 Amendment — secondary whole-set total for the "filtered-of-whole" shape: a client-side facet narrows [range-total] (a filtered count) while range-of names the unfiltered set size, e.g. "Showing 1–4 of 4 traversals (6 total)". Appends "(of <range-of> total)" onto the normal finite range text; unset (the default) changes nothing. Never applies while [range-total] is absent (loading) or "?" (open/unproven) — there's no finite range text to append onto yet. Named identically on table-toolbar-ui (ADR-0082 §"same name on both companions"). */
|
|
28
|
+
rangeOf: number;
|
|
29
|
+
/** 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). Explicit range-total="?" (gh#1877, ADR-0082 Amendment) is the OPEN/unproven- total state — cursor/hasMore server paging with no known row count yet: the range label renders "Showing X–Y" with no "of N", and the pager stays in has-more mode (next enabled) rather than hiding — distinct from both "loading" (attribute absent) and "confirmed empty" (range-total="0"). Resolves to the normal finite behavior the moment a real number replaces the "?". 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 target's own "?" carries through identically); 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. */
|
|
30
|
+
rangeTotal: number | string;
|
|
27
31
|
/** Passed through to the composed pagination-ui's own [siblings] (page-number window size on each side of the current page). */
|
|
28
32
|
siblings: number;
|
|
29
33
|
/** Passed through to the composed pagination-ui's own [size] (sm | md | lg, the universal size system). */
|