@adia-ai/web-components 0.8.44 → 0.8.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/components/card/card.css +12 -23
  3. package/components/card/card.yaml +11 -0
  4. package/components/chart/chart.a2ui.json +16 -1
  5. package/components/chart/chart.class.js +611 -41
  6. package/components/chart/chart.css +174 -0
  7. package/components/chart/chart.d.ts +5 -1
  8. package/components/chart/chart.yaml +45 -1
  9. package/components/context-menu/context-menu.a2ui.json +8 -3
  10. package/components/context-menu/context-menu.class.js +46 -5
  11. package/components/context-menu/context-menu.d.ts +6 -3
  12. package/components/context-menu/context-menu.examples.md +2 -2
  13. package/components/context-menu/context-menu.yaml +22 -5
  14. package/components/field/field.css +24 -2
  15. package/components/index.js +1 -0
  16. package/components/input/input.css +7 -0
  17. package/components/nav/nav.a2ui.json +2 -2
  18. package/components/nav/nav.css +1 -1
  19. package/components/nav/nav.d.ts +1 -1
  20. package/components/nav/nav.yaml +14 -3
  21. package/components/nav-group/nav-group.css +37 -3
  22. package/components/pagination/pagination.class.js +52 -22
  23. package/components/search/search.class.js +39 -5
  24. package/components/select/select.a2ui.json +5 -0
  25. package/components/select/select.class.js +20 -0
  26. package/components/select/select.css +25 -0
  27. package/components/select/select.d.ts +2 -0
  28. package/components/select/select.yaml +12 -0
  29. package/components/table/cell-types.js +9 -0
  30. package/components/table/table.a2ui.json +19 -4
  31. package/components/table/table.class.js +410 -45
  32. package/components/table/table.css +7 -4
  33. package/components/table/table.d.ts +9 -3
  34. package/components/table/table.yaml +115 -9
  35. package/components/table-footer/table-footer.a2ui.json +150 -0
  36. package/components/table-footer/table-footer.class.js +391 -0
  37. package/components/table-footer/table-footer.css +64 -0
  38. package/components/table-footer/table-footer.d.ts +39 -0
  39. package/components/table-footer/table-footer.examples.md +46 -0
  40. package/components/table-footer/table-footer.js +17 -0
  41. package/components/table-footer/table-footer.yaml +219 -0
  42. package/components/table-toolbar/table-toolbar.a2ui.json +15 -0
  43. package/components/table-toolbar/table-toolbar.class.js +61 -17
  44. package/components/table-toolbar/table-toolbar.css +34 -0
  45. package/components/table-toolbar/table-toolbar.d.ts +10 -0
  46. package/components/table-toolbar/table-toolbar.yaml +58 -15
  47. package/core/data-stream.js +37 -2
  48. package/core/index.d.ts +1 -0
  49. package/core/index.js +1 -0
  50. package/core/provider.d.ts +9 -13
  51. package/core/provider.js +9 -113
  52. package/core/store.d.ts +46 -0
  53. package/core/store.js +89 -0
  54. package/custom-elements.json +192 -8
  55. package/dist/host.min.css +1 -1
  56. package/dist/host.sheet.js +1 -1
  57. package/dist/theme-provider.min.js +1 -1
  58. package/dist/web-components.min.css +1 -1
  59. package/dist/web-components.min.js +96 -96
  60. package/dist/web-components.sheet.js +1 -1
  61. package/package.json +1 -1
  62. package/patterns/chart-in-card/chart-in-card.examples.html +36 -9
  63. package/patterns/new-enrollments/new-enrollments.examples.html +140 -0
  64. package/patterns/new-enrollments/new-enrollments.html +54 -0
  65. package/patterns/table-in-card/table-in-card.examples.html +168 -0
  66. package/patterns/table-in-card/table-in-card.examples.js +139 -0
  67. package/patterns/table-in-card/table-in-card.html +86 -0
  68. package/styles/api/sizing.css +46 -0
  69. package/styles/components.css +1 -0
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * `<router-ui>` provider — declarative + imperative client-side
3
- * routing built on {@link RouteController} (re-declared here for
4
- * historical reasons; the canonical export lives in `core/controller.js`).
3
+ * routing built on {@link RouteController} (re-exported here for
4
+ * back-compat gh#1766 — the canonical implementation lives in
5
+ * `core/controller.js`).
5
6
  *
6
7
  * Two consumer paths:
7
8
  *
@@ -25,22 +26,17 @@
25
26
  */
26
27
 
27
28
  import { UIElement } from './element.js';
28
- import type { Route, RouteCommands, RouteState, ControllerSchema, RouteControllerOptions } from './controller.js';
29
- import { BaseController } from './controller.js';
29
+ import type { Route } from './controller.js';
30
+ import { RouteController } from './controller.js';
30
31
 
31
32
  /**
32
- * Routing-state controller. Re-declared from `core/controller.js` for
33
- * historical compatibilityboth files define the same class shape;
34
- * consumers can import either. New code should prefer
33
+ * Routing-state controller, re-exported from `core/controller.js`
34
+ * (gh#1766) for back-compat consumers can import either path.
35
+ * New code should prefer
35
36
  * `import { RouteController } from '@adia-ai/web-components/core/controller'`
36
37
  * for the canonical path.
37
38
  */
38
- export class RouteController extends BaseController {
39
- static schema: ControllerSchema;
40
- constructor(options?: RouteControllerOptions);
41
- getState(): RouteState;
42
- commands: RouteCommands;
43
- }
39
+ export { RouteController };
44
40
 
45
41
  /**
46
42
  * Optional async transform applied to fetched route content before
package/core/provider.js CHANGED
@@ -1,8 +1,10 @@
1
1
  /**
2
2
  * AdiaUI Router — Self-contained vanilla JS bundle.
3
- * Includes: RouteController + router-ui component.
3
+ * Includes: router-ui component; re-exports `RouteController` for
4
+ * back-compat (gh#1766 — the canonical definition lives in
5
+ * `core/controller.js`, alongside its `BaseController` superclass).
4
6
  *
5
- * Depends on: core.js (UIElement), controllers.js (BaseController)
7
+ * Depends on: element.js (UIElement), controller.js (RouteController)
6
8
  *
7
9
  * Usage:
8
10
  * import { UIElement } from './core.js';
@@ -24,7 +26,7 @@
24
26
 
25
27
  import { UIElement } from './element.js';
26
28
  import { defineIfFree } from './register.js';
27
- import { BaseController } from './controller.js';
29
+ import { RouteController } from './controller.js';
28
30
  import { viewTransition } from '../traits/view-transition/view-transition.js';
29
31
 
30
32
  // ═══════════════════════════════════════════════════════════════
@@ -46,118 +48,12 @@ class UIProvider extends UIElement {
46
48
  }
47
49
 
48
50
  // ═══════════════════════════════════════════════════════════════
49
- // ROUTE CONTROLLER
51
+ // ROUTE CONTROLLER — re-exported for back-compat (gh#1766); the
52
+ // canonical definition lives in `core/controller.js`, which is
53
+ // where `BaseController` (its own superclass) already lives.
50
54
  // ═══════════════════════════════════════════════════════════════
51
55
 
52
- export class RouteController extends BaseController {
53
- static schema = {
54
- name: 'route',
55
- state: { path: 'string', params: 'object', route: 'object', previous: 'string' },
56
- commands: ['navigate', 'replace', 'back', 'forward', 'setRoutes'],
57
- attributes: ['data-route-path'],
58
- };
59
-
60
- #routes = [];
61
- #path = '';
62
- #previous = '';
63
- #params = {};
64
- #route = null;
65
- #historySync = true;
66
- #boundPopState = null;
67
-
68
- constructor({ routes = [], initial, historySync = true } = {}) {
69
- super();
70
- this.#routes = routes;
71
- this.#historySync = historySync;
72
- this.#path = initial ?? location.pathname;
73
- this.#match();
74
- }
75
-
76
- getState() {
77
- return {
78
- path: this.#path,
79
- params: { ...this.#params },
80
- route: this.#route,
81
- previous: this.#previous,
82
- };
83
- }
84
-
85
- reflect() {
86
- const host = this.host;
87
- if (!host) return;
88
- host.setAttribute('data-route-path', this.#path);
89
- }
90
-
91
- onConnect(host) {
92
- if (this.#historySync) {
93
- this.#boundPopState = () => {
94
- this.#previous = this.#path;
95
- this.#path = location.pathname;
96
- this.#match();
97
- this.notify();
98
- };
99
- window.addEventListener('popstate', this.#boundPopState);
100
- }
101
- }
102
-
103
- onDisconnect() {
104
- if (this.#boundPopState) {
105
- window.removeEventListener('popstate', this.#boundPopState);
106
- this.#boundPopState = null;
107
- }
108
- }
109
-
110
- #match() {
111
- this.#params = {};
112
- this.#route = null;
113
- for (const route of this.#routes) {
114
- const match = this.#matchPath(route.path, this.#path);
115
- if (match) {
116
- this.#params = match.params;
117
- this.#route = route;
118
- return;
119
- }
120
- }
121
- }
122
-
123
- #matchPath(pattern, path) {
124
- const patternParts = pattern.split('/').filter(Boolean);
125
- const pathParts = path.split('/').filter(Boolean);
126
- if (!pattern.includes(':')) return pattern === path ? { params: {} } : null;
127
- if (patternParts.length !== pathParts.length) return null;
128
- const params = {};
129
- for (let i = 0; i < patternParts.length; i++) {
130
- if (patternParts[i].startsWith(':')) params[patternParts[i].slice(1)] = pathParts[i];
131
- else if (patternParts[i] !== pathParts[i]) return null;
132
- }
133
- return { params };
134
- }
135
-
136
- commands = {
137
- navigate: (path) => {
138
- if (path === this.#path) return;
139
- this.#previous = this.#path;
140
- this.#path = path;
141
- this.#match();
142
- if (this.#historySync) history.pushState(null, '', path);
143
- this.notify();
144
- },
145
- replace: (path) => {
146
- this.#previous = this.#path;
147
- this.#path = path;
148
- this.#match();
149
- if (this.#historySync) history.replaceState(null, '', path);
150
- this.notify();
151
- },
152
- back: () => { if (this.#historySync) history.back(); },
153
- forward: () => { if (this.#historySync) history.forward(); },
154
- setRoutes: (routes) => {
155
- this.#routes = routes;
156
- this.#match();
157
- this.notify();
158
- },
159
- };
160
- }
56
+ export { RouteController };
161
57
 
162
58
  // ═══════════════════════════════════════════════════════════════
163
59
  // ROUTER-UI — content fragment renderer
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Shared app store — the blessed R2 primitive. Signal-backed, with a
3
+ * Set-of-listeners-compatible `subscribe()` channel for imperative
4
+ * (non-`effect()`) consumers.
5
+ *
6
+ * @see ./store.js (runtime SoT)
7
+ * @see ../USAGE.md
8
+ */
9
+
10
+ /**
11
+ * A `signal()`-backed store. `.value` composes with `computed()`/`effect()`
12
+ * exactly like a plain signal; `subscribe()` is the extra imperative
13
+ * channel the seven hand-rolled app-layer stores already share (see
14
+ * `store.js`'s header for the full rationale and the rejected
15
+ * BaseController alternative).
16
+ */
17
+ export interface Store<T> {
18
+ /** Tracked read/write, same semantics as `Signal<T>.value`. */
19
+ value: T;
20
+ /** Read without subscribing the surrounding `effect()`. */
21
+ peek(): T;
22
+ /**
23
+ * Subscribe to changes. `cb` receives the new value on every write that
24
+ * doesn't fail the `Object.is` no-notify guard. Returns an unsubscribe
25
+ * function — safe to call more than once. Compatible with
26
+ * `UIElement`'s `controller` setter (`element.controller = store`).
27
+ */
28
+ subscribe(cb: (value: T) => void): () => void;
29
+ }
30
+
31
+ /**
32
+ * Create a shared app store.
33
+ *
34
+ * @example
35
+ * const count = createStore(0);
36
+ * const stop = count.subscribe((v) => console.log('count is', v));
37
+ * count.value = 1; // logs "count is 1"
38
+ * count.value = 1; // no-op — Object.is guard, no log
39
+ * stop();
40
+ *
41
+ * @example Controller-setter interop
42
+ * class MyEl extends UIElement {}
43
+ * const el = new MyEl();
44
+ * el.controller = createStore({ ready: false }); // re-renders on change
45
+ */
46
+ export function createStore<T>(initial: T): Store<T>;
package/core/store.js ADDED
@@ -0,0 +1,89 @@
1
+ /**
2
+ * AdiaUI shared app store — the blessed R2 primitive.
3
+ *
4
+ * A signal-backed replacement for the Set-of-listeners pub/sub pattern
5
+ * hand-rolled across the app layer: plan-store, data-client, patient-visit
6
+ * record, persona store, task-service, a2ui BaseController, a2ui Surface
7
+ * watchers (reactivity review, .claude/docs/reports/2026-08-20-reactivity-
8
+ * review/03-app-layer-stores.md §4). Doctrine's pattern menu row 1 says
9
+ * reactive state -> signals; `createStore` is `signal()` plus the one thing
10
+ * those seven stores actually needed on top of it: an imperative
11
+ * `subscribe(cb) -> unsubscribe` channel for consumers that are closures or
12
+ * classes rather than `effect()` bodies.
13
+ *
14
+ * `.value` reads/writes pass straight through to a real `signal()`, so a
15
+ * store composes with `computed()`/`effect()` exactly like any other signal
16
+ * (tracked reads, `Object.is` no-notify, microtask-batched effect re-runs).
17
+ * `subscribe()` is the separate, synchronous, imperative channel the seven
18
+ * hand-rolled stores already share — same shape, so migrating one onto this
19
+ * primitive is a mechanical swap (opportunistic, later, per store; this
20
+ * primitive lands with NO migration).
21
+ *
22
+ * Interops with `UIElement`'s `controller` setter (`core/element.js`)
23
+ * out of the box: `element.controller = store` wires `store.subscribe(fn)`
24
+ * into the element's own render-trigger machinery — the setter calls
25
+ * `c.connect?.(this)` / `c.subscribe?.(...)` / `c.disconnect?.(this)`, all
26
+ * optional-chained, and `subscribe()` alone satisfies that contract.
27
+ *
28
+ * Rejected alternative: promoting/absorbing `BaseController`
29
+ * (`core/controller.js`) into this role, per the reactivity review's F5.
30
+ * BaseController's `connect()` bakes in schema/`getState()` validation
31
+ * (dev-warns without a static `schema`, dev-errors without an overridden
32
+ * `getState()`) that exists to serve DOM-reflecting UI controllers
33
+ * (`RouteController`'s attributes/commands surface) — semantics a plain
34
+ * data store has no use for and would either have to satisfy with a dummy
35
+ * schema or suppress. The one thing BaseController actually offers this
36
+ * role — the `subscribe(fn) -> unsubscribe` shape the controller setter
37
+ * expects — costs nothing to reimplement directly on `signal()`, and doing
38
+ * so keeps the store from inheriting a validation path built for a
39
+ * different kind of thing. BaseController stays as-is; still one consumer
40
+ * (`RouteController` / `<router-ui>`), still F5's to resolve separately.
41
+ *
42
+ * @see ../USAGE.md
43
+ * @see ./element.js (the `controller` setter this interops with)
44
+ * @see ./controller.js (BaseController — the rejected alternative above)
45
+ */
46
+
47
+ import { signal } from './signals.js';
48
+
49
+ export function createStore(initial) {
50
+ const s = signal(initial);
51
+ const listeners = new Set();
52
+
53
+ return {
54
+ get value() {
55
+ return s.value;
56
+ },
57
+ set value(next) {
58
+ // Object.is no-notify guard — matches persona-store's proven
59
+ // behavior (packages/llm/persona/src/store.ts) and the underlying
60
+ // signal's own equality cutoff. Checked here, not inferred from
61
+ // signal()'s silent no-op, so an identical write never reaches
62
+ // `listeners` either.
63
+ const changed = !Object.is(s.peek(), next);
64
+ s.value = next;
65
+ if (changed) {
66
+ // Pass the captured `next`, never re-read `s.peek()` here — a
67
+ // listener that re-entrantly writes `store.value` during this
68
+ // loop would otherwise make every later listener observe THAT
69
+ // write's value instead of this one's, silently skipping the
70
+ // value this write actually delivered.
71
+ for (const cb of listeners) cb(next);
72
+ }
73
+ },
74
+ /** Read without subscribing the surrounding `effect()`. */
75
+ peek() {
76
+ return s.peek();
77
+ },
78
+ /**
79
+ * Set-of-listeners-compatible subscribe. `cb` receives the new value;
80
+ * callers that only care that *something* changed (the
81
+ * `UIElement.controller` setter among them) simply ignore the
82
+ * argument. Returns an unsubscribe function.
83
+ */
84
+ subscribe(cb) {
85
+ listeners.add(cb);
86
+ return () => listeners.delete(cb);
87
+ },
88
+ };
89
+ }
@@ -2091,6 +2091,20 @@
2091
2091
  },
2092
2092
  "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) — this attribute lands the grammar + snap plumbing only; per-type studied renderings per bucket are later waves (gh#1624 plan steps 2-4)."
2093
2093
  },
2094
+ {
2095
+ "name": "labels",
2096
+ "type": {
2097
+ "text": "string"
2098
+ },
2099
+ "description": "Chart 2.0 axis-label mode (ADR-0081). Unset (default, \"\") always resolves to `outside` — today's pre-2.0 outside-the-plot axis labels, byte-for-byte unchanged. Set explicitly to `chip` to opt into the pill-treated overlay labels inset within the plot box (never hanging outside it) — the resolved value reflects onto the host as `data-labels-resolved=\"chip\"|\"outside\"`. Deliberately explicit-opt-in rather than auto-detected from full-bleed ancestry (`section[bleed]`/ `card-ui[padding=\"none\"]`): an earlier draft auto-activated chip mode on any full-bleed composition, which silently changed the rendering of every ALREADY-SHIPPED full-bleed chart the moment it shipped — this build's own 60-fixture Charts visual-eval floor caught it (2 `comp-chart-in-card-n-*` regressions). `chart-in-card` compositions opt in explicitly once their own examples are updated (gh#1805)."
2100
+ },
2101
+ {
2102
+ "name": "today",
2103
+ "type": {
2104
+ "text": "string"
2105
+ },
2106
+ "description": "Names the datum whose x-axis value marks \"today\" (ADR-0081). Unset (default, \"\") renders no marker — additive. Compared against each datum's x-key value via a loose string comparison (`String(datum[x]) === String(today)`), so category axes (\"month\", \"region\") work the same as literal date strings; chart-ui never auto-derives today from the clock. Renders a baseline dot + a short band tick (REQ-F-008) on cartesian types (bar, line, area, scatter, multi-line, stacked-bar, grouped-bar, composed); a no-op on every radial/part-to-whole type and on sparkline (gh#1690's N/A list). Fallback (not the primary mechanism): a datum already carrying a truthy `today` key is treated as an equivalent marker even with this attribute unset."
2107
+ },
2094
2108
  {
2095
2109
  "name": "smooth",
2096
2110
  "type": {
@@ -2158,7 +2172,7 @@
2158
2172
  "text": "array"
2159
2173
  },
2160
2174
  "default": "[]",
2161
- "description": "JS property (set programmatically — `el.data = [...]`). An array of plain objects; each object's keys are named by the `x` and `y` attributes — e.g. `<chart-ui x=\"month\" y=\"revenue\">` consumes `[{month:'Jan', revenue:3200}, {month:'Feb', revenue:4100}]`. The Chart.js `{labels, datasets}` envelope is NOT chart-ui's API — passing it (or any non-array value) renders an empty chart. May also be supplied declaratively as a JSON-array `data=\"[…]\"` attribute, hydrated once at connect. Custom accessor on the element class, not a reflected attribute."
2175
+ "description": "JS property (set programmatically — `el.data = [...]`). An array of plain objects; each object's keys are named by the `x` and `y` attributes — e.g. `<chart-ui x=\"month\" y=\"revenue\">` consumes `[{month:'Jan', revenue:3200}, {month:'Feb', revenue:4100}]`. The Chart.js `{labels, datasets}` envelope is NOT chart-ui's API — passing it (or any non-array value) renders an empty chart. May also be supplied declaratively as a JSON-array `data=\"[…]\"` attribute, hydrated once at connect. Custom accessor on the element class, not a reflected attribute. A datum object carrying a truthy `provisional` key (ADR-0081) renders that period's type-specific incomplete-period treatment (hollow/dashed bar, dashed line + faded area, hollow ring dot) on cartesian types; a no-op on radial/part-to-whole types and sparkline (gh#1690's N/A list). Provisional state is per-datum data-shape, not a chart-ui attribute — see ADR-0081 §3."
2162
2176
  }
2163
2177
  ],
2164
2178
  "slots": [
@@ -3271,7 +3285,7 @@
3271
3285
  "declarations": [
3272
3286
  {
3273
3287
  "kind": "class",
3274
- "description": "Right-click activated menu — the OS-native context-menu pattern as a web component. Distinct from `menu-ui` (which is button-triggered): same item shape (`menu-item-ui` children), different trigger surface (`contextmenu` event), and pointer-anchored positioning instead of element-anchored. Pattern: WAI-APG Menu. Two binding modes: **A. Wrap.** Default-slot child becomes the target: `<context-menu-ui><my-table>...</my-table>...items</context-menu-ui>`. **B. Selector.** Point at one or more existing elements via [for]: `<context-menu-ui for=\"#my-table\">...items</context-menu-ui>`. On `contextmenu` event on a target: `preventDefault()`, position the menu at the pointer coords, show via Popover API. Touch long-press (configurable via [long-press-ms]) does the same. Shift+F10 / Menu key opens at the focused target's center for keyboard users.",
3288
+ "description": "Right-click activated menu — the OS-native context-menu pattern as a web component. Distinct from `menu-ui` (which is button-triggered): same item shape (`menu-item-ui` children), different trigger surface (`contextmenu` event), and pointer-anchored positioning instead of element-anchored. Pattern: WAI-APG Menu. Two binding modes: **A. Wrap.** Default-slot child becomes the target: `<context-menu-ui><my-table>...</my-table>...items</context-menu-ui>`. **B. Selector.** Point at one or more existing elements via [target-selector]: `<context-menu-ui target-selector=\"#my-table\">...items</context-menu-ui>`. On `contextmenu` event on a target: `preventDefault()`, position the menu at the pointer coords, show via Popover API. Touch long-press (configurable via [long-press-ms]) does the same. Shift+F10 / Menu key opens at the focused target's center for keyboard users.",
3275
3289
  "name": "UIContextMenu",
3276
3290
  "tagName": "context-menu-ui",
3277
3291
  "superclass": {
@@ -3279,12 +3293,19 @@
3279
3293
  "module": "/core/element.js"
3280
3294
  },
3281
3295
  "attributes": [
3296
+ {
3297
+ "name": "target-selector",
3298
+ "type": {
3299
+ "text": "string"
3300
+ },
3301
+ "description": "CSS selector(s) for target element(s). Empty = use default-slot child. Ratified name (gh#1764/#1780, ADR-0079) for what [for] used to carry — [for] means an id-ref everywhere else in AdiaUI (table-toolbar-ui, chart-legend-ui, tooltip-ui); context-menu-ui's own selector meaning predates that convention and was a same-name/ different-contract collision (ADR-0053's no-shadowing doctrine)."
3302
+ },
3282
3303
  {
3283
3304
  "name": "for",
3284
3305
  "type": {
3285
3306
  "text": "string"
3286
3307
  },
3287
- "description": "CSS selector(s) for target element(s). Empty = use default-slot child."
3308
+ "description": "DEPRECATED alias for [target-selector] — still a CSS selector, NOT an id-ref. Honored when [target-selector] is unset, with a one-shot console.warn pointing consumers at the replacement. New authoring should use [target-selector]."
3288
3309
  },
3289
3310
  {
3290
3311
  "name": "open",
@@ -6519,7 +6540,7 @@
6519
6540
  "type": {
6520
6541
  "text": "string"
6521
6542
  },
6522
- "description": "Optional kicker label. Section variant renders it via ::before; primary uses it as aria-label only."
6543
+ "description": "Optional kicker label. Section variant renders it via ::before; primary uses it as aria-label only. For a VISIBLE kicker inside a primary-variant rail — or more than one kicker per <nav-ui> — hand-place <span data-nav-label> in the default slot instead; see slots below."
6523
6544
  },
6524
6545
  {
6525
6546
  "name": "multi-expand",
@@ -6533,7 +6554,7 @@
6533
6554
  "slots": [
6534
6555
  {
6535
6556
  "name": "default",
6536
- "description": "Primary slot — accepts <nav-group-ui> + <nav-item-ui> children, plus <hr data-nav-divider> for hand-placed dividers."
6557
+ "description": "Primary slot — accepts <nav-group-ui> + <nav-item-ui> children, plus <hr data-nav-divider> for hand-placed dividers and <span data-nav-label> for hand-placed group-label kickers (titled runs of items/groups that aren't wrapped in a <nav-group-ui>). <span data-nav-label> renders with the same uppercase/tracking/muted kicker treatment as the [heading] ::before kicker, and is hidden alongside dividers whenever the primary-variant rail collapses ([collapsed] or ≤96px container width)."
6537
6558
  }
6538
6559
  ],
6539
6560
  "events": [
@@ -8566,6 +8587,14 @@
8566
8587
  },
8567
8588
  "description": "Label text above the trigger"
8568
8589
  },
8590
+ {
8591
+ "name": "label-hidden",
8592
+ "type": {
8593
+ "text": "boolean"
8594
+ },
8595
+ "default": "false",
8596
+ "description": "When true, [label] still sets the accessible name (aria-label) but the visible `::before` text is suppressed via the canonical sr-only technique (gh#1748, mirrors check-ui's [label-hidden], gh#1010). Use when a sibling/ancestor composition already conveys the same name visually (e.g. a scope/view-switcher select in a table-toolbar-ui [slot=\"scope\"], where the toolbar's own title already names the view) and a second visible \"Scope\"-style label would paint it twice."
8597
+ },
8569
8598
  {
8570
8599
  "name": "hint",
8571
8600
  "type": {
@@ -9897,6 +9926,103 @@
9897
9926
  }
9898
9927
  ]
9899
9928
  },
9929
+ {
9930
+ "kind": "javascript-module",
9931
+ "path": "components/table-footer/table-footer.js",
9932
+ "declarations": [
9933
+ {
9934
+ "kind": "class",
9935
+ "description": "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.",
9936
+ "name": "UITableFooter",
9937
+ "tagName": "table-footer-ui",
9938
+ "superclass": {
9939
+ "name": "UIElement",
9940
+ "module": "/core/element.js"
9941
+ },
9942
+ "attributes": [
9943
+ {
9944
+ "name": "for",
9945
+ "type": {
9946
+ "text": "string"
9947
+ },
9948
+ "description": "id-ref of the table-ui to control. Falls back to the first sibling table-ui within the same parent when omitted."
9949
+ },
9950
+ {
9951
+ "name": "page",
9952
+ "type": {
9953
+ "text": "number"
9954
+ },
9955
+ "default": "1",
9956
+ "description": "Current page, 1-based. Explicit attributes always win over derivation (per-attribute, not all-or-nothing) — see page-size/range-total."
9957
+ },
9958
+ {
9959
+ "name": "page-size",
9960
+ "type": {
9961
+ "text": "number"
9962
+ },
9963
+ "default": "0",
9964
+ "description": "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."
9965
+ },
9966
+ {
9967
+ "name": "range-total",
9968
+ "type": {
9969
+ "text": "number"
9970
+ },
9971
+ "default": "0",
9972
+ "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). 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."
9973
+ },
9974
+ {
9975
+ "name": "siblings",
9976
+ "type": {
9977
+ "text": "number"
9978
+ },
9979
+ "default": "1",
9980
+ "description": "Passed through to the composed pagination-ui's own [siblings] (page-number window size on each side of the current page)."
9981
+ },
9982
+ {
9983
+ "name": "size",
9984
+ "type": {
9985
+ "text": "string"
9986
+ },
9987
+ "default": "md",
9988
+ "description": "Passed through to the composed pagination-ui's own [size] (sm | md | lg, the universal size system)."
9989
+ }
9990
+ ],
9991
+ "slots": [
9992
+ {
9993
+ "name": "empty",
9994
+ "description": "Renders in place of the range label — the same [data-range] region — when [range-total] is EXPLICITLY present and equal to \"0\" (checked via hasAttribute, never the computed value alone, so an unset range-total during loading never flashes this content). Mutually exclusive with a positive [range-total]: the normal \"Showing X–Y of N\" (or count-only) text always wins whenever range-total resolves positive. Plain author-supplied markup — table-footer-ui does not template or constrain its shape. Naming matches table-toolbar-ui's own `empty` slot convention."
9995
+ }
9996
+ ],
9997
+ "events": [
9998
+ {
9999
+ "name": "footer-page",
10000
+ "type": {
10001
+ "text": "CustomEvent"
10002
+ },
10003
+ "description": "gh#1807, ADR-0080 (events-only interaction contract, extending ADR-0079's sender-prefixed toolbar-* precedent to a second sender) — dispatched directly at the resolved [for] target on every pager interaction. Detail: { page } (1-based). table-ui listens for this on itself, applied only when [paginate] > 0 (clamped to the valid page range; a non-numeric/NaN detail.page is a no-op); at [paginate=\"0\"] the command is a documented no-op on the table. Additive: any consumer, not only table-footer-ui, may dispatch this at a table-ui."
10004
+ },
10005
+ {
10006
+ "name": "page-change",
10007
+ "type": {
10008
+ "text": "CustomEvent"
10009
+ },
10010
+ "description": "The footer's own consumer-facing notification — a distinct name from the inbound footer-page command (ADR-0079's command≠notification separation). Fires on every pager interaction, in both client- and server-mode. Detail: { page } (1-based). This is the server-mode fetch hook: a consumer listens for it, fetches the new page, then sets `table.data` and the footer's own `page` (and `range-total` if the server's total moved) — the footer never optimistically advances its own state in server-mode."
10011
+ }
10012
+ ]
10013
+ }
10014
+ ],
10015
+ "exports": [
10016
+ {
10017
+ "kind": "custom-element-definition",
10018
+ "name": "table-footer-ui",
10019
+ "declaration": {
10020
+ "name": "UITableFooter",
10021
+ "module": "./components/table-footer/table-footer.js"
10022
+ }
10023
+ }
10024
+ ]
10025
+ },
9900
10026
  {
9901
10027
  "kind": "javascript-module",
9902
10028
  "path": "components/table-toolbar/table-toolbar.js",
@@ -10089,6 +10215,41 @@
10089
10215
  "text": "CustomEvent"
10090
10216
  },
10091
10217
  "description": "Page-size select changed. Detail: { pageSize }."
10218
+ },
10219
+ {
10220
+ "name": "toolbar-search",
10221
+ "type": {
10222
+ "text": "CustomEvent"
10223
+ },
10224
+ "description": "gh#1764/#1780, ADR-0079 (events-only interaction contract) — dispatched directly at the resolved [for] target (not bubbled from this element) in place of the pre-#1780 direct `.search =` write. table-ui listens for this on itself. Detail: { value }."
10225
+ },
10226
+ {
10227
+ "name": "toolbar-filter-set",
10228
+ "type": {
10229
+ "text": "CustomEvent"
10230
+ },
10231
+ "description": "gh#1764/#1780, ADR-0079 — dispatched directly at the resolved [for] target in place of the pre-#1780 direct `target.setFilter()` call. table-ui listens for this on itself. Detail: { key, value, op }; a null `value` clears that one column's filter."
10232
+ },
10233
+ {
10234
+ "name": "toolbar-filter-clear",
10235
+ "type": {
10236
+ "text": "CustomEvent"
10237
+ },
10238
+ "description": "gh#1764/#1780, ADR-0079 — dispatched directly at the resolved [for] target in place of the pre-#1780 direct `target.clearFilters()` call. table-ui listens for this on itself. No detail."
10239
+ },
10240
+ {
10241
+ "name": "toolbar-columns-set",
10242
+ "type": {
10243
+ "text": "CustomEvent"
10244
+ },
10245
+ "description": "gh#1764/#1780, ADR-0079 — dispatched directly at the resolved [for] target in place of the pre-#1780 direct `target.columns =` write. table-ui listens for this on itself. Detail: { columns }."
10246
+ },
10247
+ {
10248
+ "name": "toolbar-paginate",
10249
+ "type": {
10250
+ "text": "CustomEvent"
10251
+ },
10252
+ "description": "gh#1764/#1780, ADR-0079 — dispatched directly at the resolved [for] target in place of the pre-#1780 direct `target.paginate =` write. table-ui listens for this on itself. Detail: { pageSize }."
10092
10253
  }
10093
10254
  ]
10094
10255
  }
@@ -10156,7 +10317,30 @@
10156
10317
  "text": "number"
10157
10318
  },
10158
10319
  "default": "0",
10159
- "description": "Rows per page. 0 = show all rows without pagination. When > 0, renders a pagination bar"
10320
+ "description": "Rows per page. 0 = show all rows without pagination. When > 0, renders the internal"
10321
+ },
10322
+ {
10323
+ "name": "no-pager",
10324
+ "type": {
10325
+ "text": "boolean"
10326
+ },
10327
+ "default": "false",
10328
+ "description": "Hide the internal pagination bar while leaving [paginate] slicing, page state, the `page` event, and the `footer-page` command listener all intact (gh#1807, ADR-0080). The anti-doubled-pager mechanism for a table-footer-ui composition, client- or server-mode alike (gh#1754, ADR-0082): without it, a bound footer's own composed pager and the table's own internal one would both render for any non-empty paginated table. Explicit author intent — never suppressed automatically just because a footer happens to be bound."
10329
+ },
10330
+ {
10331
+ "name": "range-total",
10332
+ "type": {
10333
+ "text": "number"
10334
+ },
10335
+ "default": "0",
10336
+ "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."
10337
+ },
10338
+ {
10339
+ "name": "filteredCount",
10340
+ "type": {
10341
+ "text": "number"
10342
+ },
10343
+ "description": "Read-only. The row count AFTER search + column filters apply but BEFORE pagination slices it (gh#1807, ADR-0080, REQ-W-006) — a plain JS getter, no setter, never a reflected attribute. table-footer-ui's client-mode range-total derivation reads this directly; a raw `.data.length` read (the toolbar's own `count` fallback precedent) would double-count a filtered-out row. In server mode ([range-total] set, gh#1754, ADR-0082), filteredCount stays page-scoped — the loaded page's rows after local filters, NOT the server total across all pages; [range-total] is the server total, filteredCount never is (REQ-D-006)."
10160
10344
  },
10161
10345
  {
10162
10346
  "name": "raw",
@@ -10171,7 +10355,7 @@
10171
10355
  "type": {
10172
10356
  "text": "string"
10173
10357
  },
10174
- "description": "Global search/filter string. Filters visible rows across all columns using"
10358
+ "description": "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."
10175
10359
  },
10176
10360
  {
10177
10361
  "name": "selectable",
@@ -10257,7 +10441,7 @@
10257
10441
  "type": {
10258
10442
  "text": "CustomEvent"
10259
10443
  },
10260
- "description": "Fired when the user navigates to a different page."
10444
+ "description": "Fired when the table's own internal page state changes — an internal pager click, or a `footer-page` command applied (REQ-W-002). This is a RESYNC notification a bound `<table-footer-ui>` listens to for its own `page` attribute (REQ-W-004) — it is NOT the public server-mode fetch trigger a consumer should listen to; use `<table-footer-ui>`'s own `page-change` event for that (1-based, gh#1754/ADR-0082 REQ-W-003). Listening to both risks a duplicate fetch or an index-base mismatch."
10261
10445
  },
10262
10446
  {
10263
10447
  "name": "resize",