@adukiorg/anza 0.2.0 → 0.2.3

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 (83) hide show
  1. package/CHANGELOG.md +90 -4
  2. package/README.md +97 -133
  3. package/bin/anza/anza-linux-arm64 +0 -0
  4. package/bin/anza/anza-linux-x64 +0 -0
  5. package/bin/anza/anza-macos-arm64 +0 -0
  6. package/bin/anza/anza-macos-x64 +0 -0
  7. package/bin/anza/anza-windows-x64.exe +0 -0
  8. package/bin/anza/find.js +35 -0
  9. package/bin/anza/index.js +34 -0
  10. package/bin/anza/launch.js +19 -0
  11. package/bin/common/index.js +7 -0
  12. package/bin/common/logs.js +62 -0
  13. package/bin/create/copy.js +18 -0
  14. package/bin/create/index.js +45 -0
  15. package/bin/create/run.js +210 -0
  16. package/bin/create/write.js +19 -0
  17. package/importmap.json +4 -0
  18. package/package.json +16 -10
  19. package/src/core/offline/{usage.md → notes/usage.md} +11 -1
  20. package/src/core/router/boot.js +82 -0
  21. package/src/core/router/cascade.js +76 -0
  22. package/src/core/router/container.js +63 -72
  23. package/src/core/router/graph.js +144 -0
  24. package/src/core/router/index.js +12 -2
  25. package/src/core/router/intercept.js +26 -7
  26. package/src/core/router/lca.js +58 -0
  27. package/src/core/router/match.js +49 -36
  28. package/src/core/router/notes/audit-old.md +887 -0
  29. package/src/core/router/notes/audti.md +773 -0
  30. package/src/core/router/notes/tasks.md +473 -0
  31. package/src/core/router/{usage.md → notes/usage.md} +57 -35
  32. package/src/core/router/sync/tab.js +6 -4
  33. package/src/core/router/transitions.js +35 -8
  34. package/src/core/router/trie.js +130 -0
  35. package/src/core/security/{usage.md → notes/usage.md} +1 -2
  36. package/src/core/storage/{usage.md → notes/usage.md} +6 -6
  37. package/src/core/theme/index.js +78 -0
  38. package/src/core/ui/define/index.js +2 -1
  39. package/src/core/ui/define/orchestrator.js +10 -4
  40. package/src/core/ui/defs/dock.js +134 -0
  41. package/src/core/ui/defs/index.js +20 -0
  42. package/src/core/ui/defs/page.js +89 -0
  43. package/src/core/ui/defs/part.js +28 -0
  44. package/src/core/ui/defs/spec.js +96 -0
  45. package/src/core/ui/defs/view.js +23 -0
  46. package/src/core/ui/index.js +16 -3
  47. package/src/core/ui/notes/definations.md +979 -0
  48. package/src/tokens/index.css +1 -0
  49. package/src/tokens/semantic/contrast.css +18 -0
  50. package/src/tokens/semantic/transitions.css +32 -0
  51. package/types/core/platform/index.d.ts +39 -10
  52. package/types/core/router/index.d.ts +9 -0
  53. package/types/core/theme/index.d.ts +18 -0
  54. package/types/core/ui/index.d.ts +11 -0
  55. package/types/index.d.ts +1 -0
  56. package/bin/anza.js +0 -63
  57. package/bin/create.js +0 -150
  58. package/src/core/api/plan.md +0 -209
  59. package/src/core/events/missing.md +0 -103
  60. package/src/core/events/plan.md +0 -177
  61. package/src/core/offline/missing.md +0 -89
  62. package/src/core/offline/plan.md +0 -143
  63. package/src/core/platform/missing.md +0 -119
  64. package/src/core/platform/platform.d.ts +0 -88
  65. package/src/core/router/missing.md +0 -716
  66. package/src/core/router/outlet.js +0 -139
  67. package/src/core/router/plan.md +0 -370
  68. package/src/core/security/missing.md +0 -97
  69. package/src/core/state/missing.md +0 -165
  70. package/src/core/storage/missing.md +0 -165
  71. package/src/core/storage/plan.md +0 -69
  72. package/src/core/ui/implementation.md +0 -170
  73. package/src/core/ui/plan.md +0 -510
  74. package/src/core/ui/ui.types.md +0 -890
  75. /package/src/core/animations/{usage.md → notes/usage.md} +0 -0
  76. /package/src/core/api/{usage.md → notes/usage.md} +0 -0
  77. /package/src/core/events/{usage.md → notes/usage.md} +0 -0
  78. /package/src/core/platform/{usage.md → notes/usage.md} +0 -0
  79. /package/src/core/state/{usage.md → notes/usage.md} +0 -0
  80. /package/src/core/ui/{usage.md → notes/usage.md} +0 -0
  81. /package/src/core/ui/{watch.md → notes/watch.md} +0 -0
  82. /package/src/core/workers/{plan.md → notes/plan.md} +0 -0
  83. /package/src/core/workers/{usage.md → notes/usage.md} +0 -0
@@ -0,0 +1,473 @@
1
+ # Implementation Tasks
2
+
3
+ Comprehensive task list derived from `myaudit.md` and `../ui/definations.md`. Covers the router overhaul, the new definition layer (`page`/`dock`/`view`/`part`), and the Rust toolchain changes required to support them.
4
+
5
+ Companion documents:
6
+
7
+ - `myaudit.md` — architecture audit, proposed fixes, data structures, edge-case bugs
8
+ - `../ui/definations.md` — unified `page`/`dock`/`view`/`part` API specification
9
+
10
+ ---
11
+
12
+ ## Phase 1 — Boot Gate
13
+
14
+ **Goal.** Survive hard refresh. Defer the router's initial `'found'` emit until routes are registered and containers are mounted.
15
+
16
+ ### 1.1 Create `router/boot.js`
17
+
18
+ New module. Exports `gate(promise)`, `boot(emitFn)`, `ready()`.
19
+
20
+ - `gate` accepts a `Promise` that must resolve before the router emits its first match.
21
+ - `boot` waits for `DOMContentLoaded` (if `document.readyState === 'loading'`), then `Promise.all(gates)`, then calls `emitFn`.
22
+ - `ready` returns `true` after boot completes.
23
+
24
+ **APIs used.** `document.readyState`, `DOMContentLoaded` event, `Promise.all`.
25
+
26
+ ### 1.2 Update `router/intercept.js`
27
+
28
+ Replace the `Promise.resolve().then(...)` initial-emit block inside `setup()` with a call to `boot()`.
29
+
30
+ Remove the current microtask:
31
+
32
+ ```js
33
+ // DELETE
34
+ Promise.resolve().then(async () => { ... });
35
+ ```
36
+
37
+ Replace with:
38
+
39
+ ```js
40
+ import { boot, ready } from './boot.js';
41
+
42
+ // inside setup(), after attaching listeners:
43
+ boot(async () => {
44
+ const url = window.navigation.currentEntry?.url || location.href;
45
+ const found = await match(url);
46
+ if (found) emit('found', { ...found, direction: 'load' });
47
+ else emit('notfound', { url });
48
+ });
49
+ ```
50
+
51
+ ### 1.3 Wire boot gate into element registration
52
+
53
+ In the new `defs/page.js` and `defs/dock.js` (Phase 6), after `customElements.define`, call:
54
+
55
+ ```js
56
+ import { gate } from '../router/boot.js';
57
+ gate(customElements.whenDefined(tag));
58
+ ```
59
+
60
+ Until Phase 6 exists, wire it into the existing `ui/define/element.js` after `customElements.define(tag, DeclarativeElement)`:
61
+
62
+ ```js
63
+ if (spec.url) {
64
+ gate(customElements.whenDefined(tag));
65
+ }
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Phase 2 — Window Bridge
71
+
72
+ **Goal.** Attach the router to `window` so non-module scripts and devtools can access it.
73
+
74
+ ### 2.1 Update `router/index.js`
75
+
76
+ In the auto-bootstrap block, add before `setup()`:
77
+
78
+ ```js
79
+ Object.defineProperty(window, 'router', {
80
+ value: router,
81
+ writable: false,
82
+ enumerable: false,
83
+ configurable: false
84
+ });
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Phase 3 — Container Graph
90
+
91
+ **Goal.** Replace the flat `Map<string, WeakRef>` with a hierarchical tree that enables LCA traversal and cascade mounting.
92
+
93
+ ### 3.1 Create `router/graph.js`
94
+
95
+ New module. Data structure:
96
+
97
+ ```
98
+ Node { name, ref: WeakRef, parent: Node|null, children: Set<Node>, depth: number }
99
+ ```
100
+
101
+ Exports:
102
+
103
+ - `add(name, element, parent = 'body')` — insert a node, update parent's children set
104
+ - `remove(name)` — delete a node, reparent orphans to grandparent
105
+ - `get(name)` — return `Node` or `null`
106
+ - `element(name)` — return `HTMLElement` via `WeakRef.deref()` or `null`
107
+ - `clear()` — reset the tree to root-only
108
+
109
+ Virtual root node `'body'` is created at module load. `FinalizationRegistry` prunes stale nodes.
110
+
111
+ ### 3.2 Update `router/container.js`
112
+
113
+ Rewrite to delegate to `graph.js`. Keep the same public API signatures for backward compatibility:
114
+
115
+ - `registerContainer(name, element, parent)` → calls `graph.add`
116
+ - `unregisterContainer(name, element)` → calls `graph.remove`
117
+ - `getContainer(name)` → calls `graph.element`
118
+ - `clearContainers()` → calls `graph.clear`
119
+
120
+ Remove the `MutationObserver` logic (docks self-register in `connectedCallback`; no polling needed).
121
+
122
+ ### 3.3 Add `parent` parameter to container registration
123
+
124
+ `registerContainer` gains an optional third argument `parent`. Default: `'body'`. Passed through to `graph.add`.
125
+
126
+ ---
127
+
128
+ ## Phase 4 — LCA Algorithm
129
+
130
+ **Goal.** Enable cross-branch navigation by computing the lowest common ancestor between two containers.
131
+
132
+ ### 4.1 Create `router/lca.js`
133
+
134
+ Exports:
135
+
136
+ - `lca(a, b)` — depth-equalisation + tandem walk, returns `Node` or `null`
137
+ - `path(from, to)` — returns `[Node, ...]` from ancestor to target
138
+
139
+ O(d) complexity where d = tree depth.
140
+
141
+ ---
142
+
143
+ ## Phase 5 — Cascade Mount
144
+
145
+ **Goal.** When a target container is absent, sequentially mount intermediate containers from the nearest live ancestor down to the target.
146
+
147
+ ### 5.1 Create `router/cascade.js`
148
+
149
+ Exports:
150
+
151
+ - `ensure(target, current)` — walk `path()`, find deepest mounted node, sequentially `createElement` + `swap`/`replaceChildren` + `rAF` yield for each missing level.
152
+
153
+ Uses `customElements.whenDefined`, `document.createElement`, `requestAnimationFrame`.
154
+
155
+ ### 5.2 Update `router/intercept.js` handler
156
+
157
+ Replace the hard `throw` on missing container with cascade logic:
158
+
159
+ ```js
160
+ const chain = meta.via ?? (meta.container ? [meta.container] : []);
161
+ const target = chain.at(-1);
162
+
163
+ for (let i = 0; i < chain.length; i++) {
164
+ if (!element(chain[i])) {
165
+ await ensure(chain[i], chain[i - 1] ?? 'body');
166
+ }
167
+ }
168
+ ```
169
+
170
+ ---
171
+
172
+ ## Phase 6 — Definition Layer (`defs/`)
173
+
174
+ **Goal.** Implement the `page`, `dock`, `view`, `part` definition functions from `definations.md`. These supersede the split `ui.element` + `router.register` pattern.
175
+
176
+ ### 6.1 Create `ui/defs/page.js`
177
+
178
+ `page(route, config, base)` — route-bound element.
179
+
180
+ Internal steps:
181
+
182
+ 1. Normalize `config.template` — detect file paths (strings ending in `.html`/`.css` or starting with `./`) vs inline strings.
183
+ 2. If file paths: resolve against `base` (the caller's `import.meta.url`).
184
+ 3. Define an `HTMLElement` subclass. Attach shadow DOM, preload resources via `preloadResources()`.
185
+ 4. Install `on.load`/`on.unload`/`on.connect`/`on.disconnect` lifecycle hooks.
186
+ 5. Install `config.props` as reactive observed attributes/properties.
187
+ 6. Call `customElements.define(config.tag, cls)`.
188
+ 7. Call `router.register(route, config.tag, { via: config.via, props: config.props, query: config.query })`.
189
+ 8. Register spec in `specRegistry`.
190
+ 9. Call `gate(customElements.whenDefined(config.tag))`.
191
+ 10. If `config.guard` is defined, wrap it as a route-scoped guard on `router.guard`.
192
+
193
+ ### 6.2 Create `ui/defs/dock.js`
194
+
195
+ `dock(name, config)` — container element.
196
+
197
+ Internal steps:
198
+
199
+ 1. Derive tag from `config.tag ?? 'dock-' + name`.
200
+ 2. Define an `HTMLElement` subclass. If template omitted, use passthrough `<slot></slot>`.
201
+ 3. In `connectedCallback`: call `graph.add(name, this, config.parent ?? 'body')`.
202
+ 4. In `disconnectedCallback`: call `graph.remove(name)`.
203
+ 5. Attach `swap` method — either `config.on.swap` override or the default view-transition implementation.
204
+ 6. Call `customElements.define(tag, cls)`.
205
+ 7. Call `gate(customElements.whenDefined(tag))`.
206
+
207
+ ### 6.3 Create `ui/defs/view.js`
208
+
209
+ `view(tag, config, base)` — composable stateful component. No route, no graph.
210
+
211
+ Same element-definition logic as `page` minus routing and container registration. Supports `props`, `template`, `on.connect`/`on.disconnect`/`on.change`, `methods`.
212
+
213
+ ### 6.4 Create `ui/defs/part.js`
214
+
215
+ `part(tag, config, base)` — atomic stateless primitive.
216
+
217
+ Minimal subset of `view`. No reactive updates, no `on.change`. Constructor-time attribute reads, static shadow DOM.
218
+
219
+ ### 6.5 Create `ui/defs/index.js`
220
+
221
+ Facade exporting `{ page, dock, view, part }`.
222
+
223
+ ### 6.6 Update `ui/define/orchestrator.js`
224
+
225
+ Update the `'found'` listener to understand `via` chains (from `meta.via`) instead of a single `container` string. Use the cascade logic from Phase 5.
226
+
227
+ ### 6.7 Deprecate (do not remove) existing APIs
228
+
229
+ `ui.element` and `ui.container` remain functional. Add a console info message on first use suggesting migration to `page`/`dock`/`view`/`part`. Do not break existing code.
230
+
231
+ ---
232
+
233
+ ## Phase 7 — Trie Route Matcher
234
+
235
+ **Goal.** O(k) route matching where k = URL segment count, replacing O(n) linear scan.
236
+
237
+ ### 7.1 Create `router/trie.js`
238
+
239
+ `Segment` class with `static: Map`, `param: Segment|null`, `wild: Segment|null`, `route`, `key`.
240
+
241
+ Exports:
242
+
243
+ - `insert(pattern, route)` — decompose pattern into segments, walk/create trie nodes
244
+ - `find(pathname)` — walk trie with backtracking, return `{ route, params }` or `null`
245
+
246
+ Priority order: static > param > wildcard (matching the URLPattern specificity model).
247
+
248
+ ### 7.2 Integrate into `router/match.js`
249
+
250
+ On `register()`, insert into the trie alongside the existing `routes[]` array. On `match()`, try trie first; fall back to URLPattern linear scan for patterns containing regex groups or modifiers the trie cannot express.
251
+
252
+ ---
253
+
254
+ ## Phase 8 — Edge-Case Bug Fixes
255
+
256
+ ### 8.1 MutationObserver starvation (`container.js`)
257
+
258
+ Add `{ timeout: 100 }` to `requestIdleCallback`. Fall back to `requestAnimationFrame` when `requestIdleCallback` is unavailable.
259
+
260
+ ### 8.2 WeakRef timing hole (`graph.js`)
261
+
262
+ Track explicit unregisters in a `Set<string>`. Return `undefined` from `element()` if the name is in the set. Clear the entry after one macrotask via `setTimeout(..., 0)`.
263
+
264
+ ### 8.3 Cycle detection (`match.js`)
265
+
266
+ Add a `visited` Set to the parent-chain walker. Break on cycle.
267
+
268
+ ### 8.4 Concurrent navigation race (`defs/dock.js`)
269
+
270
+ In the default `swap` method, call `this._tx?.skipTransition()` before starting a new view transition. Clear `this._tx` on completion.
271
+
272
+ ### 8.5 Tab sync redirect amplification (`sync/tab.js`)
273
+
274
+ Delay `isSyncing = false` by one macrotask using `setTimeout(() => { isSyncing = false }, 0)` to cover guard-triggered redirects.
275
+
276
+ ---
277
+
278
+ ## Phase 9 — Rust Toolchain Changes
279
+
280
+ **Goal.** The `anza` CLI must understand the new `page`/`dock`/`view`/`part` definition functions and the native `.html`/`.css` file convention.
281
+
282
+ ### 9.1 Update `ExtractedSpec` struct (`types/runner.rs`)
283
+
284
+ Add new fields to `ExtractedSpec`:
285
+
286
+ ```rust
287
+ pub struct ExtractedSpec {
288
+ pub tag: String,
289
+ pub kind: String, // "page" | "dock" | "view" | "part" | "element" | "container"
290
+ pub props: HashMap<String, PropConfig>,
291
+ pub methods: Vec<String>,
292
+ pub url: Option<String>, // page only
293
+ pub container: Option<String>, // backward compat
294
+ pub via: Vec<String>, // page: ordered dock chain
295
+ pub parent: Option<String>, // dock: parent dock name
296
+ pub meta: HashMap<String, String>,
297
+ }
298
+ ```
299
+
300
+ ### 9.2 Update element extraction (`extract/runner.rs`)
301
+
302
+ The `ElementVisitor` currently only recognises `ui.element(...)` and `ui.container(...)`. Expand to also match:
303
+
304
+ - `page(route, config)` — top-level function call, not `ui.page`
305
+ - `dock(name, config)` — top-level function call
306
+ - `view(tag, config)` — top-level function call
307
+ - `part(tag, config)` — top-level function call
308
+
309
+ For each, extract:
310
+
311
+ | Call | `tag` source | `url` source | `via` source | `parent` source | `kind` |
312
+ |------|-------------|-------------|-------------|----------------|--------|
313
+ | `page(route, { tag, ... })` | `config.tag` | first argument (`route`) | `config.via` | — | `"page"` |
314
+ | `dock(name, { tag, ... })` | `config.tag` or `'dock-' + name` | — | — | `config.parent` | `"dock"` |
315
+ | `view(tag, config)` | first argument | — | — | — | `"view"` |
316
+ | `part(tag, config)` | first argument | — | — | — | `"part"` |
317
+
318
+ Update `parse_spec_object` to also extract:
319
+
320
+ - `via` — array of string literals
321
+ - `parent` — string literal
322
+ - `template.html` and `template.css` — when `template` is an object with `html`/`css` string properties that are file paths
323
+
324
+ ### 9.3 Update asset resolution in build graph (`build/graph.rs`)
325
+
326
+ The `Collector::scan_spec` method currently looks for top-level `template` and `style` string literals:
327
+
328
+ ```rust
329
+ "template" | "style" => {
330
+ if let Expr::Lit(Lit::Str(s)) = &*kv.value { ... }
331
+ }
332
+ ```
333
+
334
+ Add handling for when `template` is an **object** with `html` and `css` properties:
335
+
336
+ ```rust
337
+ "template" => {
338
+ match &*kv.value {
339
+ // Existing: template: './file.html'
340
+ Expr::Lit(Lit::Str(s)) => { /* existing logic */ }
341
+ // New: template: { html: './template.html', css: './style.css' }
342
+ Expr::Object(obj) => {
343
+ for prop in &obj.props {
344
+ if let PropOrSpread::Prop(p) = prop {
345
+ if let Prop::KeyValue(inner_kv) = &**p {
346
+ if let PropName::Ident(inner_key) = &inner_kv.key {
347
+ if (inner_key.sym == "html" || inner_key.sym == "css") {
348
+ if let Expr::Lit(Lit::Str(s)) = &*inner_kv.value {
349
+ if let Some(v) = str_value(s) {
350
+ if v.starts_with("./") || v.starts_with("../") || v.starts_with('/') {
351
+ self.assets.push((v, s.span));
352
+ }
353
+ }
354
+ }
355
+ }
356
+ }
357
+ }
358
+ }
359
+ }
360
+ }
361
+ _ => {}
362
+ }
363
+ }
364
+ ```
365
+
366
+ Also recognise the new top-level call patterns (`page(...)`, `dock(...)`, `view(...)`, `part(...)`) so that their `template` objects are scanned for asset paths. Currently only `ui.element(...)` and `ui.container(...)` trigger `scan_spec`.
367
+
368
+ ### 9.4 Update route manifest (`extract/routes.rs`)
369
+
370
+ Extend `RouteRecord` to include `via`:
371
+
372
+ ```rust
373
+ struct RouteRecord<'a> {
374
+ tag: &'a str,
375
+ path: &'a str,
376
+ #[serde(skip_serializing_if = "Option::is_none")]
377
+ container: Option<&'a str>,
378
+ #[serde(skip_serializing_if = "Vec::is_empty")]
379
+ via: &'a Vec<String>,
380
+ params: Vec<String>,
381
+ }
382
+ ```
383
+
384
+ Update `routes.json` output to include the `via` array for pages. Update `routes.d.ts` `RouteMap` interface to type-check `via`.
385
+
386
+ ### 9.5 Update type generation
387
+
388
+ Generate interfaces for all four definition types, not just `ui.element`. The `kind` field in `ExtractedSpec` distinguishes them. For docks, generate a `swap` method in the interface:
389
+
390
+ ```typescript
391
+ export interface DockSidebarElement extends HTMLElement {
392
+ swap(el: HTMLElement, options?: { direction?: string }): Promise<void>;
393
+ }
394
+ ```
395
+
396
+ ### 9.6 Update `doctor` command (`main.rs`)
397
+
398
+ Add checks for:
399
+
400
+ - `src/docks/` directory (dock definitions)
401
+ - `src/pages/` directory (page definitions)
402
+ - `src/views/` directory (view definitions)
403
+ - `src/parts/` directory (part definitions)
404
+
405
+ Warn (don't error) if `src/elements/` exists alongside `src/pages/` — suggest migration.
406
+
407
+ ### 9.7 Update watcher HMR classification (`watcher/runner.rs`)
408
+
409
+ `.html` and `.css` changes inside component folders should trigger the correct HMR message kind (`Html` or `Css`). The existing classification already handles this by extension, but verify that changes to `template.html` and `style.css` inside `pages/profile/` correctly propagate.
410
+
411
+ ---
412
+
413
+ ## Phase 10 — Cleanup & Migration
414
+
415
+ ### 10.1 Update `<route-outlet>` (`router/outlet.js`)
416
+
417
+ `<route-outlet>` remains functional but is deprecated. Add a console info suggesting migration to `<dock-*>` elements on first `connectedCallback`.
418
+
419
+ ### 10.2 Update `ui/define/index.js`
420
+
421
+ Export `page`, `dock`, `view`, `part` alongside `define`, `element`, `container`.
422
+
423
+ ### 10.3 Update package exports (`library/package.json`)
424
+
425
+ Add `./defs` export path mapping to `src/core/ui/defs/index.js`.
426
+
427
+ ### 10.4 Update documentation
428
+
429
+ - `router/usage.md` — add sections for `page`, `dock`, `via` chains, cascade mounting
430
+ - `ui/usage.md` — add `page`/`dock`/`view`/`part` API documentation
431
+ - `router/plan.md` — mark completed sections, add new architecture diagrams
432
+
433
+ ---
434
+
435
+ ## Dependency Order
436
+
437
+ ```text
438
+ Phase 1 (boot.js)
439
+ └─ Phase 2 (window bridge) — independent
440
+ └─ Phase 3 (graph.js)
441
+ └─ Phase 4 (lca.js)
442
+ └─ Phase 5 (cascade.js)
443
+ └─ Phase 6 (defs/)
444
+ └─ Phase 9 (Rust toolchain)
445
+ └─ Phase 10 (cleanup)
446
+ └─ Phase 7 (trie.js) — independent
447
+ └─ Phase 8 (bug fixes) — independent
448
+ ```
449
+
450
+ Phases 1, 2, 7, and 8 can run in parallel. Phases 3-6 are sequential. Phase 9 (Rust) can start after Phase 6 API is finalized. Phase 10 runs last.
451
+
452
+ ---
453
+
454
+ ## Verification
455
+
456
+ Each phase must pass before proceeding:
457
+
458
+ | Phase | Verification |
459
+ | ----- | ------------ |
460
+ | 1 | Hard refresh on a registered route renders correctly. No console errors on cold boot. |
461
+ | 2 | `window.router` is available in devtools console. `window.router === router` is `true`. |
462
+ | 3 | `graph.add('a', el, 'body')` + `graph.get('a')` returns the node. `graph.element('a')` returns the element. |
463
+ | 4 | `lca('sidebar', 'settings')` returns the correct common ancestor. `path('body', 'settings')` returns the correct chain. |
464
+ | 5 | Navigate to a route whose dock chain has 3 levels, none mounted. All three mount sequentially. Page renders. |
465
+ | 6 | `page('/test', { tag: 'page-test', via: ['main'], template: { html: './t.html', css: './s.css' } }, import.meta.url)` defines element, registers route, gates boot. |
466
+ | 7 | 100 routes registered. `match('/route/99')` returns in < 0.1ms. |
467
+ | 8 | Tab sync with redirect guard does not loop. Rapid double-click navigation does not throw. |
468
+ | 9 | `anza scan` extracts specs from `page(...)` calls. `routes.json` includes `via` arrays. Build graph resolves `template.html` and `style.css` inside `template: {}` objects. |
469
+ | 10 | `<route-outlet>` still works. `ui.element` still works. No regressions. |
470
+
471
+ ---
472
+
473
+ *End of tasks.*
@@ -29,7 +29,7 @@ import {
29
29
  ## 1. Choosing an API
30
30
 
31
31
  | Need | Use |
32
- |---|---|
32
+ |------|-----|
33
33
  | Register a route | `router.register` |
34
34
  | Bulk register routes from JSON | `router.load` |
35
35
  | Match a URL manually | `router.match` |
@@ -114,7 +114,7 @@ A handler is exactly one of the following shapes. This single contract is shared
114
114
  by `match()` and the navigation interceptor, so a handler is never invoked twice:
115
115
 
116
116
  | Shape | Meaning |
117
- |---|---|
117
+ |-------|---------|
118
118
  | `'page-tag'` | static element tag |
119
119
  | `async () => 'page-tag'` | lazy tag factory (zero arity) |
120
120
  | `{ tag: 'page-tag' }` | static tag (object form) |
@@ -399,44 +399,65 @@ router.miss.clear();
399
399
 
400
400
  ## 10. Declarative UI Routes
401
401
 
402
- The UI layer can register routes automatically through `ui.element`.
402
+ Import `page` and `dock` from the definition layer and declare the route graph
403
+ explicitly.
403
404
 
404
405
  ```javascript
405
- import { ui } from '@adukiorg/anza/ui';
406
-
407
- ui.element('page-member', {
408
- url: '/members/:member',
409
- container: 'main',
410
-
411
- props: {
412
- member: { type: String, default: '' }
413
- },
406
+ import { page, dock } from '@adukiorg/anza/defs';
414
407
 
415
- template: './member.html',
416
- style: './member.css',
408
+ // A persistent container shell, registered in the graph under 'main'.
409
+ dock('main', { parent: 'body' });
417
410
 
418
- mount({ el }) {
419
- loadMember(el.member);
420
- },
421
-
422
- update({ el, name, val }) {
423
- if (name === 'member') {
424
- loadMember(val);
425
- }
411
+ // A route-bound page that renders through the 'main' → 'content' chain.
412
+ page('/members/:id', {
413
+ tag: 'page-member',
414
+ via: ['main', 'content'],
415
+ template: { html: './member.html', css: './member.css' },
416
+ props: { id: { type: Number } },
417
+ on: {
418
+ load({ params }) { return loadMember(params.id); }
426
419
  }
427
420
  }, import.meta.url);
428
421
  ```
429
422
 
430
423
  What happens:
431
424
 
432
- 1. `ui.element` calls `router.register(url, tag, meta)`.
425
+ 1. `page()` calls `router.register(url, tag, meta)`.
433
426
  2. The router matches the URL.
434
427
  3. The UI orchestrator listens for `found`.
435
- 4. The orchestrator finds the named container.
428
+ 4. The orchestrator finds the named container (from the `via` chain).
436
429
  5. The page element is created and params are assigned as properties.
437
430
  6. If the same page tag is already mounted, params are updated on the existing instance.
438
431
 
439
- The native toolchain also emits `routes.json` for declarative routes during `scan`, `build`, and `dev`.
432
+ ### Why `via` chains matter
433
+
434
+ A page declares the ordered root-to-leaf container chain it renders through.
435
+ Missing containers are mounted automatically by the cascade rather than
436
+ throwing. Docks register parent/child relationships in a hierarchical graph,
437
+ enabling lowest-common-ancestor traversal on cross-branch navigation.
438
+
439
+ ### Cascade mounting
440
+
441
+ When a navigation targets a container chain whose intermediate containers are
442
+ not yet mounted, the interceptor walks the graph path from the deepest live
443
+ ancestor down to the target, creating each missing dock in order and yielding a
444
+ frame between each so `connectedCallback` (and graph self-registration) runs.
445
+ This is what makes `via: ['main', 'sidebar', 'settings-panel']` work on a cold
446
+ hard refresh.
447
+
448
+ ### Boot gate
449
+
450
+ `page()` and `dock()` register a prerequisite on the boot gate so the initial
451
+ match waits for element definitions. A hard refresh on a deep route builds its
452
+ own layout instead of erroring.
453
+
454
+ ### Native files
455
+
456
+ `template: { html: './t.html', css: './s.css' }` resolves relative to the third
457
+ `import.meta.url` argument, giving full IDE support.
458
+
459
+ The native toolchain also emits `routes.json` for declarative routes during `scan`,
460
+ `build`, and `dev`.
440
461
 
441
462
  ```json
442
463
  {
@@ -446,6 +467,7 @@ The native toolchain also emits `routes.json` for declarative routes during `sca
446
467
  "tag": "page-member",
447
468
  "path": "/members/:member",
448
469
  "container": "main",
470
+ "via": ["main"],
449
471
  "params": ["member"]
450
472
  }
451
473
  ]
@@ -454,19 +476,19 @@ The native toolchain also emits `routes.json` for declarative routes during `sca
454
476
 
455
477
  ## 11. Containers
456
478
 
457
- Use `ui.container` for router-owned layout slots.
479
+ Use `dock` for router-owned layout slots. It registers the container in the
480
+ hierarchical graph and supports LCA traversal and cascade mounting.
458
481
 
459
482
  ```javascript
460
- ui.container('app-main', {
461
- template: '<slot></slot>',
462
- style: ':host { display: block; }'
463
- });
483
+ import { dock } from '@adukiorg/anza/defs';
484
+
485
+ dock('app-main', { parent: 'body' });
464
486
  ```
465
487
 
466
488
  Mount it in HTML:
467
489
 
468
490
  ```html
469
- <app-main name="main"></app-main>
491
+ <app-main></app-main>
470
492
  ```
471
493
 
472
494
  The container registers itself with the router using its `name` attribute or tag name.
@@ -633,7 +655,7 @@ The router wraps route event work in `transitions.run`.
633
655
  import { transitions } from '@adukiorg/anza/router';
634
656
 
635
657
  await transitions.run(() => {
636
- outlet.replaceChildren(page);
658
+ container.replaceChildren(page);
637
659
  });
638
660
  ```
639
661
 
@@ -641,7 +663,7 @@ Shared element source:
641
663
 
642
664
  ```javascript
643
665
  await transitions.run(() => {
644
- outlet.replaceChildren(detail);
666
+ container.replaceChildren(detail);
645
667
  }, {
646
668
  sourceElement: card,
647
669
  name: 'selected-card'
@@ -756,8 +778,8 @@ Polyfills exist in `core/platform` for Navigation API and URLPattern. Other APIs
756
778
 
757
779
  - Use `router.register` for manual routes.
758
780
  - Use `router.load` to bulk-register routes from JSON configuration.
759
- - Use `ui.element({ url, container })` for declarative page routes.
760
- - Use `ui.container` for named route outlets.
781
+ - Use `page()` for declarative route-bound elements.
782
+ - Use `dock()` for named layout containers.
761
783
  - Use `router.on('found')`, `router.on('notfound')`, and `router.on('error')` for events.
762
784
  - Use `router.guard` for client-side route gates.
763
785
  - Use `router.guards.clear()` in tests or teardown.
@@ -44,12 +44,14 @@ export function start(router) {
44
44
  isSyncing = true;
45
45
  const navResult = router.navigate(url, { state });
46
46
 
47
+ // Delay clearing isSyncing by one macrotask so a guard-triggered redirect
48
+ // (whose URL differs from `sent`) does not slip past the echo guard and
49
+ // re-broadcast, amplifying across every open tab (RT bug 8.5).
50
+ const finish = () => setTimeout(() => { isSyncing = false; }, 0);
47
51
  if (navResult?.finished) {
48
- navResult.finished.finally(() => {
49
- isSyncing = false;
50
- }).catch(() => {});
52
+ navResult.finished.then(finish, finish);
51
53
  } else {
52
- isSyncing = false;
54
+ finish();
53
55
  }
54
56
  }
55
57
  };