@adukiorg/anza 0.2.0 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/CHANGELOG.md +90 -4
  2. package/README.md +97 -133
  3. package/bin/anza/anza-linux-arm64 +0 -0
  4. package/bin/anza/anza-linux-x64 +0 -0
  5. package/bin/anza/anza-macos-arm64 +0 -0
  6. package/bin/anza/anza-macos-x64 +0 -0
  7. package/bin/anza/anza-windows-x64.exe +0 -0
  8. package/bin/anza/find.js +35 -0
  9. package/bin/anza/index.js +34 -0
  10. package/bin/anza/launch.js +19 -0
  11. package/bin/common/index.js +7 -0
  12. package/bin/common/logs.js +62 -0
  13. package/bin/create/copy.js +18 -0
  14. package/bin/create/index.js +45 -0
  15. package/bin/create/run.js +210 -0
  16. package/bin/create/write.js +19 -0
  17. package/importmap.json +4 -0
  18. package/package.json +16 -10
  19. package/src/core/offline/{usage.md → notes/usage.md} +11 -1
  20. package/src/core/router/boot.js +82 -0
  21. package/src/core/router/cascade.js +76 -0
  22. package/src/core/router/container.js +63 -72
  23. package/src/core/router/graph.js +144 -0
  24. package/src/core/router/index.js +12 -2
  25. package/src/core/router/intercept.js +26 -7
  26. package/src/core/router/lca.js +58 -0
  27. package/src/core/router/match.js +49 -36
  28. package/src/core/router/notes/audit-old.md +887 -0
  29. package/src/core/router/notes/audti.md +773 -0
  30. package/src/core/router/notes/tasks.md +473 -0
  31. package/src/core/router/{usage.md → notes/usage.md} +57 -35
  32. package/src/core/router/sync/tab.js +6 -4
  33. package/src/core/router/transitions.js +35 -8
  34. package/src/core/router/trie.js +130 -0
  35. package/src/core/security/{usage.md → notes/usage.md} +1 -2
  36. package/src/core/storage/{usage.md → notes/usage.md} +6 -6
  37. package/src/core/theme/index.js +78 -0
  38. package/src/core/ui/define/index.js +2 -1
  39. package/src/core/ui/define/orchestrator.js +10 -4
  40. package/src/core/ui/defs/dock.js +134 -0
  41. package/src/core/ui/defs/index.js +20 -0
  42. package/src/core/ui/defs/page.js +89 -0
  43. package/src/core/ui/defs/part.js +28 -0
  44. package/src/core/ui/defs/spec.js +96 -0
  45. package/src/core/ui/defs/view.js +23 -0
  46. package/src/core/ui/index.js +16 -3
  47. package/src/core/ui/notes/definations.md +979 -0
  48. package/src/tokens/index.css +1 -0
  49. package/src/tokens/semantic/contrast.css +18 -0
  50. package/src/tokens/semantic/transitions.css +32 -0
  51. package/types/core/platform/index.d.ts +39 -10
  52. package/types/core/router/index.d.ts +9 -0
  53. package/types/core/theme/index.d.ts +18 -0
  54. package/types/core/ui/index.d.ts +11 -0
  55. package/types/index.d.ts +1 -0
  56. package/bin/anza.js +0 -63
  57. package/bin/create.js +0 -150
  58. package/src/core/api/plan.md +0 -209
  59. package/src/core/events/missing.md +0 -103
  60. package/src/core/events/plan.md +0 -177
  61. package/src/core/offline/missing.md +0 -89
  62. package/src/core/offline/plan.md +0 -143
  63. package/src/core/platform/missing.md +0 -119
  64. package/src/core/platform/platform.d.ts +0 -88
  65. package/src/core/router/missing.md +0 -716
  66. package/src/core/router/outlet.js +0 -139
  67. package/src/core/router/plan.md +0 -370
  68. package/src/core/security/missing.md +0 -97
  69. package/src/core/state/missing.md +0 -165
  70. package/src/core/storage/missing.md +0 -165
  71. package/src/core/storage/plan.md +0 -69
  72. package/src/core/ui/implementation.md +0 -170
  73. package/src/core/ui/plan.md +0 -510
  74. package/src/core/ui/ui.types.md +0 -890
  75. /package/src/core/animations/{usage.md → notes/usage.md} +0 -0
  76. /package/src/core/api/{usage.md → notes/usage.md} +0 -0
  77. /package/src/core/events/{usage.md → notes/usage.md} +0 -0
  78. /package/src/core/platform/{usage.md → notes/usage.md} +0 -0
  79. /package/src/core/state/{usage.md → notes/usage.md} +0 -0
  80. /package/src/core/ui/{usage.md → notes/usage.md} +0 -0
  81. /package/src/core/ui/{watch.md → notes/watch.md} +0 -0
  82. /package/src/core/workers/{plan.md → notes/plan.md} +0 -0
  83. /package/src/core/workers/{usage.md → notes/usage.md} +0 -0
@@ -0,0 +1,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.*