@adukiorg/anza 0.2.0 → 0.2.2
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 +81 -4
- package/README.md +97 -133
- package/bin/anza/anza +0 -0
- package/bin/anza/anza.exe +0 -0
- package/bin/anza/find.js +35 -0
- package/bin/anza/index.js +34 -0
- package/bin/anza/launch.js +19 -0
- package/bin/common/index.js +7 -0
- package/bin/common/logs.js +62 -0
- package/bin/create/copy.js +18 -0
- package/bin/create/index.js +45 -0
- package/bin/create/run.js +210 -0
- package/bin/create/write.js +19 -0
- package/importmap.json +4 -0
- package/package.json +16 -10
- package/src/core/offline/{usage.md → notes/usage.md} +11 -1
- package/src/core/router/boot.js +82 -0
- package/src/core/router/cascade.js +76 -0
- package/src/core/router/container.js +63 -72
- package/src/core/router/graph.js +144 -0
- package/src/core/router/index.js +12 -2
- package/src/core/router/intercept.js +26 -7
- package/src/core/router/lca.js +58 -0
- package/src/core/router/match.js +49 -36
- package/src/core/router/notes/audit-old.md +887 -0
- package/src/core/router/notes/audti.md +773 -0
- package/src/core/router/notes/tasks.md +473 -0
- package/src/core/router/{usage.md → notes/usage.md} +57 -35
- package/src/core/router/sync/tab.js +6 -4
- package/src/core/router/transitions.js +35 -8
- package/src/core/router/trie.js +130 -0
- package/src/core/security/{usage.md → notes/usage.md} +1 -2
- package/src/core/storage/{usage.md → notes/usage.md} +6 -6
- package/src/core/theme/index.js +78 -0
- package/src/core/ui/define/index.js +2 -1
- package/src/core/ui/define/orchestrator.js +10 -4
- package/src/core/ui/defs/dock.js +134 -0
- package/src/core/ui/defs/index.js +20 -0
- package/src/core/ui/defs/page.js +89 -0
- package/src/core/ui/defs/part.js +28 -0
- package/src/core/ui/defs/spec.js +96 -0
- package/src/core/ui/defs/view.js +23 -0
- package/src/core/ui/index.js +16 -3
- package/src/core/ui/notes/definations.md +979 -0
- package/src/tokens/index.css +1 -0
- package/src/tokens/semantic/contrast.css +18 -0
- package/src/tokens/semantic/transitions.css +32 -0
- package/types/core/platform/index.d.ts +39 -10
- package/types/core/router/index.d.ts +9 -0
- package/types/core/theme/index.d.ts +18 -0
- package/types/core/ui/index.d.ts +11 -0
- package/types/index.d.ts +1 -0
- package/bin/anza.js +0 -63
- package/bin/create.js +0 -150
- package/src/core/api/plan.md +0 -209
- package/src/core/events/missing.md +0 -103
- package/src/core/events/plan.md +0 -177
- package/src/core/offline/missing.md +0 -89
- package/src/core/offline/plan.md +0 -143
- package/src/core/platform/missing.md +0 -119
- package/src/core/platform/platform.d.ts +0 -88
- package/src/core/router/missing.md +0 -716
- package/src/core/router/outlet.js +0 -139
- package/src/core/router/plan.md +0 -370
- package/src/core/security/missing.md +0 -97
- package/src/core/state/missing.md +0 -165
- package/src/core/storage/missing.md +0 -165
- package/src/core/storage/plan.md +0 -69
- package/src/core/ui/implementation.md +0 -170
- package/src/core/ui/plan.md +0 -510
- package/src/core/ui/ui.types.md +0 -890
- /package/src/core/animations/{usage.md → notes/usage.md} +0 -0
- /package/src/core/api/{usage.md → notes/usage.md} +0 -0
- /package/src/core/events/{usage.md → notes/usage.md} +0 -0
- /package/src/core/platform/{usage.md → notes/usage.md} +0 -0
- /package/src/core/state/{usage.md → notes/usage.md} +0 -0
- /package/src/core/ui/{usage.md → notes/usage.md} +0 -0
- /package/src/core/ui/{watch.md → notes/watch.md} +0 -0
- /package/src/core/workers/{plan.md → notes/plan.md} +0 -0
- /package/src/core/workers/{usage.md → notes/usage.md} +0 -0
|
@@ -6,9 +6,36 @@
|
|
|
6
6
|
* falling back instantly to standard synchronous rendering when unsupported
|
|
7
7
|
* or when the user prefers reduced motion.
|
|
8
8
|
*
|
|
9
|
+
* Injects a token-aware stylesheet on first run so VT timing and backdrop
|
|
10
|
+
* derive from the semantic token layer.
|
|
11
|
+
*
|
|
9
12
|
* Source: doc 09 — Routing §8, plan.md §5
|
|
10
13
|
*/
|
|
11
14
|
|
|
15
|
+
let injected = false;
|
|
16
|
+
|
|
17
|
+
function injectSheet() {
|
|
18
|
+
if (injected || typeof document === 'undefined') return;
|
|
19
|
+
injected = true;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const sheet = new CSSStyleSheet();
|
|
23
|
+
sheet.replaceSync(`
|
|
24
|
+
::view-transition-group(root) {
|
|
25
|
+
animation-duration: var(--transition-duration);
|
|
26
|
+
animation-timing-function: var(--transition-easing);
|
|
27
|
+
}
|
|
28
|
+
::view-transition-old(root),
|
|
29
|
+
::view-transition-new(root) {
|
|
30
|
+
animation-duration: var(--transition-duration);
|
|
31
|
+
animation-timing-function: var(--transition-easing);
|
|
32
|
+
background: var(--transition-bg);
|
|
33
|
+
}
|
|
34
|
+
`);
|
|
35
|
+
document.adoptedStyleSheets.push(sheet);
|
|
36
|
+
} catch (_) {}
|
|
37
|
+
}
|
|
38
|
+
|
|
12
39
|
export const transitions = {
|
|
13
40
|
/**
|
|
14
41
|
* Wraps a DOM modification callback in a view transition.
|
|
@@ -18,40 +45,40 @@ export const transitions = {
|
|
|
18
45
|
async run(updateDOM, options = {}) {
|
|
19
46
|
const { sourceElement, name = 'selected-item' } = options;
|
|
20
47
|
|
|
21
|
-
const
|
|
48
|
+
const reduced =
|
|
22
49
|
typeof window !== 'undefined' &&
|
|
23
50
|
window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
|
24
51
|
|
|
25
52
|
if (
|
|
26
|
-
!
|
|
53
|
+
!reduced &&
|
|
27
54
|
typeof document !== 'undefined' &&
|
|
28
55
|
typeof document.startViewTransition === 'function'
|
|
29
56
|
) {
|
|
57
|
+
injectSheet();
|
|
58
|
+
|
|
30
59
|
const hasSource = sourceElement && sourceElement instanceof HTMLElement;
|
|
31
60
|
if (hasSource) {
|
|
32
61
|
sourceElement.style.viewTransitionName = name;
|
|
33
62
|
}
|
|
34
63
|
|
|
35
|
-
const
|
|
64
|
+
const tx = document.startViewTransition(() => {
|
|
36
65
|
const res = updateDOM();
|
|
37
66
|
if (res instanceof Promise) return res;
|
|
38
67
|
});
|
|
39
68
|
|
|
40
69
|
if (hasSource) {
|
|
41
|
-
|
|
42
|
-
transition.finished.finally(() => {
|
|
70
|
+
tx.finished.finally(() => {
|
|
43
71
|
sourceElement.style.viewTransitionName = '';
|
|
44
72
|
});
|
|
45
73
|
}
|
|
46
74
|
|
|
47
75
|
try {
|
|
48
|
-
await
|
|
76
|
+
await tx.finished;
|
|
49
77
|
} catch (err) {
|
|
50
|
-
// Silence aborted
|
|
78
|
+
// Silence aborted or superseded transition errors to preserve router stability
|
|
51
79
|
console.warn('View Transition was aborted or failed:', err);
|
|
52
80
|
}
|
|
53
81
|
} else {
|
|
54
|
-
// Graceful fallback for non-supporting browsers or reduced-motion preference
|
|
55
82
|
const res = updateDOM();
|
|
56
83
|
if (res instanceof Promise) await res;
|
|
57
84
|
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/router/trie.js
|
|
3
|
+
*
|
|
4
|
+
* Radix-trie route matcher. Decomposes patterns into '/'-separated segments and
|
|
5
|
+
* matches a pathname in O(k) where k is the segment count, instead of the O(n)
|
|
6
|
+
* linear scan over all routes. Static segments beat params beat wildcards, the
|
|
7
|
+
* same specificity order the URLPattern scan uses.
|
|
8
|
+
*
|
|
9
|
+
* Patterns the trie cannot express (regex groups, optional/repeat modifiers)
|
|
10
|
+
* are rejected by insert() so match.js can keep them on the URLPattern path.
|
|
11
|
+
*
|
|
12
|
+
* Source: tasks.md Phase 7
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
class Segment {
|
|
16
|
+
constructor() {
|
|
17
|
+
this.static = new Map(); // literal -> Segment
|
|
18
|
+
this.param = null; // Segment for a :named segment
|
|
19
|
+
this.wild = null; // Segment for a * segment
|
|
20
|
+
this.route = null; // route entry stored at a terminal node
|
|
21
|
+
this.key = null; // param name (when this node is a param child)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const trie = new Segment();
|
|
26
|
+
|
|
27
|
+
/** A pattern is trie-expressible only if every segment is plain, :param, or *. */
|
|
28
|
+
export function expressible(pattern) {
|
|
29
|
+
if (typeof pattern !== 'string') return false;
|
|
30
|
+
if (pattern.startsWith('http://') || pattern.startsWith('https://')) return false;
|
|
31
|
+
for (const part of pattern.split('/')) {
|
|
32
|
+
if (!part) continue;
|
|
33
|
+
if (part === '*') continue;
|
|
34
|
+
if (part.startsWith(':')) {
|
|
35
|
+
// Reject modifiers/regex groups: ':id?', ':id+', ':id(\\d+)'.
|
|
36
|
+
if (/[?+*(){}]/.test(part)) return false;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
// A literal segment must contain no pattern metacharacters.
|
|
40
|
+
if (/[:*?+(){}]/.test(part)) return false;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Inserts a route under its pattern.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} pattern - e.g. '/members/:id/posts/:post'.
|
|
49
|
+
* @param {object} route - the route entry to store.
|
|
50
|
+
* @returns {boolean} true if inserted, false if the pattern is not expressible.
|
|
51
|
+
*/
|
|
52
|
+
export function insert(pattern, route) {
|
|
53
|
+
if (!expressible(pattern)) return false;
|
|
54
|
+
|
|
55
|
+
const parts = pattern.split('/').filter(Boolean);
|
|
56
|
+
let node = trie;
|
|
57
|
+
|
|
58
|
+
for (const part of parts) {
|
|
59
|
+
if (part === '*') {
|
|
60
|
+
node.wild ??= new Segment();
|
|
61
|
+
node = node.wild;
|
|
62
|
+
} else if (part.startsWith(':')) {
|
|
63
|
+
node.param ??= new Segment();
|
|
64
|
+
node.param.key = part.slice(1);
|
|
65
|
+
node = node.param;
|
|
66
|
+
} else {
|
|
67
|
+
if (!node.static.has(part)) node.static.set(part, new Segment());
|
|
68
|
+
node = node.static.get(part);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
node.route = route;
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Finds the most specific route for a pathname.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} pathname - e.g. '/members/42/posts/7'.
|
|
80
|
+
* @returns {{ route: object, params: Record<string,string> }|null}
|
|
81
|
+
*/
|
|
82
|
+
export function find(pathname) {
|
|
83
|
+
const parts = pathname.split('/').filter(Boolean);
|
|
84
|
+
const params = {};
|
|
85
|
+
const route = walk(trie, parts, 0, params);
|
|
86
|
+
return route ? { route, params } : null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function walk(node, parts, index, params) {
|
|
90
|
+
if (index === parts.length) return node.route;
|
|
91
|
+
|
|
92
|
+
const segment = parts[index];
|
|
93
|
+
|
|
94
|
+
// 1. Static — highest specificity.
|
|
95
|
+
const staticNode = node.static.get(segment);
|
|
96
|
+
if (staticNode) {
|
|
97
|
+
const hit = walk(staticNode, parts, index + 1, params);
|
|
98
|
+
if (hit) return hit;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 2. Param — medium specificity. Backtrack the binding if it fails.
|
|
102
|
+
if (node.param) {
|
|
103
|
+
params[node.param.key] = decode(segment);
|
|
104
|
+
const hit = walk(node.param, parts, index + 1, params);
|
|
105
|
+
if (hit) return hit;
|
|
106
|
+
delete params[node.param.key];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 3. Wildcard — lowest specificity, swallows the remainder.
|
|
110
|
+
if (node.wild) {
|
|
111
|
+
params['*'] = parts.slice(index).map(decode).join('/');
|
|
112
|
+
return node.wild.route;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function decode(s) {
|
|
119
|
+
try { return decodeURIComponent(s); } catch (_) { return s; }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Empties the trie. */
|
|
123
|
+
export function clear() {
|
|
124
|
+
trie.static.clear();
|
|
125
|
+
trie.param = null;
|
|
126
|
+
trie.wild = null;
|
|
127
|
+
trie.route = null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export { Segment };
|
|
@@ -19,7 +19,7 @@ import { uuid, hash, generateKey, encrypt, decrypt, seal, unseal, sign, verify,
|
|
|
19
19
|
## 1. Choosing an API
|
|
20
20
|
|
|
21
21
|
| Need | API | Characteristics |
|
|
22
|
-
|
|
22
|
+
| --- | --- | --- |
|
|
23
23
|
| Unique correlation ID | `uuid()` | Synchronous, `crypto.randomUUID()`, centralized and mockable |
|
|
24
24
|
| Content hashing | `hash(data, algo)` | `SubtleCrypto.digest`, returns `ArrayBuffer` |
|
|
25
25
|
| AES-GCM encryption | `generateKey`, `encrypt`, `decrypt` | Non-extractable by default, fresh 12-byte IV per encrypt |
|
|
@@ -193,7 +193,6 @@ const link = String(sanitize('<a href="javascript:void(0)">click</a>'));
|
|
|
193
193
|
|
|
194
194
|
> [!NOTE]
|
|
195
195
|
> The return type is `TrustedHTML | string`. Use `String(sanitize(...))` when you only need the raw string. Pass the raw result directly to DOM sinks that accept `TrustedHTML` under a Trusted Types policy.
|
|
196
|
-
|
|
197
196
|
> [!WARNING]
|
|
198
197
|
> `sanitize()` is client-side only. It returns the input unchanged when called outside a browser (e.g. Node.js SSR).
|
|
199
198
|
|
|
@@ -19,7 +19,7 @@ import { Database, LRUCache, WeakLRUCache } from '@adukiorg/anza/storage';
|
|
|
19
19
|
## 1. Choosing an API
|
|
20
20
|
|
|
21
21
|
| Need | Tier / API | Characteristics |
|
|
22
|
-
|
|
22
|
+
| --- | --- | --- |
|
|
23
23
|
| In-memory caching | `'memory'` | Synchronous, ephemeral, size-bounded LRU |
|
|
24
24
|
| General structured data | `'idb'` (default) | Asynchronous, persistent, transactional, compressed if >64KB |
|
|
25
25
|
| High-performance files | `'opfs'` | Dedicated worker, synchronous I/O access handles, Web Locked |
|
|
@@ -36,11 +36,11 @@ the box. Call `storage.configure(...)` once, before first use, to customize it.
|
|
|
36
36
|
|
|
37
37
|
```javascript
|
|
38
38
|
storage.configure({
|
|
39
|
-
idb:
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
lru:
|
|
39
|
+
idb: { name: 'app-db', version: 2, migrations: [
|
|
40
|
+
(db) => db.createObjectStore('keyval'),
|
|
41
|
+
(db) => db.createObjectStore('users')
|
|
42
|
+
] },
|
|
43
|
+
lru: { maxSize: 500 },
|
|
44
44
|
cache: { name: 'app-cache' }
|
|
45
45
|
});
|
|
46
46
|
```
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/theme/index.js
|
|
3
|
+
*
|
|
4
|
+
* Automatic theme switching. Attaches to window.theme on import and
|
|
5
|
+
* restores the saved preference (or respects OS dark mode) without any
|
|
6
|
+
* manual init call. The user can still import { theme } and call set or
|
|
7
|
+
* toggle — both update the same global instance.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const KEY = 'anza-theme';
|
|
11
|
+
|
|
12
|
+
function root() {
|
|
13
|
+
return document.documentElement;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function restore() {
|
|
17
|
+
let saved;
|
|
18
|
+
try {
|
|
19
|
+
saved = localStorage.getItem(KEY);
|
|
20
|
+
} catch (_) {}
|
|
21
|
+
|
|
22
|
+
if (saved && saved !== 'auto') {
|
|
23
|
+
root().dataset.theme = saved;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const prefersDark =
|
|
28
|
+
typeof window !== 'undefined' &&
|
|
29
|
+
window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
30
|
+
if (prefersDark) {
|
|
31
|
+
root().dataset.theme = 'dark';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const theme = {
|
|
36
|
+
/** Return the active theme name: light, dark, contrast, or auto. */
|
|
37
|
+
get() {
|
|
38
|
+
const attr = root().dataset.theme;
|
|
39
|
+
if (attr) return attr;
|
|
40
|
+
return 'auto';
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
/** Apply a theme name and persist it. */
|
|
44
|
+
set(name) {
|
|
45
|
+
const el = root();
|
|
46
|
+
if (name === 'auto') {
|
|
47
|
+
delete el.dataset.theme;
|
|
48
|
+
} else {
|
|
49
|
+
el.dataset.theme = name;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
localStorage.setItem(KEY, name);
|
|
53
|
+
} catch (_) {}
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
/** Toggle between light and dark. */
|
|
57
|
+
toggle() {
|
|
58
|
+
const current = this.get();
|
|
59
|
+
const prefersDark =
|
|
60
|
+
typeof window !== 'undefined' &&
|
|
61
|
+
window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
62
|
+
const resolved = current === 'auto' ? (prefersDark ? 'dark' : 'light') : current;
|
|
63
|
+
this.set(resolved === 'dark' ? 'light' : 'dark');
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Auto-bootstrap on client load.
|
|
68
|
+
if (typeof window !== 'undefined') {
|
|
69
|
+
if (!('theme' in window)) {
|
|
70
|
+
Object.defineProperty(window, 'theme', {
|
|
71
|
+
value: theme,
|
|
72
|
+
writable: false,
|
|
73
|
+
enumerable: false,
|
|
74
|
+
configurable: false
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
restore();
|
|
78
|
+
}
|
|
@@ -2,8 +2,9 @@ import { define } from './define.js';
|
|
|
2
2
|
import { element } from './element.js';
|
|
3
3
|
import { container } from './container.js';
|
|
4
4
|
import { initOrchestrator } from './orchestrator.js';
|
|
5
|
+
import { page, dock, view, part } from '../defs/index.js';
|
|
5
6
|
|
|
6
7
|
// Initialize the global routing orchestrator
|
|
7
8
|
initOrchestrator();
|
|
8
9
|
|
|
9
|
-
export { define, element, container };
|
|
10
|
+
export { define, element, container, page, dock, view, part };
|
|
@@ -21,12 +21,18 @@ export function initOrchestrator() {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
const spec = specRegistry.get(topTag.toLowerCase());
|
|
24
|
-
|
|
24
|
+
// Resolve the render target: the last container in the `via` chain, or
|
|
25
|
+
// the legacy single `container`. Without either there is nothing to mount.
|
|
26
|
+
const target = (Array.isArray(spec?.via) && spec.via.length)
|
|
27
|
+
? spec.via[spec.via.length - 1]
|
|
28
|
+
: spec?.container;
|
|
29
|
+
if (!spec || !target) return;
|
|
25
30
|
|
|
26
|
-
// Use Advanced Container Registry lookup instead of blind DOM query
|
|
27
|
-
|
|
31
|
+
// Use Advanced Container Registry lookup instead of blind DOM query.
|
|
32
|
+
// The interceptor's cascade has already ensured the chain is mounted.
|
|
33
|
+
const containerEl = router.getContainer(target);
|
|
28
34
|
if (!containerEl) {
|
|
29
|
-
console.warn(`Target container "${
|
|
35
|
+
console.warn(`Target container "${target}" not found in DOM for element <${topTag}>`);
|
|
30
36
|
return;
|
|
31
37
|
}
|
|
32
38
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/ui/defs/dock.js
|
|
3
|
+
*
|
|
4
|
+
* `dock(name, config)` — a persistent container shell. It lives across route
|
|
5
|
+
* changes, registers its position in the hierarchical container graph the
|
|
6
|
+
* moment it connects, declares its parent dock (for LCA + cascade), and exposes
|
|
7
|
+
* a `swap` method the router calls to replace child content with a view
|
|
8
|
+
* transition. Replaces `<route-outlet>`.
|
|
9
|
+
*
|
|
10
|
+
* Source: definations.md §4, tasks.md Phase 6
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { router } from '../../router/index.js';
|
|
14
|
+
import { gate } from '../../router/boot.js';
|
|
15
|
+
import { element } from '../define/element.js';
|
|
16
|
+
import { translate } from './spec.js';
|
|
17
|
+
|
|
18
|
+
// Element-scoped containment so View Transitions are isolated to the dock.
|
|
19
|
+
const CONTAIN = ':host { contain: layout; display: block; }';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {string} name - unique key in the container graph (e.g. 'main').
|
|
23
|
+
* @param {object} [config]
|
|
24
|
+
* @param {string} [config.tag] - tag name; defaults to `dock-<name>`.
|
|
25
|
+
* @param {string} [config.parent='body'] - parent dock key in the graph.
|
|
26
|
+
* @param {object} [config.template] - { html, css, shadow }.
|
|
27
|
+
* @param {string} [base] - import.meta.url of the caller (file templates).
|
|
28
|
+
*/
|
|
29
|
+
export function dock(name, config = {}, base) {
|
|
30
|
+
const tag = config.tag ?? `dock-${name}`;
|
|
31
|
+
const parent = config.parent ?? 'body';
|
|
32
|
+
|
|
33
|
+
const spec = translate(config);
|
|
34
|
+
|
|
35
|
+
// Default passthrough template — a dock is a shell around its slotted content.
|
|
36
|
+
if (spec.template == null) spec.template = '<slot></slot>';
|
|
37
|
+
// Prepend containment styling to whatever the dock declares.
|
|
38
|
+
spec.style = spec.style ? `${CONTAIN}\n${spec.style}` : CONTAIN;
|
|
39
|
+
|
|
40
|
+
// Register in the graph on connect, unregister on disconnect. Wrap any
|
|
41
|
+
// user-supplied connect/disconnect rather than clobbering them.
|
|
42
|
+
const userMount = spec.mount;
|
|
43
|
+
spec.mount = (ctx) => {
|
|
44
|
+
router.registerContainer(name, ctx.el, parent);
|
|
45
|
+
return userMount?.(ctx);
|
|
46
|
+
};
|
|
47
|
+
const userUnmount = spec.unmount;
|
|
48
|
+
spec.unmount = (ctx) => {
|
|
49
|
+
router.unregisterContainer(name, ctx.el);
|
|
50
|
+
return userUnmount?.(ctx);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
element(tag, spec, base);
|
|
54
|
+
gate(customElements.whenDefined(tag));
|
|
55
|
+
|
|
56
|
+
// Install the swap interface used by the orchestrator and cascade.
|
|
57
|
+
const Cls = customElements.get(tag);
|
|
58
|
+
if (Cls && !Cls.prototype.swap) {
|
|
59
|
+
Object.defineProperty(Cls.prototype, 'swap', { value: swap, configurable: true });
|
|
60
|
+
}
|
|
61
|
+
// Back-compat alias for the legacy orchestrator/container API.
|
|
62
|
+
if (Cls && !Cls.prototype.swapView) {
|
|
63
|
+
Object.defineProperty(Cls.prototype, 'swapView', { value: swap, configurable: true });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Replaces child content under a view transition. Concurrent-safe: an in-flight
|
|
69
|
+
* transition is skipped before a new one starts so rapid navigations don't
|
|
70
|
+
* leave a half-finished animation (RT bug 8.4).
|
|
71
|
+
*/
|
|
72
|
+
async function swap(el, options = {}) {
|
|
73
|
+
const { direction = 'push' } = options;
|
|
74
|
+
this.dataset.transitionDirection = direction;
|
|
75
|
+
|
|
76
|
+
const go = () => {
|
|
77
|
+
this.replaceChildren(el);
|
|
78
|
+
delete this.dataset.transitionDirection;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Abort any transition still running on this dock.
|
|
82
|
+
if (this._tx && typeof this._tx.skipTransition === 'function') {
|
|
83
|
+
try { this._tx.skipTransition(); } catch (_) {}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Read directional easing from tokens and temporarily override the root
|
|
87
|
+
// variable so the injected VT stylesheet picks it up.
|
|
88
|
+
let prior;
|
|
89
|
+
if (typeof document !== 'undefined') {
|
|
90
|
+
const root = document.documentElement;
|
|
91
|
+
const style = getComputedStyle(root);
|
|
92
|
+
const easing = direction === 'pop'
|
|
93
|
+
? style.getPropertyValue('--transition-pop').trim()
|
|
94
|
+
: style.getPropertyValue('--transition-push').trim();
|
|
95
|
+
if (easing) {
|
|
96
|
+
prior = style.getPropertyValue('--transition-easing').trim();
|
|
97
|
+
root.style.setProperty('--transition-easing', easing);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function restore() {
|
|
102
|
+
if (prior !== undefined && typeof document !== 'undefined') {
|
|
103
|
+
document.documentElement.style.setProperty('--transition-easing', prior);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (typeof this.startViewTransition === 'function') {
|
|
108
|
+
try {
|
|
109
|
+
this._tx = this.startViewTransition({ callback: go });
|
|
110
|
+
await this._tx.finished;
|
|
111
|
+
} catch (err) {
|
|
112
|
+
if (err?.name !== 'AbortError') console.warn('[Native UI] dock scoped VT aborted:', err);
|
|
113
|
+
} finally {
|
|
114
|
+
this._tx = null;
|
|
115
|
+
restore();
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (typeof document !== 'undefined' && typeof document.startViewTransition === 'function') {
|
|
121
|
+
try {
|
|
122
|
+
const vt = document.startViewTransition(go);
|
|
123
|
+
await vt.finished;
|
|
124
|
+
} catch (err) {
|
|
125
|
+
if (err?.name !== 'AbortError') console.warn('[Native UI] dock document VT aborted:', err);
|
|
126
|
+
} finally {
|
|
127
|
+
restore();
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
go();
|
|
133
|
+
restore();
|
|
134
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/ui/defs/index.js
|
|
3
|
+
*
|
|
4
|
+
* Definition layer facade. The four first-class UI primitives that supersede
|
|
5
|
+
* the split `ui.element` + `router.register` pattern and `<route-outlet>`.
|
|
6
|
+
*
|
|
7
|
+
* page — route-bound navigable unit
|
|
8
|
+
* dock — persistent container shell in the graph
|
|
9
|
+
* view — composable stateful component
|
|
10
|
+
* part — atomic stateless primitive
|
|
11
|
+
*
|
|
12
|
+
* Source: definations.md, tasks.md Phase 6
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { page } from './page.js';
|
|
16
|
+
import { dock } from './dock.js';
|
|
17
|
+
import { view } from './view.js';
|
|
18
|
+
import { part } from './part.js';
|
|
19
|
+
|
|
20
|
+
export { page, dock, view, part };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/ui/defs/page.js
|
|
3
|
+
*
|
|
4
|
+
* `page(route, config, base)` — a route-bound navigable unit. Maps a URL
|
|
5
|
+
* pattern to a custom element, declares the ordered container chain (`via`) the
|
|
6
|
+
* router traverses to reach the render target, and gates the boot sequence on
|
|
7
|
+
* the element's definition so a hard refresh waits for it.
|
|
8
|
+
*
|
|
9
|
+
* Source: definations.md §3, tasks.md Phase 6
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { router } from '../../router/index.js';
|
|
13
|
+
import { gate } from '../../router/boot.js';
|
|
14
|
+
import { element } from '../define/element.js';
|
|
15
|
+
import { specRegistry } from '../define/state.js';
|
|
16
|
+
import { translate } from './spec.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} route - URL pattern, e.g. '/profile/:id'.
|
|
20
|
+
* @param {object} config - page definition.
|
|
21
|
+
* @param {string} config.tag - custom element tag (must contain a hyphen).
|
|
22
|
+
* @param {string[]} [config.via] - ordered container chain, root-to-leaf.
|
|
23
|
+
* @param {string} [config.container] - single container (back-compat for via).
|
|
24
|
+
* @param {object} [config.props] - reactive props.
|
|
25
|
+
* @param {string[]} [config.query] - query params to map onto props.
|
|
26
|
+
* @param {Function} [config.guard] - route-scoped navigation guard.
|
|
27
|
+
* @param {object} [config.on] - lifecycle hooks (load, connect, disconnect, change).
|
|
28
|
+
* @param {string} [base] - import.meta.url of the caller (file templates).
|
|
29
|
+
*/
|
|
30
|
+
export function page(route, config, base) {
|
|
31
|
+
const tag = config.tag;
|
|
32
|
+
if (!tag) {
|
|
33
|
+
console.error(`[Native UI] page('${route}') is missing a 'tag'.`);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Normalise the container chain. The render target is the last container.
|
|
38
|
+
const via = Array.isArray(config.via) && config.via.length
|
|
39
|
+
? config.via
|
|
40
|
+
: (config.container ? [config.container] : []);
|
|
41
|
+
const target = via.at(-1) ?? null;
|
|
42
|
+
|
|
43
|
+
const spec = translate(config, { visual: true });
|
|
44
|
+
// Carry routing metadata on the spec so the orchestrator can resolve the
|
|
45
|
+
// render target and cast query params (specRegistry is populated by element()).
|
|
46
|
+
spec.via = via;
|
|
47
|
+
spec.container = target;
|
|
48
|
+
if (config.query) spec.query = config.query;
|
|
49
|
+
|
|
50
|
+
element(tag, spec, base);
|
|
51
|
+
|
|
52
|
+
// Register the route. Keep both `via` (full chain) and `container` (target)
|
|
53
|
+
// in meta so the interceptor cascade and orchestrator both work.
|
|
54
|
+
router.register(route, tag, {
|
|
55
|
+
...config.meta,
|
|
56
|
+
via,
|
|
57
|
+
container: target
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Hold the initial match until this element is defined (hard-refresh safety).
|
|
61
|
+
if (typeof customElements !== 'undefined') {
|
|
62
|
+
gate(customElements.whenDefined(tag));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Route-scoped guard: only runs when the destination matches this route.
|
|
66
|
+
if (typeof config.guard === 'function') {
|
|
67
|
+
registerGuard(route, config.guard);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Adds a global guard that delegates to `fn` only for matching destinations. */
|
|
72
|
+
function registerGuard(route, fn) {
|
|
73
|
+
let pattern = null;
|
|
74
|
+
const Pattern = typeof URLPattern !== 'undefined' ? URLPattern : null;
|
|
75
|
+
if (Pattern) {
|
|
76
|
+
try {
|
|
77
|
+
pattern = route.startsWith('http') ? new Pattern(route) : new Pattern({ pathname: route });
|
|
78
|
+
} catch (_) { pattern = null; }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
router.guard(async (destination, controller) => {
|
|
82
|
+
if (pattern) {
|
|
83
|
+
let url = destination?.url;
|
|
84
|
+
try { url = new URL(url, globalThis.location?.href).href; } catch (_) {}
|
|
85
|
+
if (!pattern.test(url)) return null; // not this route — allow
|
|
86
|
+
}
|
|
87
|
+
return fn(destination, controller);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/core/ui/defs/part.js
|
|
3
|
+
*
|
|
4
|
+
* `part(tag, config, base)` — an atomic, stateless UI primitive (buttons,
|
|
5
|
+
* icons, badges, chips). Configurable through props, but carries no reactive
|
|
6
|
+
* `on.change` re-render loop. If you reach for `on.change`, promote to a `view`.
|
|
7
|
+
*
|
|
8
|
+
* Source: definations.md §6, tasks.md Phase 6
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { element } from '../define/element.js';
|
|
12
|
+
import { translate } from './spec.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} tag - custom element tag name (must contain a hyphen).
|
|
16
|
+
* @param {object} config - part definition (props, template, optional on.connect).
|
|
17
|
+
* @param {string} [base] - import.meta.url of the caller; required when
|
|
18
|
+
* template/style are file paths.
|
|
19
|
+
*/
|
|
20
|
+
export function part(tag, config, base) {
|
|
21
|
+
if (config?.on?.change) {
|
|
22
|
+
console.warn(`[Native UI] <${tag}> is a 'part' but declares on.change. Parts are stateless — promote it to a 'view' if it needs reactive re-rendering.`);
|
|
23
|
+
}
|
|
24
|
+
const spec = translate(config);
|
|
25
|
+
// Parts never run a reactive update loop, even if on.change slipped through.
|
|
26
|
+
delete spec.update;
|
|
27
|
+
element(tag, spec, base);
|
|
28
|
+
}
|