@adukiorg/anza 0.4.2 → 0.4.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.
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adukiorg/anza",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Anza web platform library — reactive state, networking, offline, animations, custom elements. Instant build step. Pure browser ESM.",
5
5
  "author": "fescii",
6
6
  "license": "MIT",
@@ -59,12 +59,22 @@ export async function ensure(target, current = 'main') {
59
59
  if (resolve(node.name)) continue;
60
60
  const parentEl = node.parent?.ref?.deref() ?? null;
61
61
  if (!parentEl || !parentEl.isConnected) throw new Error(`CascadeError: parent '${node.parent?.name}' is disconnected while mounting '${node.name}'`);
62
- const tag = node.name;
62
+ const tag = node.tag;
63
63
  if (tag.includes('-') && typeof customElements !== 'undefined' && !customElements.get(tag)) await customElements.whenDefined(tag);
64
64
  const el = document.createElement(tag);
65
65
  parentEl.replaceChildren(el);
66
- await frame();
67
- if (!resolve(node.name)) throw new Error(`CascadeError: container '${node.name}' failed to register after mount`);
66
+
67
+ // Wait for the element's connectedCallback to complete, including any
68
+ // async resource loading (template/style fetches). A single frame is
69
+ // not sufficient when preloadResources() hits the network.
70
+ let attempts = 0;
71
+ while (!resolve(node.name) && attempts < 50) {
72
+ await frame();
73
+ attempts++;
74
+ }
75
+ if (!resolve(node.name)) {
76
+ throw new Error(`CascadeError: container '${node.name}' failed to register after mount`);
77
+ }
68
78
  }
69
79
 
70
80
  return resolve(target);
@@ -62,8 +62,8 @@ function ensureObserver() {
62
62
  * @param {string|null} [parent='main'] - parent container key in the graph.
63
63
  * Pass null explicitly when registering the root (no parent).
64
64
  */
65
- export function registerContainer(name, element, parent = 'main') {
66
- add(name, element, parent);
65
+ export function registerContainer(name, element, parent = 'main', tag = null) {
66
+ add(name, element, parent, tag);
67
67
  }
68
68
 
69
69
  /**
@@ -14,8 +14,9 @@
14
14
  */
15
15
 
16
16
  class Node {
17
- constructor(name, ref, parent) {
17
+ constructor(name, tag, ref, parent) {
18
18
  this.name = name; // registry key, e.g. 'main' | 'sidebar'
19
+ this.tag = tag; // custom element tag, e.g. 'dock-docs' | 'main'
19
20
  this.ref = ref; // WeakRef<Element> | null (null for virtual root)
20
21
  this.parent = parent; // Node | null
21
22
  this.children = new Set(); // Set<Node>
@@ -33,7 +34,7 @@ const nodes = new Map();
33
34
  // The permanent root node. ref is null at module-evaluation time because
34
35
  // modules may be evaluated before the parser reaches <main id="main">.
35
36
  // boot.js fills in the WeakRef during anchor().
36
- const root = new Node('main', null, null);
37
+ const root = new Node('main', 'main', null, null);
37
38
  root.depth = 0;
38
39
  nodes.set('main', root);
39
40
 
@@ -76,9 +77,10 @@ function detach(name) {
76
77
  * @param {string} name - unique registry key.
77
78
  * @param {Element} el - the container element.
78
79
  * @param {string} [parent='main'] - parent registry key.
80
+ * @param {string} [tag] - custom element tag; defaults to name.
79
81
  * @returns {Node} the inserted node.
80
82
  */
81
- export function add(name, el, parent = 'main') {
83
+ export function add(name, el, parent = 'main', tag = null) {
82
84
  gone.delete(name);
83
85
 
84
86
  const existing = nodes.get(name);
@@ -103,7 +105,8 @@ export function add(name, el, parent = 'main') {
103
105
  }
104
106
 
105
107
  const parentNode = nodes.get(parent) ?? root;
106
- const node = new Node(name, el ? new WeakRef(el) : null, parentNode);
108
+ const resolvedTag = tag || name;
109
+ const node = new Node(name, resolvedTag, el ? new WeakRef(el) : null, parentNode);
107
110
  parentNode.children.add(node);
108
111
  nodes.set(name, node);
109
112
  if (el) finalizer.register(el, name);
@@ -16,7 +16,6 @@ import { getContainer } from './container.js';
16
16
  import { get as graphGet } from './graph.js';
17
17
  import { match } from './match.js';
18
18
  import { specRegistry } from '../ui/define/state.js';
19
- import { transitions } from './transitions.js';
20
19
 
21
20
  /**
22
21
  * Built-in minimal 404 HTML rendered when no user notfound is configured.
@@ -241,39 +240,37 @@ async function pipe(event, precommitted) {
241
240
  }
242
241
  }
243
242
 
244
- await transitions.run(async () => {
245
- if (routeMatch) {
246
- if (isCallback(routeMatch.route.handler)) {
247
- try {
248
- await runCallback(routeMatch.route.handler, routeMatch.params, event);
249
- } catch (err) {
250
- emit('error', { error: err, url: destination.url, route: routeMatch.route, phase: 'handler' });
251
- return;
252
- }
243
+ if (routeMatch) {
244
+ if (isCallback(routeMatch.route.handler)) {
245
+ try {
246
+ await runCallback(routeMatch.route.handler, routeMatch.params, event);
247
+ } catch (err) {
248
+ emit('error', { error: err, url: destination.url, route: routeMatch.route, phase: 'handler' });
249
+ return;
253
250
  }
251
+ }
254
252
 
255
- const ctx = buildRouteContext(routeMatch.tag, routeMatch.params, destination.url);
256
- emit('found', {
257
- tag: routeMatch.tag,
258
- params: ctx.params,
259
- query: ctx.query,
260
- raw: ctx.raw,
261
- hash: routeMatch.hash,
262
- chain: routeMatch.chain,
263
- via: chain,
264
- container: chain.at(-1) ?? null,
265
- url: destination.url,
266
- direction: event.navigationType
267
- });
253
+ const ctx = buildRouteContext(routeMatch.tag, routeMatch.params, destination.url);
254
+ emit('found', {
255
+ tag: routeMatch.tag,
256
+ params: ctx.params,
257
+ query: ctx.query,
258
+ raw: ctx.raw,
259
+ hash: routeMatch.hash,
260
+ chain: routeMatch.chain,
261
+ via: chain,
262
+ container: chain.at(-1) ?? null,
263
+ url: destination.url,
264
+ direction: event.navigationType
265
+ });
266
+ } else {
267
+ emit('notfound', { url: destination.url });
268
+ if (notFoundHandler) {
269
+ await notFoundHandler(event);
268
270
  } else {
269
- emit('notfound', { url: destination.url });
270
- if (notFoundHandler) {
271
- await notFoundHandler(event);
272
- } else {
273
- renderNotFound('main');
274
- }
271
+ renderNotFound('main');
275
272
  }
276
- });
273
+ }
277
274
  }
278
275
 
279
276
  /**
@@ -21,12 +21,13 @@ function injectSheet() {
21
21
  try {
22
22
  const sheet = new CSSStyleSheet();
23
23
  sheet.replaceSync(`
24
- ::view-transition-group(root) {
24
+ /* Dock-swap group: element-scoped content transitions */
25
+ ::view-transition-group(dock-swap) {
25
26
  animation-duration: var(--transition-duration);
26
27
  animation-timing-function: var(--transition-easing);
27
28
  }
28
- ::view-transition-old(root),
29
- ::view-transition-new(root) {
29
+ ::view-transition-old(dock-swap),
30
+ ::view-transition-new(dock-swap) {
30
31
  animation-duration: var(--transition-duration);
31
32
  animation-timing-function: var(--transition-easing);
32
33
  background: var(--transition-bg);
@@ -53,7 +53,9 @@ export function container(tag, spec, base = import.meta.url) {
53
53
  delete this.dataset.transitionDirection;
54
54
  };
55
55
 
56
- // Strategy 1: Element-scoped transition (Chrome 147+, concurrent-safe)
56
+ // Only use element-scoped view transitions. Document-level
57
+ // startViewTransition() captures the entire page and causes
58
+ // chrome flicker — never use it as a fallback.
57
59
  if (typeof this.startViewTransition === 'function') {
58
60
  try {
59
61
  const vt = this.startViewTransition(doSwap);
@@ -64,18 +66,6 @@ export function container(tag, spec, base = import.meta.url) {
64
66
  return;
65
67
  }
66
68
 
67
- // Strategy 2: Document-scoped transition (Baseline Oct 2025)
68
- if (typeof document.startViewTransition === 'function') {
69
- try {
70
- const vt = document.startViewTransition(doSwap);
71
- await vt.ready;
72
- } catch (err) {
73
- if (err?.name !== 'AbortError') console.warn('[UI Container] Document VT aborted:', err);
74
- }
75
- return;
76
- }
77
-
78
- // Strategy 3: Synchronous direct swap
79
69
  doSwap();
80
70
  };
81
71
  }
@@ -34,7 +34,7 @@ export function dock(name, config = {}, base) {
34
34
  const parent = config.parent ?? 'main';
35
35
 
36
36
  // Statically register the layout container in the graph with a null element
37
- router.registerContainer(name, null, parent);
37
+ router.registerContainer(name, null, parent, tag);
38
38
 
39
39
  const spec = translate(config);
40
40
 
@@ -51,7 +51,7 @@ export function dock(name, config = {}, base) {
51
51
  // user-supplied connect/disconnect rather than clobbering them.
52
52
  const userMount = spec.mount;
53
53
  spec.mount = (ctx) => {
54
- router.registerContainer(name, ctx.el, parent);
54
+ router.registerContainer(name, ctx.el, parent, tag);
55
55
  return userMount?.(ctx);
56
56
  };
57
57
  const userUnmount = spec.unmount;
@@ -121,26 +121,22 @@ async function swap(el, options = {}) {
121
121
  }
122
122
  }
123
123
 
124
+ // Only use element-scoped view transitions. If the browser doesn't support
125
+ // this.startViewTransition (Chrome < 147), fall back to a direct synchronous
126
+ // swap. Document-level startViewTransition() captures the entire page and
127
+ // causes the sidebar/header to flicker — we never want that.
124
128
  if (typeof this.startViewTransition === 'function') {
125
129
  try {
130
+ // Assign a named view-transition so CSS can target this element's
131
+ // group explicitly, keeping the root group stable.
132
+ this.style.viewTransitionName = 'dock-swap';
126
133
  this._tx = this.startViewTransition(go);
127
134
  await this._tx.finished;
128
135
  } catch (err) {
129
136
  if (err?.name !== 'AbortError') console.warn('[Native UI] dock scoped VT aborted:', err);
130
137
  } finally {
131
138
  this._tx = null;
132
- restore();
133
- }
134
- return;
135
- }
136
-
137
- if (typeof document !== 'undefined' && typeof document.startViewTransition === 'function') {
138
- try {
139
- const vt = document.startViewTransition(go);
140
- await vt.finished;
141
- } catch (err) {
142
- if (err?.name !== 'AbortError') console.warn('[Native UI] dock document VT aborted:', err);
143
- } finally {
139
+ this.style.viewTransitionName = '';
144
140
  restore();
145
141
  }
146
142
  return;
@@ -11,8 +11,6 @@
11
11
  *::before,
12
12
  *::after {
13
13
  box-sizing: border-box;
14
- margin: 0;
15
- padding: 0;
16
14
  }
17
15
 
18
16
  html,
@@ -2,6 +2,10 @@
2
2
  * tokens/semantic/transitions.css
3
3
  *
4
4
  * View Transition pseudo-element mapping and layout swap animations.
5
+ *
6
+ * Docks use element-scoped view transitions via this.startViewTransition().
7
+ * The transitioned element gets viewTransitionName='dock-swap'.
8
+ * Never target (root) — that animates the stable chrome (sidebar, header).
5
9
  */
6
10
 
7
11
  :root {
@@ -14,10 +18,10 @@
14
18
  --transition-bg: var(--color-surface-page);
15
19
  }
16
20
 
17
- ::view-transition-old(root) {
21
+ ::view-transition-old(dock-swap) {
18
22
  animation: 90ms ease-in both fade-out;
19
23
  }
20
- ::view-transition-new(root) {
24
+ ::view-transition-new(dock-swap) {
21
25
  animation: var(--transition-duration) var(--transition-easing) both slide-in;
22
26
  }
23
27
 
@@ -34,9 +38,8 @@
34
38
  --transition-duration: 0ms;
35
39
  --transition-easing: linear;
36
40
  }
37
- ::view-transition-old(root),
38
- ::view-transition-new(root) {
41
+ ::view-transition-old(dock-swap),
42
+ ::view-transition-new(dock-swap) {
39
43
  animation: none !important;
40
44
  }
41
45
  }
42
-