@adia-ai/web-components 0.8.44 → 0.8.45

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 (43) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/components/context-menu/context-menu.a2ui.json +8 -3
  3. package/components/context-menu/context-menu.class.js +46 -5
  4. package/components/context-menu/context-menu.d.ts +6 -3
  5. package/components/context-menu/context-menu.examples.md +2 -2
  6. package/components/context-menu/context-menu.yaml +22 -5
  7. package/components/nav/nav.a2ui.json +2 -2
  8. package/components/nav/nav.css +1 -1
  9. package/components/nav/nav.d.ts +1 -1
  10. package/components/nav/nav.yaml +14 -3
  11. package/components/nav-group/nav-group.css +37 -3
  12. package/components/pagination/pagination.class.js +52 -22
  13. package/components/search/search.class.js +39 -5
  14. package/components/select/select.a2ui.json +5 -0
  15. package/components/select/select.class.js +20 -0
  16. package/components/select/select.css +25 -0
  17. package/components/select/select.d.ts +2 -0
  18. package/components/select/select.yaml +12 -0
  19. package/components/table/cell-types.js +9 -0
  20. package/components/table/table.class.js +247 -33
  21. package/components/table/table.css +7 -4
  22. package/components/table/table.yaml +6 -1
  23. package/components/table-toolbar/table-toolbar.a2ui.json +15 -0
  24. package/components/table-toolbar/table-toolbar.class.js +61 -17
  25. package/components/table-toolbar/table-toolbar.css +34 -0
  26. package/components/table-toolbar/table-toolbar.d.ts +10 -0
  27. package/components/table-toolbar/table-toolbar.yaml +41 -9
  28. package/core/data-stream.js +37 -2
  29. package/core/index.d.ts +1 -0
  30. package/core/index.js +1 -0
  31. package/core/provider.d.ts +9 -13
  32. package/core/provider.js +9 -113
  33. package/core/store.d.ts +46 -0
  34. package/core/store.js +89 -0
  35. package/custom-elements.json +54 -4
  36. package/dist/host.min.css +1 -1
  37. package/dist/host.sheet.js +1 -1
  38. package/dist/theme-provider.min.js +1 -1
  39. package/dist/web-components.min.css +1 -1
  40. package/dist/web-components.min.js +93 -93
  41. package/dist/web-components.sheet.js +1 -1
  42. package/package.json +1 -1
  43. package/styles/api/sizing.css +46 -0
@@ -29,11 +29,22 @@
29
29
  * Popovers use the platform Popover API + core/anchor.js, the same primitives
30
30
  * that menu-ui / popover-ui / toolbar-ui already use in this package.
31
31
  *
32
- * State flow:
33
- * search → table.search (string property)
34
- * filters → table.setFilter() (per-key)
35
- * sort → simulated click on table's [data-sort-key] header
36
- * column hidden table.columns = (clone with hidden flag flipped)
32
+ * State flow — events-only interaction contract (gh#1764/#1780, ADR-0079;
33
+ * mirrors chart-legend-ui↔chart-ui's own bubbling-CustomEvent model). The
34
+ * toolbar never writes a property or calls a method on the bound table
35
+ * directly every command is a `toolbar-*` CustomEvent dispatched AT the
36
+ * resolved target, which table.class.js listens for in its own
37
+ * connected()/disconnected() and applies via its existing public surface:
38
+ * search → `toolbar-search` { value } → table.search =
39
+ * filters → `toolbar-filter-set` { key, value, op } → table.setFilter()
40
+ * filters clear → `toolbar-filter-clear` {} → table.clearFilters()
41
+ * sort → simulated click on table's [data-sort-key] header (already
42
+ * event-shaped — a real DOM event dispatched at the target,
43
+ * not a property write; left as-is, see gh#1780's PR notes)
44
+ * column hidden → `toolbar-columns-set` { columns } → table.columns =
45
+ * page size → `toolbar-paginate` { pageSize } → table.paginate =
46
+ * The table→toolbar direction (`sort`, `filter-change` listened on #target)
47
+ * was already events-only and is unchanged.
37
48
  */
38
49
 
39
50
  import { UIElement } from '../../core/element.js';
@@ -846,11 +857,16 @@ export class UITableToolbar extends UIElement {
846
857
  #onPageSizeChange = (e) => {
847
858
  const pageSize = Number(e.detail?.value ?? this.querySelector(':scope [data-page-size]')?.value) || 0;
848
859
  this.pageSize = pageSize;
849
- // Applied directly to the bound table's own [paginate] (rows-per-page)
850
- // prop the same "mirror to URL/analytics via the event, but the
851
- // toolbar already wires the change into the target" pattern
852
- // columns-change/sort-change already use.
853
- if (this.#target) this.#target.paginate = pageSize;
860
+ // gh#1764/#1780 (events-only interaction contract, ADR-0079) the
861
+ // bound table applies its own [paginate] through the `toolbar-paginate`
862
+ // command event rather than a direct property write; table.class.js
863
+ // listens for it in connected() and sets `this.paginate` itself.
864
+ if (this.#target) {
865
+ this.#target.dispatchEvent(new CustomEvent('toolbar-paginate', {
866
+ bubbles: true,
867
+ detail: { pageSize },
868
+ }));
869
+ }
854
870
  this.dispatchEvent(new CustomEvent('page-size-change', {
855
871
  bubbles: true,
856
872
  detail: { pageSize },
@@ -902,7 +918,15 @@ export class UITableToolbar extends UIElement {
902
918
 
903
919
  #onSearch = (e) => {
904
920
  const value = e.detail?.value ?? '';
905
- if (this.#target) this.#target.search = value;
921
+ // gh#1764/#1780 (events-only interaction contract, ADR-0079) —
922
+ // `toolbar-search` replaces the direct `.search =` write; table.class.js
923
+ // listens for it in connected() and sets `this.search` itself.
924
+ if (this.#target) {
925
+ this.#target.dispatchEvent(new CustomEvent('toolbar-search', {
926
+ bubbles: true,
927
+ detail: { value },
928
+ }));
929
+ }
906
930
  this.dispatchEvent(new CustomEvent('search', {
907
931
  bubbles: true,
908
932
  detail: { value },
@@ -1044,8 +1068,14 @@ export class UITableToolbar extends UIElement {
1044
1068
  }
1045
1069
  sel.addEventListener('change', () => {
1046
1070
  const v = sel.value || '';
1047
- if (v) target.setFilter(col.key, v, 'select');
1048
- else target.setFilter(col.key, null);
1071
+ // gh#1764/#1780 (events-only interaction contract, ADR-0079)
1072
+ // `toolbar-filter-set` replaces the direct target.setFilter()
1073
+ // call; table.class.js applies it synchronously in its own
1074
+ // listener, so `target.filters` below already reflects it.
1075
+ target.dispatchEvent(new CustomEvent('toolbar-filter-set', {
1076
+ bubbles: true,
1077
+ detail: { key: col.key, value: v || null, op: 'select' },
1078
+ }));
1049
1079
  this.dispatchEvent(new CustomEvent('filter-change', {
1050
1080
  bubbles: true,
1051
1081
  detail: { filters: target.filters },
@@ -1061,8 +1091,12 @@ export class UITableToolbar extends UIElement {
1061
1091
  if (current?.op === 'contains') input.value = current.value ?? '';
1062
1092
  input.addEventListener('input', () => {
1063
1093
  const v = input.value;
1064
- if (v) target.setFilter(col.key, v, 'contains');
1065
- else target.setFilter(col.key, null);
1094
+ // gh#1764/#1780 (events-only interaction contract, ADR-0079)
1095
+ // see the select-filter handler above for the same normalization.
1096
+ target.dispatchEvent(new CustomEvent('toolbar-filter-set', {
1097
+ bubbles: true,
1098
+ detail: { key: col.key, value: v || null, op: 'contains' },
1099
+ }));
1066
1100
  this.dispatchEvent(new CustomEvent('filter-change', {
1067
1101
  bubbles: true,
1068
1102
  detail: { filters: target.filters },
@@ -1078,7 +1112,11 @@ export class UITableToolbar extends UIElement {
1078
1112
 
1079
1113
  if (Object.keys(filters).length) {
1080
1114
  const clear = this.#mkPopoverAction('Clear all filters', () => {
1081
- target.clearFilters();
1115
+ // gh#1764/#1780 (events-only interaction contract, ADR-0079)
1116
+ // `toolbar-filter-clear` replaces the direct target.clearFilters()
1117
+ // call; table.class.js applies it synchronously in its own
1118
+ // listener before #refreshFilterPanel() re-reads target.filters.
1119
+ target.dispatchEvent(new CustomEvent('toolbar-filter-clear', { bubbles: true }));
1082
1120
  this.dispatchEvent(new CustomEvent('filter-change', {
1083
1121
  bubbles: true,
1084
1122
  detail: { filters: {} },
@@ -1227,7 +1265,13 @@ export class UITableToolbar extends UIElement {
1227
1265
  const next = target.columns.map((c) => (
1228
1266
  c.key === col.key ? { ...c, hidden: !check.hasAttribute('checked') } : { ...c }
1229
1267
  ));
1230
- target.columns = next;
1268
+ // gh#1764/#1780 (events-only interaction contract, ADR-0079) —
1269
+ // `toolbar-columns-set` replaces the direct target.columns= write;
1270
+ // table.class.js applies it synchronously in its own listener.
1271
+ target.dispatchEvent(new CustomEvent('toolbar-columns-set', {
1272
+ bubbles: true,
1273
+ detail: { columns: next },
1274
+ }));
1231
1275
  this.dispatchEvent(new CustomEvent('columns-change', {
1232
1276
  bubbles: true,
1233
1277
  detail: { hiddenColumns: next.filter((c) => c.hidden).map((c) => c.key) },
@@ -163,6 +163,40 @@
163
163
  width: var(--button-height);
164
164
  }
165
165
 
166
+ /* ═══════ Actions-leading icon-only reduction ═══════ (gh#1748) — extends
167
+ the SAME icon-only collapse above to a plain <button-ui> a consumer
168
+ slots into [slot="actions-leading"] (gh#1649, e.g. the app-owned
169
+ Filter/Columns triggers on the ratified adiav2 row), so it reads as
170
+ part of the same toolbar affordance cluster rather than staying
171
+ full-width while its native siblings compact. A separate rule block
172
+ (rather than folding into [data-toolbar-btn]'s own selector list above)
173
+ keeps that block's literal text byte-identical for its own
174
+ source-assertion tests. Same breakpoint/pin pair, same
175
+ aria-label-survives guarantee (button-ui's own REQ-X-001 stamp,
176
+ unaffected here) — light-DOM real insertion point (no shadow
177
+ boundary), so a plain descendant tag selector reaches it directly, no
178
+ `::slotted()` needed. Literal `40rem` bound to
179
+ --table-toolbar-bp-icon-only's default, same as the native block. */
180
+ @container table-toolbar (max-width: 40rem) {
181
+ :scope:is(:not([stage]), [stage=""]) [data-actions-leading] button-ui[text]::after {
182
+ content: none;
183
+ }
184
+ :scope:is(:not([stage]), [stage=""]) [data-actions-leading] button-ui {
185
+ --button-px: 0;
186
+
187
+ width: var(--button-height);
188
+ }
189
+ }
190
+
191
+ :scope:is([stage="icon-only"], [stage="overflow"]) [data-actions-leading] button-ui[text]::after {
192
+ content: none;
193
+ }
194
+ :scope:is([stage="icon-only"], [stage="overflow"]) [data-actions-leading] button-ui {
195
+ --button-px: 0;
196
+
197
+ width: var(--button-height);
198
+ }
199
+
166
200
  /* ═══════ Range-summary conditional compact/hide ═══════ (ADR-0076
167
201
  REQ-S-003, step 4) — at the SAME icon-only breakpoint as above:
168
202
  - [data-page-size] present AND visible (no [hidden]) → its own
@@ -17,6 +17,11 @@ export type TableToolbarFilterChangeEvent = CustomEvent<unknown>;
17
17
  export type TableToolbarPageSizeChangeEvent = CustomEvent<unknown>;
18
18
  export type TableToolbarSearchEvent = CustomEvent<unknown>;
19
19
  export type TableToolbarSortChangeEvent = CustomEvent<unknown>;
20
+ export type TableToolbarToolbarColumnsSetEvent = CustomEvent<unknown>;
21
+ export type TableToolbarToolbarFilterClearEvent = CustomEvent<unknown>;
22
+ export type TableToolbarToolbarFilterSetEvent = CustomEvent<unknown>;
23
+ export type TableToolbarToolbarPaginateEvent = CustomEvent<unknown>;
24
+ export type TableToolbarToolbarSearchEvent = CustomEvent<unknown>;
20
25
 
21
26
  export class UITableToolbar extends UIElement {
22
27
  /** Suppress all four native controls/search at once (filter, sort, columns, search) as additive sugar over noFilter/noSort/noColumns/ noSearch (ADR-0076, ADIA2-9123). Precedence is pure, absolute OR — while set, all four stay off regardless of any individual no-* attribute's own value, with no partial re-enable path; to re-enable one control, remove chrome-only entirely and set the other three no-* attributes explicitly instead. The four granular attributes are not deprecated or removed — they remain the independently-addressable shipped API; chrome-only never replaces them, it's a convenience preset on top. */
@@ -57,6 +62,11 @@ export class UITableToolbar extends UIElement {
57
62
  addEventListener(type: 'page-size-change', listener: (ev: TableToolbarPageSizeChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
58
63
  addEventListener(type: 'search', listener: (ev: TableToolbarSearchEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
59
64
  addEventListener(type: 'sort-change', listener: (ev: TableToolbarSortChangeEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
65
+ addEventListener(type: 'toolbar-columns-set', listener: (ev: TableToolbarToolbarColumnsSetEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
66
+ addEventListener(type: 'toolbar-filter-clear', listener: (ev: TableToolbarToolbarFilterClearEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
67
+ addEventListener(type: 'toolbar-filter-set', listener: (ev: TableToolbarToolbarFilterSetEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
68
+ addEventListener(type: 'toolbar-paginate', listener: (ev: TableToolbarToolbarPaginateEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
69
+ addEventListener(type: 'toolbar-search', listener: (ev: TableToolbarToolbarSearchEvent) => unknown, options?: boolean | AddEventListenerOptions): void;
60
70
  addEventListener<K extends keyof HTMLElementEventMap>(
61
71
  type: K,
62
72
  listener: (this: UITableToolbar, ev: HTMLElementEventMap[K]) => unknown,
@@ -199,6 +199,33 @@ events:
199
199
  description: "Column visibility changed. Detail: { hiddenColumns }."
200
200
  page-size-change:
201
201
  description: "Page-size select changed. Detail: { pageSize }."
202
+ toolbar-search:
203
+ description: >-
204
+ gh#1764/#1780, ADR-0079 (events-only interaction contract) — dispatched
205
+ directly at the resolved [for] target (not bubbled from this element)
206
+ in place of the pre-#1780 direct `.search =` write. table-ui listens
207
+ for this on itself. Detail: { value }.
208
+ toolbar-filter-set:
209
+ description: >-
210
+ gh#1764/#1780, ADR-0079 — dispatched directly at the resolved [for]
211
+ target in place of the pre-#1780 direct `target.setFilter()` call.
212
+ table-ui listens for this on itself. Detail: { key, value, op }; a
213
+ null `value` clears that one column's filter.
214
+ toolbar-filter-clear:
215
+ description: >-
216
+ gh#1764/#1780, ADR-0079 — dispatched directly at the resolved [for]
217
+ target in place of the pre-#1780 direct `target.clearFilters()` call.
218
+ table-ui listens for this on itself. No detail.
219
+ toolbar-columns-set:
220
+ description: >-
221
+ gh#1764/#1780, ADR-0079 — dispatched directly at the resolved [for]
222
+ target in place of the pre-#1780 direct `target.columns =` write.
223
+ table-ui listens for this on itself. Detail: { columns }.
224
+ toolbar-paginate:
225
+ description: >-
226
+ gh#1764/#1780, ADR-0079 — dispatched directly at the resolved [for]
227
+ target in place of the pre-#1780 direct `target.paginate =` write.
228
+ table-ui listens for this on itself. Detail: { pageSize }.
202
229
  slots:
203
230
  scope:
204
231
  description: Leading region rendered BEFORE the [text]/[count] title cluster — e.g. a scope/view-switcher menu (an org's teams, a saved view). Positioning is CSS by DOM order, mirroring how [slot="actions"] is a real, author-fillable insertion point rather than template-stamped content.
@@ -322,15 +349,20 @@ a2ui:
322
349
  EXPLICIT range-total="0" — omitting range-* entirely (loading) never
323
350
  shows it.
324
351
  - >-
325
- Read the host's [data-stage-resolved] attribute (ADR-0076 REQ-M-005,
326
- ADIA2-9123 S4-ii) to compact a consumer's own [slot="actions-leading"]
327
- content in sympathy with table-toolbar's native regions, without
328
- re-deriving the same breakpoints yourself. Informational only
329
- mirrors the CSS-decided compaction stage (full | search-tight |
330
- icon-only | overflow), never drives table-toolbar's own rendering.
331
- Reports the pinned [stage] value when set, the live width
332
- classification otherwise; updates on both a live resize and a
333
- [stage] attribute change.
352
+ A plain <button-ui> slotted into [slot="actions-leading"] (e.g. the
353
+ app-owned Filter/Columns triggers above) already collapses to
354
+ icon-only at the SAME breakpoint as the native Filter/Sort/Columns
355
+ buttons (gh#1748) no consumer JS/CSS required. Read the host's
356
+ [data-stage-resolved] attribute (ADR-0076 REQ-M-005, ADIA2-9123
357
+ S4-ii) only for compaction BEYOND plain icon-only reduction — e.g.
358
+ hiding a slotted button entirely at the overflow stage, or reacting
359
+ to the stage in a consumer's own drawer/menu — without re-deriving
360
+ the same breakpoints yourself. Informational only — mirrors the
361
+ CSS-decided compaction stage (full | search-tight | icon-only |
362
+ overflow), never drives table-toolbar's own rendering. Reports the
363
+ pinned [stage] value when set, the live width classification
364
+ otherwise; updates on both a live resize and a [stage] attribute
365
+ change.
334
366
  anti_patterns: []
335
367
  examples:
336
368
  - name: members-toolbar
@@ -37,6 +37,14 @@
37
37
  * stream-load — first signal value received for this element
38
38
  * stream-update — each subsequent value, detail.data = the new value
39
39
  * stream-error — transport-level error, detail.error = message
40
+ *
41
+ * Opting out (gh#1760):
42
+ * `data-stream-managed="false"` tells the document-level observer to
43
+ * never claim this element, even though it carries `data-stream-src`.
44
+ * For elements that own their own imperative fetch (e.g. the billing
45
+ * composites' `refresh()`) and must not also be claimed by this module —
46
+ * never mix an imperative fetch with an unmanaged `data-stream-src` on
47
+ * the same element (data-stream-protocol.md §10.1).
40
48
  */
41
49
 
42
50
  import { signal, effect, untracked } from './signals.js';
@@ -54,6 +62,11 @@ const ATTRS = {
54
62
  merge: 'data-stream-merge',
55
63
  format: 'data-stream-format',
56
64
  id: 'data-stream-id',
65
+ // gh#1760 — opt-out for elements that carry `data-stream-src` but manage
66
+ // their own fetch (e.g. the billing composites' hand-rolled `refresh()`).
67
+ // `"false"` tells the document-level observer to never claim this element,
68
+ // so exactly one owner (the element's own imperative fetch) ever runs.
69
+ managed: 'data-stream-managed',
57
70
  };
58
71
 
59
72
  const STREAMS = new Map(); /* streamId → { signal, refs, transport, opts } */
@@ -386,6 +399,7 @@ function applyData(el, raw, opts) {
386
399
  export function start(el) {
387
400
  stop(el);
388
401
  if (!el.isConnected) return;
402
+ if (attr(el, 'managed') === 'false') return; // gh#1760 — self-managed opt-out
389
403
  const src = attr(el, 'src');
390
404
  if (!src) return;
391
405
 
@@ -442,7 +456,8 @@ export function stop(el) {
442
456
  const ATTR_FILTER = Object.values(ATTRS);
443
457
 
444
458
  function isStreamingEl(node) {
445
- return node && node.nodeType === 1 && node.hasAttribute && node.hasAttribute(ATTRS.src);
459
+ return !!(node && node.nodeType === 1 && node.hasAttribute && node.hasAttribute(ATTRS.src)
460
+ && node.getAttribute(ATTRS.managed) !== 'false');
446
461
  }
447
462
 
448
463
  function visitSubtree(root, fn) {
@@ -476,11 +491,31 @@ const observer = typeof MutationObserver !== 'undefined'
476
491
 
477
492
  function bootstrap() {
478
493
  if (typeof document === 'undefined') return;
494
+ // Register the observer synchronously — no live mutation is missed.
479
495
  observer?.observe(document.documentElement, {
480
496
  childList: true, subtree: true,
481
497
  attributes: true, attributeFilter: ATTR_FILTER,
482
498
  });
483
- document.querySelectorAll(`[${ATTRS.src}]`).forEach(start);
499
+ // gh#1760 — defer the INITIAL sweep of already-present markup by one
500
+ // microtask. Without this, a page whose entry module imports the core
501
+ // barrel (this module) BEFORE a self-managed composite's own module
502
+ // (which sets `data-stream-managed="false"` synchronously from its
503
+ // upgrade-triggered `connectedCallback`) races: this sweep would claim
504
+ // pre-existing SSR markup before the composite ever gets a chance to
505
+ // opt out. Deferring to a microtask lets the rest of the SAME
506
+ // synchronous module-evaluation phase — including every sibling
507
+ // module's own top-level side effects — finish first, so by the time
508
+ // this sweep runs, every already-loaded composite has already set its
509
+ // marker. (A composite loaded asynchronously — a dynamic `import()` —
510
+ // after this microtask has already fired is not covered by this
511
+ // deferral alone; its own `connected()` marker-set plus the observer's
512
+ // live attribute-mutation handling still tears down a wrongly-started
513
+ // stream, bounding the damage to at most one extra fetch.)
514
+ queueMicrotask(() => {
515
+ document.querySelectorAll(`[${ATTRS.src}]`).forEach((el) => {
516
+ if (isStreamingEl(el)) start(el);
517
+ });
518
+ });
484
519
  }
485
520
 
486
521
  if (typeof document !== 'undefined') {
package/core/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export * from './template.js';
9
9
  export * from './element.js';
10
10
  export * from './form.js';
11
11
  export * from './register.js';
12
+ export * from './store.js';
12
13
  // Mirror the explicit re-export in core/index.js — `streams` + `whenStream`
13
14
  // (the public surface of the data-stream attribute-driven ingestion module).
14
15
  // `export *` from data-stream.js would also expose internals; keep the
package/core/index.js CHANGED
@@ -18,6 +18,7 @@ export * from './signals.js';
18
18
  export * from './template.js';
19
19
  export * from './register.js';
20
20
  export * from './controller.js';
21
+ export * from './store.js';
21
22
  export * from './provider.js';
22
23
  export * from './anchor.js';
23
24
  export * from './icons.js';
@@ -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>;