@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.
- package/CHANGELOG.md +90 -4
- package/README.md +97 -133
- package/bin/anza/anza-linux-arm64 +0 -0
- package/bin/anza/anza-linux-x64 +0 -0
- package/bin/anza/anza-macos-arm64 +0 -0
- package/bin/anza/anza-macos-x64 +0 -0
- package/bin/anza/anza-windows-x64.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
|
@@ -0,0 +1,979 @@
|
|
|
1
|
+
# Definitions
|
|
2
|
+
|
|
3
|
+
Companion to `audit.md`. Supersedes the split `ui.element` + `router.register` pattern and the `<route-outlet>` element. Defines four first-class UI primitives — `page`, `dock`, `view`, `part` — their configuration schema, internal registration behaviour, lifecycle contracts, file conventions, and composition model.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Overview
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
┌──────────────────────────────────────────────────────────┐
|
|
11
|
+
│ Route-bound │
|
|
12
|
+
│ page('/path', config) ← has URL, renders into a dock │
|
|
13
|
+
└──────────────────────────────────────────────────────────┘
|
|
14
|
+
┌──────────────────────────────────────────────────────────┐
|
|
15
|
+
│ Router-managed │
|
|
16
|
+
│ dock('name', config) ← container shell in graph │
|
|
17
|
+
└──────────────────────────────────────────────────────────┘
|
|
18
|
+
┌──────────────────────────────────────────────────────────┐
|
|
19
|
+
│ Pure Web Components │
|
|
20
|
+
│ view('tag', config) ← stateful, composable unit │
|
|
21
|
+
│ part('tag', config) ← atomic, stateless primitive │
|
|
22
|
+
└──────────────────────────────────────────────────────────┘
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
| Function | Route | In graph | Swap method | Template | Composable |
|
|
26
|
+
|----------|-------|----------|-------------|----------|------------|
|
|
27
|
+
| `page` | Yes | No | No | Required | Yes |
|
|
28
|
+
| `dock` | No | Yes | Yes | Optional | Yes |
|
|
29
|
+
| `view` | No | No | No | Required | Yes |
|
|
30
|
+
| `part` | No | No | No | Required | Yes |
|
|
31
|
+
|
|
32
|
+
All four call `customElements.define` internally. All four produce standard Custom Elements — you use them by their tag name in any HTML template.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 2. Import
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
import { page, dock, view, part } from './defs/index.js'
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Or individually:
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
import { page } from './defs/page.js'
|
|
46
|
+
import { dock } from './defs/dock.js'
|
|
47
|
+
import { view } from './defs/view.js'
|
|
48
|
+
import { part } from './defs/part.js'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 3. `page`
|
|
54
|
+
|
|
55
|
+
### Page Overview
|
|
56
|
+
|
|
57
|
+
A `page` is a route-bound navigable unit. It maps a URL pattern to a Custom Element tag, declares the ordered container chain the router must traverse to reach the render target, and defines how the element receives route parameters.
|
|
58
|
+
|
|
59
|
+
A `page` has no persistent presence in the container graph. It is mounted into the last dock in its `via` chain, remains live for the duration of the route, and is removed when the route changes.
|
|
60
|
+
|
|
61
|
+
A `page` element can use `view` and `part` elements freely in its template.
|
|
62
|
+
|
|
63
|
+
### Page Signature
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
page(route, config)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
| Argument | Type | Required | Description |
|
|
70
|
+
|----------|--------|----------|--------------------------|
|
|
71
|
+
| `route` | string | Yes | URLPattern pathname |
|
|
72
|
+
| `config` | object | Yes | See schema below |
|
|
73
|
+
|
|
74
|
+
### Page Schema
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
page('/settings/profile/:id', {
|
|
78
|
+
|
|
79
|
+
// ── Identity ────────────────────────────────────────────
|
|
80
|
+
tag: 'page-settings-profile', // required. Custom Element tag. Must contain '-'.
|
|
81
|
+
|
|
82
|
+
// ── Container chain ─────────────────────────────────────
|
|
83
|
+
via: ['main', 'sidebar', 'settings-panel'],
|
|
84
|
+
// Ordered array of dock names, root to leaf.
|
|
85
|
+
// The last entry is the render target.
|
|
86
|
+
// Docks not yet in the DOM are mounted sequentially by cascade.js.
|
|
87
|
+
// Backward-compatible: a single string is treated as via: [string].
|
|
88
|
+
|
|
89
|
+
// ── Template ────────────────────────────────────────────
|
|
90
|
+
// Inline form — template strings directly in the config:
|
|
91
|
+
template: {
|
|
92
|
+
html: `
|
|
93
|
+
<slot name="header"></slot>
|
|
94
|
+
<slot></slot>
|
|
95
|
+
`,
|
|
96
|
+
css: `:host { display: block; padding: 1rem; }`,
|
|
97
|
+
shadow: 'open'
|
|
98
|
+
// shadow: 'open' | 'closed' | false
|
|
99
|
+
// false = light DOM — useful when parent CSS must pierce the element.
|
|
100
|
+
},
|
|
101
|
+
// File form — native .html and .css file paths:
|
|
102
|
+
// template: {
|
|
103
|
+
// html: './template.html',
|
|
104
|
+
// css: './style.css',
|
|
105
|
+
// shadow: 'open'
|
|
106
|
+
// },
|
|
107
|
+
// File paths are resolved relative to the calling module's import.meta.url.
|
|
108
|
+
// Using native files gives you full IDE completion, syntax highlighting,
|
|
109
|
+
// Emmet, linting, and the browser's native CSS/HTML parsers.
|
|
110
|
+
|
|
111
|
+
// ── Route parameter typing ───────────────────────────────
|
|
112
|
+
props: {
|
|
113
|
+
id: { type: Number }, // /settings/profile/:id → element.id
|
|
114
|
+
tab: { type: String }, // from query string → element.tab
|
|
115
|
+
open: { type: Boolean } // from query string → element.open
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
// ── Query params to promote as props ────────────────────
|
|
119
|
+
query: ['tab', 'open'],
|
|
120
|
+
// Named query keys from the URL that are cast and set as element properties.
|
|
121
|
+
// Casting follows the type declared in props above.
|
|
122
|
+
|
|
123
|
+
// ── Route guard ─────────────────────────────────────────
|
|
124
|
+
guard({ params, query }) {
|
|
125
|
+
if (!auth.check()) return '/login' // return string → redirect
|
|
126
|
+
return true // return true → proceed
|
|
127
|
+
// return undefined / void → → proceed
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
// ── Lifecycle ────────────────────────────────────────────
|
|
131
|
+
on: {
|
|
132
|
+
load({ params, query, hash, state }) {
|
|
133
|
+
// Called before the element is swapped into the dock.
|
|
134
|
+
// 'this' is the element instance.
|
|
135
|
+
// Returning a rejected Promise aborts the navigation.
|
|
136
|
+
this.userId = params.id
|
|
137
|
+
},
|
|
138
|
+
unload() {
|
|
139
|
+
// Called before the element is removed on route change.
|
|
140
|
+
// 'this' is the element instance.
|
|
141
|
+
this.cleanup?.()
|
|
142
|
+
},
|
|
143
|
+
connect() {
|
|
144
|
+
// connectedCallback — element is in the DOM.
|
|
145
|
+
},
|
|
146
|
+
disconnect() {
|
|
147
|
+
// disconnectedCallback — element removed from DOM.
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
})
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Page Registration
|
|
155
|
+
|
|
156
|
+
`page()` performs all of the following — the developer calls nothing else:
|
|
157
|
+
|
|
158
|
+
1. Defines an `HTMLElement` subclass using the `template` config.
|
|
159
|
+
2. Calls `customElements.define(config.tag, cls)`.
|
|
160
|
+
3. Calls `router.register(route, config.tag, { via: config.via, props: config.props, query: config.query })`.
|
|
161
|
+
4. Populates `specRegistry` with `{ props, query }` so the cascade can cast typed parameters.
|
|
162
|
+
5. Calls `gate(customElements.whenDefined(config.tag))` on the boot gate so the router's initial match waits for this element to be ready.
|
|
163
|
+
|
|
164
|
+
### Page Example — inline
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
// pages/profile.js
|
|
168
|
+
import { page } from '../defs/index.js'
|
|
169
|
+
|
|
170
|
+
page('/profile/:id', {
|
|
171
|
+
tag: 'page-profile',
|
|
172
|
+
via: ['main'],
|
|
173
|
+
template: {
|
|
174
|
+
html: `<h1>Profile</h1><slot></slot>`,
|
|
175
|
+
css: `:host { display: grid; gap: 1rem; }`
|
|
176
|
+
},
|
|
177
|
+
props: {
|
|
178
|
+
id: { type: Number }
|
|
179
|
+
},
|
|
180
|
+
on: {
|
|
181
|
+
load({ params }) {
|
|
182
|
+
this.id = params.id
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Page Example — folder
|
|
189
|
+
|
|
190
|
+
Large pages with rich templates, complex loaders, or many sub-components use a folder. The folder name is the page slug.
|
|
191
|
+
|
|
192
|
+
```text
|
|
193
|
+
pages/
|
|
194
|
+
profile/
|
|
195
|
+
index.js ← page() call, imports siblings
|
|
196
|
+
template.html ← native HTML template
|
|
197
|
+
style.css ← native CSS stylesheet
|
|
198
|
+
load.js ← exports load handler
|
|
199
|
+
guard.js ← exports guard function
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
```html
|
|
203
|
+
<!-- pages/profile/template.html -->
|
|
204
|
+
<slot name="header"></slot>
|
|
205
|
+
<section class="body">
|
|
206
|
+
<slot></slot>
|
|
207
|
+
</section>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
```css
|
|
211
|
+
/* pages/profile/style.css */
|
|
212
|
+
:host {
|
|
213
|
+
display: grid;
|
|
214
|
+
grid-template-rows: auto 1fr;
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
```js
|
|
219
|
+
// pages/profile/load.js
|
|
220
|
+
export async function load({ params }) {
|
|
221
|
+
const res = await fetch('/api/users/' + params.id)
|
|
222
|
+
this.data = await res.json()
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
```js
|
|
227
|
+
// pages/profile/index.js
|
|
228
|
+
import { page } from '../../defs/index.js'
|
|
229
|
+
import { load } from './load.js'
|
|
230
|
+
import { guard } from './guard.js'
|
|
231
|
+
|
|
232
|
+
page('/profile/:id', {
|
|
233
|
+
tag: 'page-profile',
|
|
234
|
+
via: ['main', 'content'],
|
|
235
|
+
template: {
|
|
236
|
+
html: './template.html',
|
|
237
|
+
css: './style.css'
|
|
238
|
+
},
|
|
239
|
+
props: { id: { type: Number } },
|
|
240
|
+
query: ['tab'],
|
|
241
|
+
guard,
|
|
242
|
+
on: { load }
|
|
243
|
+
}, import.meta.url)
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
The third argument `import.meta.url` is required when using file paths — it provides the base URL for resolving relative paths. When using inline strings the argument is optional.
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## 4. `dock`
|
|
251
|
+
|
|
252
|
+
### Dock Overview
|
|
253
|
+
|
|
254
|
+
A `dock` is a persistent container shell. It lives in the DOM across route changes, receives swapped child content from the router, and registers itself in the container graph the moment it connects.
|
|
255
|
+
|
|
256
|
+
A `dock` is the replacement for `<route-outlet>`. Unlike the old outlet, a dock:
|
|
257
|
+
|
|
258
|
+
- Registers its own position in the hierarchical graph (via `graph.add`).
|
|
259
|
+
- Declares its parent dock, enabling LCA traversal and cascade mounting.
|
|
260
|
+
- Exposes a `swap` method the router calls to replace child content with a view transition.
|
|
261
|
+
- Automatically unregisters on `disconnectedCallback`.
|
|
262
|
+
|
|
263
|
+
A `dock` element can contain `view` and `part` elements as persistent chrome (navigation bars, sidebars, headers) alongside the `<slot>` where the router injects page content.
|
|
264
|
+
|
|
265
|
+
### Dock Signature
|
|
266
|
+
|
|
267
|
+
```text
|
|
268
|
+
dock(name, config)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
| Argument | Type | Required | Description |
|
|
272
|
+
|----------|--------|----------|--------------------------------------|
|
|
273
|
+
| `name` | string | Yes | Key in the container graph |
|
|
274
|
+
| `config` | object | Yes | See schema below |
|
|
275
|
+
|
|
276
|
+
### Dock Schema
|
|
277
|
+
|
|
278
|
+
```js
|
|
279
|
+
dock('sidebar', {
|
|
280
|
+
|
|
281
|
+
// ── Identity ─────────────────────────────────────────────
|
|
282
|
+
tag: 'dock-sidebar',
|
|
283
|
+
// Optional. Defaults to 'dock-{name}' if omitted.
|
|
284
|
+
// Must contain '-'. The tag is how the element appears in HTML.
|
|
285
|
+
|
|
286
|
+
// ── Graph position ───────────────────────────────────────
|
|
287
|
+
parent: 'main',
|
|
288
|
+
// The dock name of the parent container.
|
|
289
|
+
// Defaults to 'body' if omitted.
|
|
290
|
+
// Used by graph.js for LCA traversal and cascade ordering.
|
|
291
|
+
|
|
292
|
+
// ── Template ─────────────────────────────────────────────
|
|
293
|
+
template: {
|
|
294
|
+
html: `
|
|
295
|
+
<nav>
|
|
296
|
+
<slot name="nav"></slot>
|
|
297
|
+
</nav>
|
|
298
|
+
<slot></slot>
|
|
299
|
+
`,
|
|
300
|
+
css: `:host { display: flex; flex-direction: column; }`,
|
|
301
|
+
shadow: 'open'
|
|
302
|
+
},
|
|
303
|
+
// If template is omitted entirely, the dock renders as a transparent
|
|
304
|
+
// passthrough: shadow: false, html: '<slot></slot>'.
|
|
305
|
+
|
|
306
|
+
// ── Lifecycle ────────────────────────────────────────────
|
|
307
|
+
on: {
|
|
308
|
+
connect() {
|
|
309
|
+
// Fires after graph.add() has been called.
|
|
310
|
+
// 'this' is the element instance.
|
|
311
|
+
},
|
|
312
|
+
disconnect() {
|
|
313
|
+
// Fires before graph.remove() is called.
|
|
314
|
+
},
|
|
315
|
+
swap(el, { direction }) {
|
|
316
|
+
// Override the default swap behaviour.
|
|
317
|
+
// Called by cascade.js when mounting a new page or dock into this container.
|
|
318
|
+
// Must return a Promise that resolves when the transition is complete.
|
|
319
|
+
// Default implementation (used when swap is not declared):
|
|
320
|
+
//
|
|
321
|
+
// if (this._tx) this._tx.skipTransition()
|
|
322
|
+
// const go = () => this.replaceChildren(el)
|
|
323
|
+
// if (typeof this.startViewTransition === 'function') {
|
|
324
|
+
// this._tx = this.startViewTransition({ callback: go })
|
|
325
|
+
// await this._tx.finished
|
|
326
|
+
// this._tx = null
|
|
327
|
+
// } else {
|
|
328
|
+
// go()
|
|
329
|
+
// }
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
})
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
### Dock Registration
|
|
337
|
+
|
|
338
|
+
`dock()` performs the following internally:
|
|
339
|
+
|
|
340
|
+
1. Defines an `HTMLElement` subclass.
|
|
341
|
+
2. On `connectedCallback`: calls `graph.add(name, el, config.parent ?? 'body')`.
|
|
342
|
+
3. On `disconnectedCallback`: calls `graph.remove(name)`.
|
|
343
|
+
4. Attaches the `swap` method — either the override from `on.swap` or the default view-transition implementation.
|
|
344
|
+
5. Calls `customElements.define(config.tag ?? 'dock-' + name, cls)`.
|
|
345
|
+
6. Calls `gate(customElements.whenDefined(tag))` on the boot gate.
|
|
346
|
+
|
|
347
|
+
The dock does **not** call `router.register`. It has no URL. The router discovers it through the container graph.
|
|
348
|
+
|
|
349
|
+
### Dock Example
|
|
350
|
+
|
|
351
|
+
```js
|
|
352
|
+
// docks/main.js
|
|
353
|
+
import { dock } from '../defs/index.js'
|
|
354
|
+
|
|
355
|
+
dock('main', {
|
|
356
|
+
parent: 'body',
|
|
357
|
+
template: {
|
|
358
|
+
html: `
|
|
359
|
+
<header><slot name="header"></slot></header>
|
|
360
|
+
<main><slot></slot></main>
|
|
361
|
+
`,
|
|
362
|
+
css: `:host { display: grid; grid-template-rows: auto 1fr; min-height: 100vh; }`
|
|
363
|
+
}
|
|
364
|
+
})
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
```js
|
|
368
|
+
// docks/sidebar.js
|
|
369
|
+
import { dock } from '../defs/index.js'
|
|
370
|
+
|
|
371
|
+
dock('sidebar', {
|
|
372
|
+
parent: 'main',
|
|
373
|
+
template: {
|
|
374
|
+
html: `<aside><slot></slot></aside>`,
|
|
375
|
+
css: `:host { display: block; width: 240px; }`
|
|
376
|
+
},
|
|
377
|
+
on: {
|
|
378
|
+
async swap(el, { direction }) {
|
|
379
|
+
const anim = this.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 150 })
|
|
380
|
+
await anim.finished
|
|
381
|
+
this.replaceChildren(el)
|
|
382
|
+
this.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 150 })
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
})
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
### Placing a dock in HTML
|
|
389
|
+
|
|
390
|
+
A dock tag placed directly in HTML is discovered the moment it connects:
|
|
391
|
+
|
|
392
|
+
```html
|
|
393
|
+
<body>
|
|
394
|
+
<dock-main>
|
|
395
|
+
<dock-sidebar slot="sidebar"></dock-sidebar>
|
|
396
|
+
</dock-main>
|
|
397
|
+
</body>
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
A dock can also be placed inside a page's template — it will mount and register itself as a child of whatever dock the page is rendered in. This is how sub-layouts work.
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## 5. `view`
|
|
405
|
+
|
|
406
|
+
### View Overview
|
|
407
|
+
|
|
408
|
+
A `view` is a composable, stateful UI unit with no route and no container graph presence. It is a pure Web Component — the router never touches it directly.
|
|
409
|
+
|
|
410
|
+
A `view` is reused across pages, placed in dock chrome, used inside other views, or composed into part templates. It has observed properties, a full Custom Element lifecycle, and its own encapsulated shadow DOM.
|
|
411
|
+
|
|
412
|
+
Relationship to `page`: a `page` is a `view` that also has a URL. If you find yourself wanting to add a route to a `view`, promote it to a `page`.
|
|
413
|
+
|
|
414
|
+
### View Signature
|
|
415
|
+
|
|
416
|
+
```text
|
|
417
|
+
view(tag, config)
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
| Argument | Type | Required | Description |
|
|
421
|
+
|----------|--------|----------|------------------------------|
|
|
422
|
+
| `tag` | string | Yes | Custom Element tag name |
|
|
423
|
+
| `config` | object | Yes | See schema below |
|
|
424
|
+
|
|
425
|
+
### View Schema
|
|
426
|
+
|
|
427
|
+
```js
|
|
428
|
+
view('user-card', {
|
|
429
|
+
|
|
430
|
+
// ── Observed properties ───────────────────────────────────
|
|
431
|
+
props: {
|
|
432
|
+
name: { type: String, default: '' },
|
|
433
|
+
avatar: { type: String, default: '' },
|
|
434
|
+
count: { type: Number, default: 0 },
|
|
435
|
+
active: { type: Boolean, default: false }
|
|
436
|
+
},
|
|
437
|
+
// Each entry becomes an observed attribute AND a reflected property.
|
|
438
|
+
// On attribute change the value is cast to the declared type before
|
|
439
|
+
// attributeChangedCallback fires.
|
|
440
|
+
// 'default' is the initial value assigned in the constructor.
|
|
441
|
+
|
|
442
|
+
// ── Template ─────────────────────────────────────────────
|
|
443
|
+
template: {
|
|
444
|
+
html: `
|
|
445
|
+
<img part="avatar" alt="">
|
|
446
|
+
<span part="name"></span>
|
|
447
|
+
<slot></slot>
|
|
448
|
+
`,
|
|
449
|
+
css: `
|
|
450
|
+
:host { display: flex; align-items: center; gap: .5rem; }
|
|
451
|
+
[part="avatar"] { width: 2rem; height: 2rem; border-radius: 50%; }
|
|
452
|
+
`,
|
|
453
|
+
shadow: 'open'
|
|
454
|
+
},
|
|
455
|
+
|
|
456
|
+
// ── Lifecycle ────────────────────────────────────────────
|
|
457
|
+
on: {
|
|
458
|
+
connect() {
|
|
459
|
+
// connectedCallback.
|
|
460
|
+
},
|
|
461
|
+
disconnect() {
|
|
462
|
+
// disconnectedCallback.
|
|
463
|
+
},
|
|
464
|
+
change(attr, prev, next) {
|
|
465
|
+
// attributeChangedCallback.
|
|
466
|
+
// 'attr' is the attribute name (kebab-case).
|
|
467
|
+
// 'next' is already cast to the declared prop type.
|
|
468
|
+
if (attr === 'avatar') this.shadowRoot.querySelector('[part="avatar"]').src = next
|
|
469
|
+
if (attr === 'name') this.shadowRoot.querySelector('[part="name"]').textContent = next
|
|
470
|
+
},
|
|
471
|
+
adopt() {
|
|
472
|
+
// adoptedCallback — element moved to a new document.
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
})
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### View Registration
|
|
480
|
+
|
|
481
|
+
`view()` performs the following internally:
|
|
482
|
+
|
|
483
|
+
1. Collects `static get observedAttributes()` from the `props` keys.
|
|
484
|
+
2. Defines the `HTMLElement` subclass with casting inside `attributeChangedCallback`.
|
|
485
|
+
3. Calls `customElements.define(tag, cls)`.
|
|
486
|
+
4. Does **not** interact with the router, boot gate, or container graph.
|
|
487
|
+
|
|
488
|
+
### View Example — inline
|
|
489
|
+
|
|
490
|
+
```js
|
|
491
|
+
// views/stat-card.js
|
|
492
|
+
import { view } from '../defs/index.js'
|
|
493
|
+
|
|
494
|
+
view('stat-card', {
|
|
495
|
+
props: {
|
|
496
|
+
label: { type: String, default: '' },
|
|
497
|
+
value: { type: Number, default: 0 }
|
|
498
|
+
},
|
|
499
|
+
template: {
|
|
500
|
+
html: `<dt part="label"></dt><dd part="value"></dd>`,
|
|
501
|
+
css: `:host { display: contents; } [part="value"] { font-size: 2rem; font-weight: 700; }`
|
|
502
|
+
},
|
|
503
|
+
on: {
|
|
504
|
+
change(attr, _, next) {
|
|
505
|
+
this.shadowRoot.querySelector('[part="' + attr + '"]').textContent = next
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
})
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
### View Example — folder
|
|
512
|
+
|
|
513
|
+
```text
|
|
514
|
+
views/
|
|
515
|
+
data-table/
|
|
516
|
+
index.js ← view() call
|
|
517
|
+
template.html ← native HTML template
|
|
518
|
+
style.css ← native CSS stylesheet
|
|
519
|
+
sort.js ← sort logic used inside on.connect
|
|
520
|
+
filter.js ← filter logic
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
```js
|
|
524
|
+
// views/data-table/index.js
|
|
525
|
+
import { view } from '../../defs/index.js'
|
|
526
|
+
import { sort } from './sort.js'
|
|
527
|
+
import { filter } from './filter.js'
|
|
528
|
+
|
|
529
|
+
view('data-table', {
|
|
530
|
+
props: {
|
|
531
|
+
rows: { type: String, default: '[]' }, // JSON string
|
|
532
|
+
sortby: { type: String, default: '' },
|
|
533
|
+
filter: { type: String, default: '' }
|
|
534
|
+
},
|
|
535
|
+
template: {
|
|
536
|
+
html: './template.html',
|
|
537
|
+
css: './style.css'
|
|
538
|
+
},
|
|
539
|
+
on: {
|
|
540
|
+
connect() { this.render() },
|
|
541
|
+
change() { this.render() },
|
|
542
|
+
render() {
|
|
543
|
+
const rows = JSON.parse(this.rows)
|
|
544
|
+
const sorted = sort(rows, this.sortby)
|
|
545
|
+
const filtered = filter(sorted, this.filter)
|
|
546
|
+
// ... update DOM
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
}, import.meta.url)
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
|
|
554
|
+
## 6. `part`
|
|
555
|
+
|
|
556
|
+
### Part Overview
|
|
557
|
+
|
|
558
|
+
A `part` is an atomic, stateless UI primitive. It is the smallest reusable unit — buttons, icons, badges, chips, inputs, labels. It may declare props for configuration (variant, size, disabled) but carries no internal state beyond what the native element provides.
|
|
559
|
+
|
|
560
|
+
A `part` differs from a `view` in that it does not own data, does not fetch, does not manage layout, and its lifecycle is minimal. If you find yourself adding `on.change` logic beyond updating a CSS class or an attribute reflection, promote it to a `view`.
|
|
561
|
+
|
|
562
|
+
### Part Signature
|
|
563
|
+
|
|
564
|
+
```text
|
|
565
|
+
part(tag, config)
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### Part Schema
|
|
569
|
+
|
|
570
|
+
```js
|
|
571
|
+
part('icon-btn', {
|
|
572
|
+
|
|
573
|
+
// ── Props (optional) ──────────────────────────────────────
|
|
574
|
+
props: {
|
|
575
|
+
variant: { type: String, default: 'default' },
|
|
576
|
+
size: { type: String, default: 'md' },
|
|
577
|
+
disabled: { type: Boolean, default: false }
|
|
578
|
+
},
|
|
579
|
+
|
|
580
|
+
// ── Template ─────────────────────────────────────────────
|
|
581
|
+
template: {
|
|
582
|
+
html: `
|
|
583
|
+
<button part="btn">
|
|
584
|
+
<slot name="icon"></slot>
|
|
585
|
+
<slot></slot>
|
|
586
|
+
</button>
|
|
587
|
+
`,
|
|
588
|
+
css: `
|
|
589
|
+
:host { display: inline-flex; }
|
|
590
|
+
:host([disabled]) [part="btn"] { pointer-events: none; opacity: .4; }
|
|
591
|
+
`,
|
|
592
|
+
shadow: 'open'
|
|
593
|
+
},
|
|
594
|
+
|
|
595
|
+
// ── Lifecycle (minimal, optional) ────────────────────────
|
|
596
|
+
on: {
|
|
597
|
+
connect() {
|
|
598
|
+
this.shadowRoot.querySelector('[part="btn"]').disabled = this.disabled
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
})
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
### Part Registration
|
|
606
|
+
|
|
607
|
+
`part()` performs the following internally:
|
|
608
|
+
|
|
609
|
+
1. Defines the `HTMLElement` subclass.
|
|
610
|
+
2. Calls `customElements.define(tag, cls)`.
|
|
611
|
+
3. Does **not** interact with the router, boot gate, or container graph.
|
|
612
|
+
4. Observed attributes are derived from `props` keys, same casting rules as `view`.
|
|
613
|
+
|
|
614
|
+
### Part Example
|
|
615
|
+
|
|
616
|
+
```js
|
|
617
|
+
// parts/badge.js
|
|
618
|
+
import { part } from '../defs/index.js'
|
|
619
|
+
|
|
620
|
+
part('status-badge', {
|
|
621
|
+
props: {
|
|
622
|
+
state: { type: String, default: 'idle' }
|
|
623
|
+
},
|
|
624
|
+
template: {
|
|
625
|
+
html: `<span part="dot"></span><slot></slot>`,
|
|
626
|
+
css: `
|
|
627
|
+
:host { display: inline-flex; align-items: center; gap: .25rem; }
|
|
628
|
+
[part="dot"] { width: .5rem; height: .5rem; border-radius: 50%; background: currentColor; }
|
|
629
|
+
:host([state="live"]) { color: #16a34a; }
|
|
630
|
+
:host([state="error"]) { color: #dc2626; }
|
|
631
|
+
`
|
|
632
|
+
}
|
|
633
|
+
})
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
---
|
|
637
|
+
|
|
638
|
+
## 7. Composition
|
|
639
|
+
|
|
640
|
+
All four primitives produce Custom Elements. They compose by using each other's tags in HTML templates.
|
|
641
|
+
|
|
642
|
+
### view inside page
|
|
643
|
+
|
|
644
|
+
```js
|
|
645
|
+
// Import the view so it is defined before the page renders.
|
|
646
|
+
import '../views/stat-card.js'
|
|
647
|
+
import '../views/user-card.js'
|
|
648
|
+
import { page } from '../defs/index.js'
|
|
649
|
+
|
|
650
|
+
page('/dashboard', {
|
|
651
|
+
tag: 'page-dashboard',
|
|
652
|
+
via: ['main'],
|
|
653
|
+
template: {
|
|
654
|
+
html: `
|
|
655
|
+
<section class="stats">
|
|
656
|
+
<stat-card label="Users" value="0"></stat-card>
|
|
657
|
+
<stat-card label="Revenue" value="0"></stat-card>
|
|
658
|
+
</section>
|
|
659
|
+
<user-card></user-card>
|
|
660
|
+
`,
|
|
661
|
+
css: `:host { display: grid; gap: 1rem; }`
|
|
662
|
+
}
|
|
663
|
+
})
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
Import order is the only requirement. Because all definitions call `customElements.define`, the browser upgrades the tags as soon as they appear in the shadow DOM.
|
|
667
|
+
|
|
668
|
+
### part inside view
|
|
669
|
+
|
|
670
|
+
```js
|
|
671
|
+
import '../parts/icon-btn.js'
|
|
672
|
+
import { view } from '../defs/index.js'
|
|
673
|
+
|
|
674
|
+
view('action-bar', {
|
|
675
|
+
template: {
|
|
676
|
+
html: `
|
|
677
|
+
<icon-btn variant="ghost"><slot name="icon" slot="icon"></slot>Edit</icon-btn>
|
|
678
|
+
<icon-btn variant="danger">Delete</icon-btn>
|
|
679
|
+
`,
|
|
680
|
+
css: `:host { display: flex; gap: .5rem; }`
|
|
681
|
+
}
|
|
682
|
+
})
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
### dock inside page (sub-layout)
|
|
686
|
+
|
|
687
|
+
A page can declare a dock in its template to introduce a new layout region for its child routes.
|
|
688
|
+
|
|
689
|
+
```js
|
|
690
|
+
import '../docks/tab-panel.js' // defines dock('tab-panel', { parent: 'content' })
|
|
691
|
+
import { page } from '../defs/index.js'
|
|
692
|
+
|
|
693
|
+
page('/settings', {
|
|
694
|
+
tag: 'page-settings',
|
|
695
|
+
via: ['main'],
|
|
696
|
+
template: {
|
|
697
|
+
html: `
|
|
698
|
+
<nav>
|
|
699
|
+
<a href="/settings/profile">Profile</a>
|
|
700
|
+
<a href="/settings/security">Security</a>
|
|
701
|
+
</nav>
|
|
702
|
+
<dock-tab-panel></dock-tab-panel>
|
|
703
|
+
`,
|
|
704
|
+
css: `:host { display: grid; grid-template-rows: auto 1fr; }`
|
|
705
|
+
}
|
|
706
|
+
})
|
|
707
|
+
|
|
708
|
+
// Child routes declare the sub-dock in via:
|
|
709
|
+
page('/settings/profile', {
|
|
710
|
+
tag: 'page-settings-profile',
|
|
711
|
+
via: ['main', 'tab-panel'],
|
|
712
|
+
template: { html: `<slot></slot>`, css: `:host { display: block; }` }
|
|
713
|
+
})
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
When navigating to `/settings/profile` from an unrelated route:
|
|
717
|
+
|
|
718
|
+
1. `cascade.js` mounts `dock-main` (if absent).
|
|
719
|
+
2. `cascade.js` mounts `page-settings` into `main` (if absent).
|
|
720
|
+
3. `dock-tab-panel` — declared inside `page-settings`'s template — self-registers on `connectedCallback`.
|
|
721
|
+
4. `cascade.js` mounts `page-settings-profile` into `tab-panel`.
|
|
722
|
+
|
|
723
|
+
### page inside dock — persistent chrome
|
|
724
|
+
|
|
725
|
+
Docks can hold permanent `view` or `part` content in named slots alongside the router's swap target slot:
|
|
726
|
+
|
|
727
|
+
```js
|
|
728
|
+
dock('main', {
|
|
729
|
+
template: {
|
|
730
|
+
html: `
|
|
731
|
+
<view-topbar slot="header"></view-topbar>
|
|
732
|
+
<slot></slot> <!-- router injects here -->
|
|
733
|
+
`,
|
|
734
|
+
css: `:host { display: grid; grid-template-rows: auto 1fr; }`
|
|
735
|
+
}
|
|
736
|
+
})
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
`view-topbar` is rendered once when the dock connects and persists across all navigations through `main`.
|
|
740
|
+
|
|
741
|
+
---
|
|
742
|
+
|
|
743
|
+
## 8. File Conventions
|
|
744
|
+
|
|
745
|
+
### Inline (single file)
|
|
746
|
+
|
|
747
|
+
Suitable when the template, style, and logic are short and cohesive.
|
|
748
|
+
|
|
749
|
+
```text
|
|
750
|
+
src/
|
|
751
|
+
docks/
|
|
752
|
+
main.js
|
|
753
|
+
sidebar.js
|
|
754
|
+
pages/
|
|
755
|
+
home.js
|
|
756
|
+
profile.js
|
|
757
|
+
views/
|
|
758
|
+
user-card.js
|
|
759
|
+
data-table.js
|
|
760
|
+
parts/
|
|
761
|
+
icon-btn.js
|
|
762
|
+
status-badge.js
|
|
763
|
+
defs/
|
|
764
|
+
page.js
|
|
765
|
+
dock.js
|
|
766
|
+
view.js
|
|
767
|
+
part.js
|
|
768
|
+
index.js
|
|
769
|
+
```
|
|
770
|
+
|
|
771
|
+
File name = the element's untagged slug. `page-profile` lives in `pages/profile.js`. `dock-sidebar` lives in `docks/sidebar.js`. `view-user-card` lives in `views/user-card.js`.
|
|
772
|
+
|
|
773
|
+
### Folder-based
|
|
774
|
+
|
|
775
|
+
When a definition grows — complex template, multiple helpers, separate load/guard logic — promote it to a folder. The folder name stays the slug.
|
|
776
|
+
|
|
777
|
+
```text
|
|
778
|
+
pages/
|
|
779
|
+
profile/
|
|
780
|
+
index.js ← page() call, the only entry point
|
|
781
|
+
template.html ← native HTML template (IDE completion, Emmet, linting)
|
|
782
|
+
style.css ← native CSS stylesheet (IDE completion, linting)
|
|
783
|
+
load.js ← named export: load handler
|
|
784
|
+
guard.js ← named export: guard function
|
|
785
|
+
|
|
786
|
+
views/
|
|
787
|
+
data-table/
|
|
788
|
+
index.js
|
|
789
|
+
template.html
|
|
790
|
+
style.css
|
|
791
|
+
sort.js
|
|
792
|
+
filter.js
|
|
793
|
+
|
|
794
|
+
docks/
|
|
795
|
+
sidebar/
|
|
796
|
+
index.js
|
|
797
|
+
template.html
|
|
798
|
+
style.css
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
Template and style files are plain native files — no JS wrappers, no export boilerplate. The browser's native parsers handle them directly. IDEs provide full syntax highlighting, autocompletion, and linting out of the box.
|
|
802
|
+
|
|
803
|
+
```html
|
|
804
|
+
<!-- pages/profile/template.html -->
|
|
805
|
+
<header part="head"><slot name="header"></slot></header>
|
|
806
|
+
<section part="body"><slot></slot></section>
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
```css
|
|
810
|
+
/* pages/profile/style.css */
|
|
811
|
+
:host {
|
|
812
|
+
display: grid;
|
|
813
|
+
grid-template-rows: auto 1fr;
|
|
814
|
+
}
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
Helper files export named functions used in lifecycle hooks:
|
|
818
|
+
|
|
819
|
+
```js
|
|
820
|
+
// pages/profile/load.js
|
|
821
|
+
export async function load({ params }) {
|
|
822
|
+
const res = await fetch('/api/profile/' + params.id)
|
|
823
|
+
this.model = await res.json()
|
|
824
|
+
}
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
The `index.js` imports helpers and points to native files:
|
|
828
|
+
|
|
829
|
+
```js
|
|
830
|
+
// pages/profile/index.js
|
|
831
|
+
import { page } from '../../defs/index.js'
|
|
832
|
+
import { load } from './load.js'
|
|
833
|
+
import { guard } from './guard.js'
|
|
834
|
+
|
|
835
|
+
page('/profile/:id', {
|
|
836
|
+
tag: 'page-profile',
|
|
837
|
+
via: ['main', 'content'],
|
|
838
|
+
template: {
|
|
839
|
+
html: './template.html',
|
|
840
|
+
css: './style.css'
|
|
841
|
+
},
|
|
842
|
+
props: { id: { type: Number } },
|
|
843
|
+
query: ['tab'],
|
|
844
|
+
guard,
|
|
845
|
+
on: { load }
|
|
846
|
+
}, import.meta.url)
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
The third argument `import.meta.url` is required when template or style values are file paths. It tells the framework how to resolve relative paths. When using inline strings it can be omitted.
|
|
850
|
+
|
|
851
|
+
---
|
|
852
|
+
|
|
853
|
+
## 9. Tag Prefixes
|
|
854
|
+
|
|
855
|
+
Custom Element tags must contain a hyphen. Use these prefixes for instant visual identification:
|
|
856
|
+
|
|
857
|
+
| Type | Prefix | Example |
|
|
858
|
+
|--------|----------|---------------------|
|
|
859
|
+
| `page` | `page-` | `page-profile` |
|
|
860
|
+
| `dock` | `dock-` | `dock-sidebar` |
|
|
861
|
+
| `view` | `view-` | `view-user-card` |
|
|
862
|
+
| `part` | `part-` | `part-icon-btn` |
|
|
863
|
+
|
|
864
|
+
Prefix is enforced by convention, not by the definition functions. The prefix is entirely visible in HTML, making the type of every element immediately clear at a glance.
|
|
865
|
+
|
|
866
|
+
If a `dock` tag is omitted from the config, it auto-derives as `dock-{name}`:
|
|
867
|
+
|
|
868
|
+
```js
|
|
869
|
+
dock('sidebar', { ... }) // tag = 'dock-sidebar'
|
|
870
|
+
dock('settings-panel', { ... }) // tag = 'dock-settings-panel'
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
---
|
|
874
|
+
|
|
875
|
+
## 10. Lifecycle Reference
|
|
876
|
+
|
|
877
|
+
### `page`
|
|
878
|
+
|
|
879
|
+
| Hook | When | `this` |
|
|
880
|
+
|-----------------------------------|-----------------------------------------------|----------|
|
|
881
|
+
| `on.load({ params, query, hash, state })` | Route activates, before swap into dock | Element |
|
|
882
|
+
| `on.unload()` | Route deactivates, before swap away | Element |
|
|
883
|
+
| `on.connect()` | `connectedCallback` — element in DOM | Element |
|
|
884
|
+
| `on.disconnect()` | `disconnectedCallback` — element removed | Element |
|
|
885
|
+
|
|
886
|
+
`on.load` receives the same `params`, `query`, `hash`, `state` that the router resolved. Returning a rejected Promise from `on.load` aborts the navigation.
|
|
887
|
+
|
|
888
|
+
### `dock`
|
|
889
|
+
|
|
890
|
+
| Hook | When | `this` |
|
|
891
|
+
|-----------------------------------|-----------------------------------------------|----------|
|
|
892
|
+
| `on.connect()` | After `graph.add()` — dock is in graph | Element |
|
|
893
|
+
| `on.disconnect()` | Before `graph.remove()` — dock leaving graph | Element |
|
|
894
|
+
| `on.swap(el, { direction })` | Router mounting content into this dock | Element |
|
|
895
|
+
|
|
896
|
+
`on.swap` must return a Promise. `direction` is the Navigation API `navigationType`: `'push'`, `'replace'`, `'traverse'`, or `'load'`.
|
|
897
|
+
|
|
898
|
+
### `view`
|
|
899
|
+
|
|
900
|
+
| Hook | When | `this` |
|
|
901
|
+
|-----------------------------------|-----------------------------------------------|----------|
|
|
902
|
+
| `on.connect()` | `connectedCallback` | Element |
|
|
903
|
+
| `on.disconnect()` | `disconnectedCallback` | Element |
|
|
904
|
+
| `on.change(attr, prev, next)` | `attributeChangedCallback` — `next` is cast | Element |
|
|
905
|
+
| `on.adopt()` | `adoptedCallback` | Element |
|
|
906
|
+
|
|
907
|
+
### `part`
|
|
908
|
+
|
|
909
|
+
| Hook | When | `this` |
|
|
910
|
+
|-----------------------------------|-----------------------------------------------|----------|
|
|
911
|
+
| `on.connect()` | `connectedCallback` — optional | Element |
|
|
912
|
+
| `on.disconnect()` | `disconnectedCallback` — optional | Element |
|
|
913
|
+
|
|
914
|
+
---
|
|
915
|
+
|
|
916
|
+
## 11. Route-Awareness Matrix
|
|
917
|
+
|
|
918
|
+
| | `page` | `dock` | `view` | `part` |
|
|
919
|
+
|-----------------------|--------|--------|--------|--------|
|
|
920
|
+
| URL pattern | Yes | No | No | No |
|
|
921
|
+
| `via` chain | Yes | No | No | No |
|
|
922
|
+
| Boot gate | Yes | Yes | No | No |
|
|
923
|
+
| Container graph | No | Yes | No | No |
|
|
924
|
+
| `swap` method | No | Yes | No | No |
|
|
925
|
+
| Observed props | Yes | No | Yes | Yes |
|
|
926
|
+
| Query param mapping | Yes | No | No | No |
|
|
927
|
+
| Route guard | Yes | No | No | No |
|
|
928
|
+
| `on.load / unload` | Yes | No | No | No |
|
|
929
|
+
| `on.change` | No | No | Yes | No |
|
|
930
|
+
| `on.adopt` | No | No | Yes | No |
|
|
931
|
+
| Use in any template | Yes | Yes | Yes | Yes |
|
|
932
|
+
|
|
933
|
+
---
|
|
934
|
+
|
|
935
|
+
## 12. Props Typing Reference
|
|
936
|
+
|
|
937
|
+
The `props` field uses the same casting model as the existing `specRegistry` in `outlet.js`. The outlet cast loop is now absorbed into the definition runtime.
|
|
938
|
+
|
|
939
|
+
| `type` | Attribute value | Cast result |
|
|
940
|
+
|-----------|-------------------|-----------------|
|
|
941
|
+
| `String` | `"hello"` | `"hello"` |
|
|
942
|
+
| `Number` | `"42"` | `42` |
|
|
943
|
+
| `Number` | `"bad"` | `0` |
|
|
944
|
+
| `Boolean` | `"true"` / `"1"` / `""` | `true` |
|
|
945
|
+
| `Boolean` | `"false"` / `"0"` | `false` |
|
|
946
|
+
|
|
947
|
+
For `page` props, route params and query values (both strings from the URL) are cast before being set as element properties. For `view` and `part` props, attribute values are cast inside `attributeChangedCallback`.
|
|
948
|
+
|
|
949
|
+
---
|
|
950
|
+
|
|
951
|
+
## 13. Migration from `ui.element` + `router.register`
|
|
952
|
+
|
|
953
|
+
| Before | After |
|
|
954
|
+
|-------------------------------------------|---------------------------|
|
|
955
|
+
| `ui.element({ url: '/path', tag, container })` | `page('/path', { tag, via: [...] })` |
|
|
956
|
+
| `ui.container('name')` | `dock('name', { ... })` |
|
|
957
|
+
| `<route-outlet>` | `<dock-main>`, `<dock-sidebar>` etc. |
|
|
958
|
+
| `router.register(pat, tag, meta)` | Called internally by `page()` |
|
|
959
|
+
| `specRegistry.set(tag, spec)` | Called internally by `page()` |
|
|
960
|
+
| `router.guard(fn)` | Still works. Per-route: `guard` field in `page()` |
|
|
961
|
+
|
|
962
|
+
Both APIs can coexist during migration. `router.register` and `registerContainer` remain public. Teams can migrate definition by definition.
|
|
963
|
+
|
|
964
|
+
---
|
|
965
|
+
|
|
966
|
+
## 14. `defs/index.js`
|
|
967
|
+
|
|
968
|
+
```js
|
|
969
|
+
export { page } from './page.js'
|
|
970
|
+
export { dock } from './dock.js'
|
|
971
|
+
export { view } from './view.js'
|
|
972
|
+
export { part } from './part.js'
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
Each file contains the definition function. The implementation wires Custom Element class creation, `customElements.define`, router registration, specRegistry population, and boot gating. None of that is the developer's concern.
|
|
976
|
+
|
|
977
|
+
---
|
|
978
|
+
|
|
979
|
+
*End of definitions.*
|