@adukiorg/anza 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -17,6 +17,16 @@ Versioning follows [Semantic Versioning](https://semver.org/).
17
17
 
18
18
  ---
19
19
 
20
+ ## [0.2.8] — 2026-06-09
21
+
22
+ ### Fixed
23
+
24
+ - Remove empty `importmap.json` from scaffold — HTML now points to `/dist/importmap.json`
25
+ - Category barrel files for generated doc pages (`docs/*/index.js`)
26
+ - Docs root route (`/docs`) uses `entry/` folder like other pages
27
+
28
+ ---
29
+
20
30
  ## [0.2.7] — 2026-06-09
21
31
 
22
32
  ### Fixed
Binary file
Binary file
Binary file
Binary file
Binary file
package/bin/create/run.js CHANGED
@@ -22,8 +22,6 @@ const DIRS = [
22
22
  'src/styles',
23
23
  ];
24
24
 
25
- const MAP = JSON.stringify({ imports: {} }, null, 2) + '\n';
26
-
27
25
  const HTML = (name) => `<!DOCTYPE html>
28
26
  <html lang="en">
29
27
  <head>
@@ -31,7 +29,7 @@ const HTML = (name) => `<!DOCTYPE html>
31
29
  <meta name="viewport" content="width=device-width, initial-scale=1" />
32
30
  <title>${name}</title>
33
31
 
34
- <script type="importmap" src="/importmap.json"></script>
32
+ <script type="importmap" src="/dist/importmap.json"></script>
35
33
 
36
34
  <link rel="stylesheet" href="/dist/tokens/index.css" />
37
35
  <link rel="stylesheet" href="/dist/styles/index.css" />
@@ -185,7 +183,6 @@ export function run(target, name, library) {
185
183
  }
186
184
  }
187
185
 
188
- write.write(join(target, 'importmap.json'), MAP);
189
186
  write.write(join(target, 'src', 'index.html'), HTML(name));
190
187
  write.write(join(target, 'src', 'app.js'), APP);
191
188
  write.write(join(target, 'src', 'sw.js'), SW);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adukiorg/anza",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Anza web platform library — reactive state, networking, offline, animations, custom elements. Zero build step. Pure browser ESM.",
5
5
  "author": "fescii",
6
6
  "license": "MIT",
@@ -6,9 +6,14 @@
6
6
  * has resolved. This is what makes a hard refresh on a deep route work: the
7
7
  * first match no longer races ahead of element registration.
8
8
  *
9
- * Source: tasks.md Phase 1
9
+ * Source: tasks.md Phase 1 / Migration 2
10
10
  */
11
11
 
12
+ import { root as graphRoot } from './graph.js';
13
+ import { registerContainer } from './container.js';
14
+
15
+ const ROOT = 'main';
16
+
12
17
  // Outstanding prerequisites the initial match must wait for.
13
18
  const gates = new Set();
14
19
 
@@ -34,6 +39,23 @@ export function gate(promise) {
34
39
  return promise;
35
40
  }
36
41
 
42
+ /**
43
+ * Wires <main id="main"> into the graph. Must be called after DOMContentLoaded
44
+ * so the element is guaranteed to exist. Throws a hard error if absent —
45
+ * there is no body fallback.
46
+ */
47
+ function anchor() {
48
+ const el = document.getElementById('main');
49
+ if (!el) {
50
+ throw new Error(
51
+ `[Router] <main id="main"> is required but was not found in the document. ` +
52
+ `Ensure your HTML shell contains exactly one <main id="main"> before any scripts run.`
53
+ );
54
+ }
55
+ graphRoot.ref = new WeakRef(el);
56
+ registerContainer(ROOT, el, null); // null parent — root has no parent
57
+ }
58
+
37
59
  /**
38
60
  * Fires the initial route match once the document is interactive and all gates
39
61
  * have settled. Idempotent — only the first call wins.
@@ -46,6 +68,9 @@ export function boot(emitFn) {
46
68
 
47
69
  const launch = async () => {
48
70
  if (booted) return;
71
+
72
+ anchor(); // 1. wire main#main into the graph — must be first
73
+
49
74
  // Snapshot current gates; settle them all (failures are non-fatal — a
50
75
  // single element that fails to define must not wedge the whole router).
51
76
  const pending = Array.from(gates);
@@ -55,7 +80,7 @@ export function boot(emitFn) {
55
80
  booted = true;
56
81
  const fn = trigger;
57
82
  trigger = null;
58
- if (fn) await fn();
83
+ if (fn) await fn(); // 3. first match + emit
59
84
  };
60
85
 
61
86
  if (typeof document !== 'undefined' && document.readyState === 'loading') {
@@ -79,4 +104,5 @@ export function reset() {
79
104
  gates.clear();
80
105
  booted = false;
81
106
  trigger = null;
107
+ graphRoot.ref = null; // invalidate the WeakRef so anchor() re-queries on next boot
82
108
  }
@@ -9,7 +9,7 @@
9
9
  * Source: tasks.md Phase 5
10
10
  */
11
11
 
12
- import { get, element as resolve, root } from './graph.js';
12
+ import { get, element as resolve } from './graph.js';
13
13
  import { path } from './lca.js';
14
14
 
15
15
  /** Yields one frame so a freshly-connected element can register itself. */
@@ -23,10 +23,10 @@ function frame() {
23
23
  * containers from the deepest currently-mounted ancestor downward.
24
24
  *
25
25
  * @param {string} target - container name that must end up in the DOM.
26
- * @param {string} [current='body'] - the source container to path from.
26
+ * @param {string} [current='main'] - the source container to path from.
27
27
  * @returns {Promise<Element|null>} the resolved target element.
28
28
  */
29
- export async function ensure(target, current = 'body') {
29
+ export async function ensure(target, current = 'main') {
30
30
  // Already mounted — nothing to do.
31
31
  const live = resolve(target);
32
32
  if (live) return live;
@@ -41,13 +41,13 @@ export async function ensure(target, current = 'body') {
41
41
  if (el && el.isConnected) mounted = node;
42
42
  else break;
43
43
  }
44
- if (!mounted) mounted = root;
44
+ if (!mounted) mounted = get('main'); // fall back to the graph root
45
45
 
46
46
  // Mount sequentially from the first unmounted node down to the target.
47
47
  const start = segments.indexOf(mounted) + 1;
48
48
  for (let i = start; i < segments.length; i++) {
49
49
  const node = segments[i];
50
- const parentEl = node.parent?.ref?.deref() ?? (node.parent === root ? document.body : null);
50
+ const parentEl = node.parent?.ref?.deref() ?? null;
51
51
  if (!parentEl || !parentEl.isConnected) {
52
52
  throw new Error(`CascadeError: parent '${node.parent?.name}' is disconnected while mounting '${node.name}'`);
53
53
  }
@@ -26,11 +26,14 @@ function ensureObserver() {
26
26
 
27
27
  const attach = () => {
28
28
  if (observer) return;
29
+ const root = document.getElementById('main');
30
+ if (!root) return; // framework root absent — nothing to observe
31
+
29
32
  observer = new MutationObserver(() => {
30
33
  let stillWaiting = false;
31
34
  for (const selector of observedSelectors) {
32
35
  if (!graphElement(selector)) {
33
- const el = document.querySelector(selector);
36
+ const el = root.querySelector(selector); // scope to root, not document
34
37
  if (el) registerContainer(selector, el);
35
38
  else stillWaiting = true;
36
39
  }
@@ -40,7 +43,8 @@ function ensureObserver() {
40
43
  observer = null;
41
44
  }
42
45
  });
43
- observer.observe(document.body, { childList: true, subtree: true });
46
+
47
+ observer.observe(root, { childList: true, subtree: true });
44
48
  };
45
49
 
46
50
  if (typeof requestIdleCallback !== 'undefined') {
@@ -55,9 +59,10 @@ function ensureObserver() {
55
59
  *
56
60
  * @param {string} name - unique registry key (or CSS selector).
57
61
  * @param {HTMLElement} element - the DOM element instance.
58
- * @param {string} [parent='body'] - parent container key in the graph.
62
+ * @param {string|null} [parent='main'] - parent container key in the graph.
63
+ * Pass null explicitly when registering the root (no parent).
59
64
  */
60
- export function registerContainer(name, element, parent = 'body') {
65
+ export function registerContainer(name, element, parent = 'main') {
61
66
  add(name, element, parent);
62
67
  }
63
68
 
@@ -86,8 +91,9 @@ export function getContainer(name) {
86
91
  if (el) return el;
87
92
 
88
93
  if (isSelector(name) && typeof document !== 'undefined') {
94
+ const root = document.getElementById('main');
89
95
  try {
90
- el = document.querySelector(name);
96
+ el = root ? root.querySelector(name) : null;
91
97
  if (el) {
92
98
  registerContainer(name, el);
93
99
  return el;
@@ -100,15 +106,16 @@ export function getContainer(name) {
100
106
  return el ?? undefined;
101
107
  }
102
108
 
103
- // Warn when a plain key shadows a real DOM element of the same tag name.
109
+ // Warn when a plain key shadows a real DOM element inside the root.
104
110
  if (typeof name === 'string' && typeof document !== 'undefined') {
111
+ const root = document.getElementById('main');
105
112
  let queryResult = null;
106
- try { queryResult = document.querySelector(name); } catch (_) {}
113
+ try { queryResult = root?.querySelector(name) ?? null; } catch (_) {}
107
114
  if (queryResult) {
108
115
  console.warn(
109
- `[Router] Container name "${name}" is ambiguous: it exists in the DOM as a selector ` +
110
- `but is being treated as a registry key. Use a selector prefix (e.g., "#${name}") ` +
111
- `to explicitly target the DOM element, or ensure it is registered via registerContainer().`
116
+ `[Router] Container name "${name}" matches a DOM element inside <main id="main"> ` +
117
+ `but is being treated as a registry key. ` +
118
+ `Use a selector prefix or register it via registerContainer().`
112
119
  );
113
120
  }
114
121
  }
@@ -7,7 +7,8 @@
7
7
  *
8
8
  * Every node holds a WeakRef to its element so an unmounted container can be
9
9
  * garbage-collected without a manual unregister. A FinalizationRegistry prunes
10
- * stale nodes. The virtual root 'body' always exists.
10
+ * stale nodes. The virtual root 'main' always exists; its WeakRef is filled in
11
+ * by boot.js anchor() once <main id="main"> is live in the DOM.
11
12
  *
12
13
  * Source: tasks.md Phase 3
13
14
  */
@@ -28,8 +29,13 @@ class Node {
28
29
  }
29
30
 
30
31
  const nodes = new Map();
31
- const root = new Node('body', null, null);
32
- nodes.set('body', root);
32
+
33
+ // The permanent root node. ref is null at module-evaluation time because
34
+ // modules may be evaluated before the parser reaches <main id="main">.
35
+ // boot.js fills in the WeakRef during anchor().
36
+ const root = new Node('main', null, null);
37
+ root.depth = 0;
38
+ nodes.set('main', root);
33
39
 
34
40
  // Explicit unregistrations. element() returns undefined for a name in this set
35
41
  // so a just-unmounted container is not confused with a GC'd one (RT bug 8.2).
@@ -61,14 +67,20 @@ function detach(name) {
61
67
  *
62
68
  * @param {string} name - unique registry key.
63
69
  * @param {Element} el - the container element.
64
- * @param {string} [parent='body'] - parent registry key.
70
+ * @param {string} [parent='main'] - parent registry key.
65
71
  * @returns {Node} the inserted node.
66
72
  */
67
- export function add(name, el, parent = 'body') {
73
+ export function add(name, el, parent = 'main') {
68
74
  gone.delete(name);
69
75
 
70
76
  const existing = nodes.get(name);
71
- if (existing && existing !== root) {
77
+ if (existing) {
78
+ if (existing === root) {
79
+ // Root re-registration from anchor(): refresh the WeakRef and return.
80
+ // The root cannot be reparented and must not spawn a duplicate node.
81
+ existing.ref = new WeakRef(el);
82
+ return existing;
83
+ }
72
84
  const prev = existing.ref?.deref();
73
85
  if (prev && prev !== el) {
74
86
  throw new Error(`ContainerError: Singleton violation — '${name}' is already mounted. A second instance cannot register while the first is active.`);
@@ -138,7 +150,8 @@ export function clear() {
138
150
  nodes.clear();
139
151
  root.children.clear();
140
152
  gone.clear();
141
- nodes.set('body', root);
153
+ root.ref = null; // reset the WeakRef so anchor() re-queries on next boot
154
+ nodes.set('main', root); // root is 'main'; 'body' has no special status
142
155
  }
143
156
 
144
157
  export { Node, root };
@@ -13,7 +13,8 @@ import {
13
13
  setup, destroy,
14
14
  addGuard, setNotFound,
15
15
  guardsApi, missApi,
16
- on, nav, registerNavigator
16
+ on, nav, registerNavigator,
17
+ getShell, getWin
17
18
  } from './intercept.js';
18
19
  import {
19
20
  navigate,
@@ -103,7 +104,12 @@ export const router = {
103
104
 
104
105
  // Lifecycle
105
106
  setup,
106
- destroy
107
+ destroy,
108
+
109
+ // Root accessors — useful for components that need the shell element
110
+ // without importing intercept directly.
111
+ shell: getShell, // router.shell() → <main id="main">
112
+ win: getWin // router.win() → window
107
113
  };
108
114
 
109
115
  // Auto-bootstrap client-side navigation listeners on client load
@@ -19,6 +19,16 @@ let guards = [];
19
19
  let notFoundHandler = null;
20
20
  let ready = false;
21
21
 
22
+ // Module-level root slots — captured once in setup(), cleared in destroy().
23
+ let win = null;
24
+ let shell = null; // WeakRef<HTMLElement> — points to <main id="main">
25
+
26
+ /** Returns the live <main id="main"> element, or null if GC'd. */
27
+ export function getShell() { return shell?.deref() ?? null; }
28
+
29
+ /** Returns the captured window reference. */
30
+ export function getWin() { return win; }
31
+
22
32
  // Navigation API listener references for teardown
23
33
  let navListener = null;
24
34
  let successListener = null;
@@ -110,6 +120,13 @@ export function setup() {
110
120
  if (typeof window === 'undefined' || !window.navigation) return;
111
121
  ready = true;
112
122
 
123
+ win = window;
124
+ const mainEl = document.getElementById('main');
125
+ // Only wrap in WeakRef when the element exists. setup() may be called at
126
+ // module-evaluation time (before DOMContentLoaded), so the element may not
127
+ // be in the DOM yet. anchor() in boot.js is the authoritative check that
128
+ // throws if the element is absent at boot time.
129
+ if (mainEl) shell = new WeakRef(mainEl);
113
130
  navListener = (event) => {
114
131
  // Skip cross-origin navigations, file downloads, or same-document hash scrolls
115
132
  if (!event.canIntercept || event.hashChange || event.downloadRequest) {
@@ -163,7 +180,7 @@ export function setup() {
163
180
  try {
164
181
  for (let i = 0; i < chain.length; i++) {
165
182
  if (!getContainer(chain[i])) {
166
- await ensure(chain[i], chain[i - 1] ?? 'body');
183
+ await ensure(chain[i], chain[i - 1] ?? 'main'); // was 'body'
167
184
  }
168
185
  }
169
186
  } catch (err) {
@@ -289,7 +306,8 @@ export function setup() {
289
306
  export function destroy() {
290
307
  if (!ready) return;
291
308
  ready = false;
292
-
309
+ win = null;
310
+ shell = null;
293
311
  if (typeof window !== 'undefined' && window.navigation) {
294
312
  if (navListener) window.navigation.removeEventListener('navigate', navListener);
295
313
  if (successListener) window.navigation.removeEventListener('navigatesuccess', successListener);