@adukiorg/anza 0.2.0
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 +137 -0
- package/README.md +215 -0
- package/bin/anza.js +63 -0
- package/bin/create.js +150 -0
- package/importmap.json +72 -0
- package/package.json +100 -0
- package/src/core/animations/index.js +55 -0
- package/src/core/animations/play.js +111 -0
- package/src/core/animations/registry.js +54 -0
- package/src/core/animations/scroll.js +50 -0
- package/src/core/animations/tokens.js +58 -0
- package/src/core/animations/usage.md +301 -0
- package/src/core/animations/waapi.js +86 -0
- package/src/core/api/cache.js +120 -0
- package/src/core/api/caches/glob.js +24 -0
- package/src/core/api/caches/index.js +118 -0
- package/src/core/api/events/index.js +75 -0
- package/src/core/api/fetch.js +99 -0
- package/src/core/api/index.js +158 -0
- package/src/core/api/pipeline.js +98 -0
- package/src/core/api/plan.md +209 -0
- package/src/core/api/prefixes/index.js +66 -0
- package/src/core/api/retry.js +69 -0
- package/src/core/api/stream.js +127 -0
- package/src/core/api/upload.js +180 -0
- package/src/core/api/usage.md +206 -0
- package/src/core/events/bus.js +38 -0
- package/src/core/events/delegate.js +79 -0
- package/src/core/events/index.js +26 -0
- package/src/core/events/listen.js +50 -0
- package/src/core/events/missing.md +103 -0
- package/src/core/events/once.js +49 -0
- package/src/core/events/plan.md +177 -0
- package/src/core/events/types/index.js +34 -0
- package/src/core/events/usage.md +107 -0
- package/src/core/offline/bridge.js +51 -0
- package/src/core/offline/clock.js +100 -0
- package/src/core/offline/connectivity.js +116 -0
- package/src/core/offline/index.js +41 -0
- package/src/core/offline/missing.md +89 -0
- package/src/core/offline/plan.md +143 -0
- package/src/core/offline/queue.js +168 -0
- package/src/core/offline/state.js +18 -0
- package/src/core/offline/sync.js +106 -0
- package/src/core/offline/usage.md +273 -0
- package/src/core/platform/guard.js +104 -0
- package/src/core/platform/index.js +42 -0
- package/src/core/platform/missing.md +119 -0
- package/src/core/platform/platform.d.ts +88 -0
- package/src/core/platform/polyfills/anchor.js +79 -0
- package/src/core/platform/polyfills/navigation.js +142 -0
- package/src/core/platform/polyfills/popover.js +142 -0
- package/src/core/platform/polyfills/scheduler.js +194 -0
- package/src/core/platform/polyfills/shadow.js +35 -0
- package/src/core/platform/polyfills/urlpattern.js +119 -0
- package/src/core/platform/supports.js +186 -0
- package/src/core/platform/usage.md +287 -0
- package/src/core/router/cache.js +95 -0
- package/src/core/router/container.js +146 -0
- package/src/core/router/handler.js +52 -0
- package/src/core/router/history.js +120 -0
- package/src/core/router/index.js +158 -0
- package/src/core/router/intercept.js +376 -0
- package/src/core/router/match.js +145 -0
- package/src/core/router/missing.md +716 -0
- package/src/core/router/outlet.js +139 -0
- package/src/core/router/plan.md +370 -0
- package/src/core/router/sync/index.js +16 -0
- package/src/core/router/sync/tab.js +115 -0
- package/src/core/router/sync/transport.js +139 -0
- package/src/core/router/transitions.js +59 -0
- package/src/core/router/usage.md +773 -0
- package/src/core/security/crypto.js +159 -0
- package/src/core/security/index.js +49 -0
- package/src/core/security/missing.md +97 -0
- package/src/core/security/permissions.js +64 -0
- package/src/core/security/sanitize.js +100 -0
- package/src/core/security/usage.md +283 -0
- package/src/core/state/derived.js +117 -0
- package/src/core/state/index.js +23 -0
- package/src/core/state/missing.md +165 -0
- package/src/core/state/persist.js +284 -0
- package/src/core/state/store.js +308 -0
- package/src/core/state/sync.js +46 -0
- package/src/core/state/usage.md +440 -0
- package/src/core/storage/cache.js +83 -0
- package/src/core/storage/idb.js +196 -0
- package/src/core/storage/index.js +373 -0
- package/src/core/storage/lru.js +107 -0
- package/src/core/storage/missing.md +165 -0
- package/src/core/storage/opfs.js +190 -0
- package/src/core/storage/plan.md +69 -0
- package/src/core/storage/quota.js +69 -0
- package/src/core/storage/usage.md +226 -0
- package/src/core/ui/base.js +50 -0
- package/src/core/ui/define/container.js +82 -0
- package/src/core/ui/define/define.js +12 -0
- package/src/core/ui/define/element.js +390 -0
- package/src/core/ui/define/index.js +9 -0
- package/src/core/ui/define/orchestrator.js +105 -0
- package/src/core/ui/define/proxy.js +644 -0
- package/src/core/ui/define/state.js +6 -0
- package/src/core/ui/define/utils.js +134 -0
- package/src/core/ui/implementation.md +170 -0
- package/src/core/ui/index.js +41 -0
- package/src/core/ui/observe.js +117 -0
- package/src/core/ui/plan.md +510 -0
- package/src/core/ui/schedule.js +60 -0
- package/src/core/ui/template.js +37 -0
- package/src/core/ui/transitions.js +37 -0
- package/src/core/ui/ui.types.md +890 -0
- package/src/core/ui/usage.md +1124 -0
- package/src/core/ui/watch.md +346 -0
- package/src/core/workers/broadcast.js +138 -0
- package/src/core/workers/dedicated.js +153 -0
- package/src/core/workers/index.js +169 -0
- package/src/core/workers/locks.js +160 -0
- package/src/core/workers/offscreen.js +166 -0
- package/src/core/workers/plan.md +381 -0
- package/src/core/workers/pool.js +267 -0
- package/src/core/workers/shared.js +137 -0
- package/src/core/workers/usage.md +622 -0
- package/src/elements/base.js +12 -0
- package/src/elements/data/card/index.html +9 -0
- package/src/elements/data/card/index.js +19 -0
- package/src/elements/data/card/index.tags.json +1 -0
- package/src/elements/data/card/style.css +46 -0
- package/src/elements/data/chart/index.html +1 -0
- package/src/elements/data/chart/index.js +143 -0
- package/src/elements/data/chart/index.tags.json +1 -0
- package/src/elements/data/chart/style.css +13 -0
- package/src/elements/data/list/index.html +3 -0
- package/src/elements/data/list/index.js +19 -0
- package/src/elements/data/list/index.tags.json +1 -0
- package/src/elements/data/list/style.css +39 -0
- package/src/elements/data/stat/index.html +9 -0
- package/src/elements/data/stat/index.js +19 -0
- package/src/elements/data/stat/index.tags.json +1 -0
- package/src/elements/data/stat/style.css +50 -0
- package/src/elements/data/table/index.html +1 -0
- package/src/elements/data/table/index.js +16 -0
- package/src/elements/data/table/index.tags.json +1 -0
- package/src/elements/data/table/style.css +50 -0
- package/src/elements/feedback/alert/index.html +11 -0
- package/src/elements/feedback/alert/index.js +28 -0
- package/src/elements/feedback/alert/index.tags.json +1 -0
- package/src/elements/feedback/alert/style.css +75 -0
- package/src/elements/feedback/empty/index.html +13 -0
- package/src/elements/feedback/empty/index.js +34 -0
- package/src/elements/feedback/empty/index.tags.json +1 -0
- package/src/elements/feedback/empty/style.css +45 -0
- package/src/elements/feedback/progress/index.html +7 -0
- package/src/elements/feedback/progress/index.js +46 -0
- package/src/elements/feedback/progress/index.tags.json +1 -0
- package/src/elements/feedback/progress/style.css +36 -0
- package/src/elements/feedback/skeleton/index.html +1 -0
- package/src/elements/feedback/skeleton/index.js +78 -0
- package/src/elements/feedback/skeleton/index.tags.json +1 -0
- package/src/elements/feedback/skeleton/style.css +28 -0
- package/src/elements/feedback/toast/index.html +3 -0
- package/src/elements/feedback/toast/index.js +65 -0
- package/src/elements/feedback/toast/index.tags.json +1 -0
- package/src/elements/feedback/toast/style.css +36 -0
- package/src/elements/forms/checkbox/index.html +7 -0
- package/src/elements/forms/checkbox/index.js +104 -0
- package/src/elements/forms/checkbox/index.tags.json +1 -0
- package/src/elements/forms/checkbox/style.css +86 -0
- package/src/elements/forms/field/index.html +13 -0
- package/src/elements/forms/field/index.js +42 -0
- package/src/elements/forms/field/index.tags.json +1 -0
- package/src/elements/forms/field/style.css +42 -0
- package/src/elements/forms/form/index.html +3 -0
- package/src/elements/forms/form/index.js +122 -0
- package/src/elements/forms/form/index.tags.json +1 -0
- package/src/elements/forms/form/style.css +11 -0
- package/src/elements/forms/input/index.html +4 -0
- package/src/elements/forms/input/index.js +103 -0
- package/src/elements/forms/input/index.tags.json +1 -0
- package/src/elements/forms/input/style.css +39 -0
- package/src/elements/forms/radio/index.html +4 -0
- package/src/elements/forms/radio/index.js +109 -0
- package/src/elements/forms/radio/index.tags.json +1 -0
- package/src/elements/forms/radio/style.css +65 -0
- package/src/elements/forms/select/index.html +9 -0
- package/src/elements/forms/select/index.js +114 -0
- package/src/elements/forms/select/index.tags.json +1 -0
- package/src/elements/forms/select/style.css +95 -0
- package/src/elements/forms/textarea/index.html +4 -0
- package/src/elements/forms/textarea/index.js +115 -0
- package/src/elements/forms/textarea/index.tags.json +1 -0
- package/src/elements/forms/textarea/style.css +46 -0
- package/src/elements/forms/toggle/index.html +4 -0
- package/src/elements/forms/toggle/index.js +89 -0
- package/src/elements/forms/toggle/index.tags.json +1 -0
- package/src/elements/forms/toggle/style.css +63 -0
- package/src/elements/forms/upload/index.html +13 -0
- package/src/elements/forms/upload/index.js +120 -0
- package/src/elements/forms/upload/index.tags.json +1 -0
- package/src/elements/forms/upload/style.css +61 -0
- package/src/elements/index.js +71 -0
- package/src/elements/layout/app/index.html +7 -0
- package/src/elements/layout/app/index.js +16 -0
- package/src/elements/layout/app/index.tags.json +1 -0
- package/src/elements/layout/app/style.css +41 -0
- package/src/elements/layout/grid/index.html +3 -0
- package/src/elements/layout/grid/index.js +41 -0
- package/src/elements/layout/grid/index.tags.json +1 -0
- package/src/elements/layout/grid/style.css +12 -0
- package/src/elements/layout/header/index.html +8 -0
- package/src/elements/layout/header/index.js +16 -0
- package/src/elements/layout/header/index.tags.json +1 -0
- package/src/elements/layout/header/style.css +28 -0
- package/src/elements/layout/scroll/index.html +3 -0
- package/src/elements/layout/scroll/index.js +19 -0
- package/src/elements/layout/scroll/index.tags.json +1 -0
- package/src/elements/layout/scroll/style.css +24 -0
- package/src/elements/layout/sidebar/index.html +3 -0
- package/src/elements/layout/sidebar/index.js +24 -0
- package/src/elements/layout/sidebar/index.tags.json +1 -0
- package/src/elements/layout/sidebar/style.css +30 -0
- package/src/elements/layout/split/index.html +3 -0
- package/src/elements/layout/split/index.js +18 -0
- package/src/elements/layout/split/index.tags.json +1 -0
- package/src/elements/layout/split/style.css +28 -0
- package/src/elements/layout/stack/index.html +3 -0
- package/src/elements/layout/stack/index.js +31 -0
- package/src/elements/layout/stack/index.tags.json +1 -0
- package/src/elements/layout/stack/style.css +15 -0
- package/src/elements/layout/surface/index.html +3 -0
- package/src/elements/layout/surface/index.js +19 -0
- package/src/elements/layout/surface/index.tags.json +1 -0
- package/src/elements/layout/surface/style.css +29 -0
- package/src/elements/navigation/breadcrumb/index.html +5 -0
- package/src/elements/navigation/breadcrumb/index.js +16 -0
- package/src/elements/navigation/breadcrumb/index.tags.json +1 -0
- package/src/elements/navigation/breadcrumb/style.css +36 -0
- package/src/elements/navigation/nav/index.html +3 -0
- package/src/elements/navigation/nav/index.js +24 -0
- package/src/elements/navigation/nav/index.tags.json +1 -0
- package/src/elements/navigation/nav/style.css +38 -0
- package/src/elements/navigation/pagination/index.html +3 -0
- package/src/elements/navigation/pagination/index.js +94 -0
- package/src/elements/navigation/pagination/index.tags.json +1 -0
- package/src/elements/navigation/pagination/style.css +39 -0
- package/src/elements/navigation/steps/index.html +6 -0
- package/src/elements/navigation/steps/index.js +64 -0
- package/src/elements/navigation/steps/index.tags.json +1 -0
- package/src/elements/navigation/steps/style.css +78 -0
- package/src/elements/navigation/tabs/index.html +6 -0
- package/src/elements/navigation/tabs/index.js +132 -0
- package/src/elements/navigation/tabs/index.tags.json +1 -0
- package/src/elements/navigation/tabs/style.css +52 -0
- package/src/elements/overlay/dialog/index.html +5 -0
- package/src/elements/overlay/dialog/index.js +57 -0
- package/src/elements/overlay/dialog/index.tags.json +1 -0
- package/src/elements/overlay/dialog/style.css +31 -0
- package/src/elements/overlay/drawer/index.html +3 -0
- package/src/elements/overlay/drawer/index.js +56 -0
- package/src/elements/overlay/drawer/index.tags.json +1 -0
- package/src/elements/overlay/drawer/style.css +48 -0
- package/src/elements/overlay/menu/index.html +3 -0
- package/src/elements/overlay/menu/index.js +107 -0
- package/src/elements/overlay/menu/index.tags.json +1 -0
- package/src/elements/overlay/menu/style.css +43 -0
- package/src/elements/overlay/popover/index.html +3 -0
- package/src/elements/overlay/popover/index.js +44 -0
- package/src/elements/overlay/popover/index.tags.json +1 -0
- package/src/elements/overlay/popover/style.css +21 -0
- package/src/elements/overlay/sheet/index.html +8 -0
- package/src/elements/overlay/sheet/index.js +105 -0
- package/src/elements/overlay/sheet/index.tags.json +1 -0
- package/src/elements/overlay/sheet/style.css +64 -0
- package/src/elements/overlay/tooltip/index.html +6 -0
- package/src/elements/overlay/tooltip/index.js +16 -0
- package/src/elements/overlay/tooltip/index.tags.json +1 -0
- package/src/elements/overlay/tooltip/style.css +41 -0
- package/src/elements/primitives/avatar/index.html +2 -0
- package/src/elements/primitives/avatar/index.js +79 -0
- package/src/elements/primitives/avatar/index.tags.json +1 -0
- package/src/elements/primitives/avatar/style.css +36 -0
- package/src/elements/primitives/badge/index.html +3 -0
- package/src/elements/primitives/badge/index.js +20 -0
- package/src/elements/primitives/badge/index.tags.json +1 -0
- package/src/elements/primitives/badge/style.css +67 -0
- package/src/elements/primitives/button/index.html +3 -0
- package/src/elements/primitives/button/index.js +61 -0
- package/src/elements/primitives/button/index.tags.json +1 -0
- package/src/elements/primitives/button/style.css +66 -0
- package/src/elements/primitives/divider/index.html +1 -0
- package/src/elements/primitives/divider/index.js +43 -0
- package/src/elements/primitives/divider/index.tags.json +1 -0
- package/src/elements/primitives/divider/style.css +39 -0
- package/src/elements/primitives/icon/index.html +3 -0
- package/src/elements/primitives/icon/index.js +66 -0
- package/src/elements/primitives/icon/index.tags.json +1 -0
- package/src/elements/primitives/icon/style.css +20 -0
- package/src/elements/primitives/link/index.html +3 -0
- package/src/elements/primitives/link/index.js +129 -0
- package/src/elements/primitives/link/index.tags.json +1 -0
- package/src/elements/primitives/link/style.css +40 -0
- package/src/elements/primitives/spinner/index.html +1 -0
- package/src/elements/primitives/spinner/index.js +62 -0
- package/src/elements/primitives/spinner/index.tags.json +1 -0
- package/src/elements/primitives/spinner/style.css +20 -0
- package/src/elements/primitives/text/index.html +1 -0
- package/src/elements/primitives/text/index.js +79 -0
- package/src/elements/primitives/text/index.tags.json +1 -0
- package/src/elements/primitives/text/style.css +25 -0
- package/src/index.js +23 -0
- package/src/styles/base.css +66 -0
- package/src/styles/index.css +10 -0
- package/src/styles/layers.css +9 -0
- package/src/styles/reset.css +66 -0
- package/src/sw/activate.js +51 -0
- package/src/sw/expire.js +47 -0
- package/src/sw/index.js +28 -0
- package/src/sw/install.js +35 -0
- package/src/sw/push.js +58 -0
- package/src/sw/queue.js +60 -0
- package/src/sw/routes.js +71 -0
- package/src/sw/strategies.js +247 -0
- package/src/sw/sync.js +80 -0
- package/src/tokens/index.css +26 -0
- package/src/tokens/primitives/colors.css +54 -0
- package/src/tokens/primitives/motion.css +34 -0
- package/src/tokens/primitives/radius.css +16 -0
- package/src/tokens/primitives/shadow.css +34 -0
- package/src/tokens/primitives/spacing.css +27 -0
- package/src/tokens/primitives/typography.css +46 -0
- package/src/tokens/primitives/zindex.css +18 -0
- package/src/tokens/registered/colors.css +133 -0
- package/src/tokens/registered/dimensions.css +31 -0
- package/src/tokens/semantic/components.css +125 -0
- package/src/tokens/semantic/contrast.css +33 -0
- package/src/tokens/semantic/dark.css +61 -0
- package/src/tokens/semantic/light.css +64 -0
- package/types/core/animations/index.d.ts +52 -0
- package/types/core/api/index.d.ts +68 -0
- package/types/core/events/index.d.ts +50 -0
- package/types/core/offline/index.d.ts +68 -0
- package/types/core/platform/index.d.ts +60 -0
- package/types/core/router/index.d.ts +203 -0
- package/types/core/security/index.d.ts +33 -0
- package/types/core/state/index.d.ts +68 -0
- package/types/core/storage/index.d.ts +40 -0
- package/types/core/ui/index.d.ts +446 -0
- package/types/core/workers/index.d.ts +221 -0
- package/types/elements/index.d.ts +150 -0
- package/types/index.d.ts +18 -0
|
@@ -0,0 +1,1124 @@
|
|
|
1
|
+
# Native UI Usage Guide
|
|
2
|
+
|
|
3
|
+
The Native UI layer defines custom elements with Shadow DOM, lifecycle-owned cleanup, cached template anchors, delegated events, scoped mutation watching, observer wrappers, scheduler helpers, View Transitions, and cached template fragments.
|
|
4
|
+
|
|
5
|
+
Status: this guide documents the implemented public UI contract: `ui.define`, `ui.element`, `ui.container`, props, `reflect`, `methods`, `refs`, `tags`, delegated `on`, `watch`, `ui.schedule`, `ui.scheduleFrame`, `ui.yield`, `ui.observe`, `ui.transition`, `ui.template`, form-associated elements, generated `.tags.json` descriptors, generated `routes.json`, and TypeScript component typing.
|
|
6
|
+
|
|
7
|
+
Import from the UI entry point:
|
|
8
|
+
|
|
9
|
+
```javascript
|
|
10
|
+
import { ui } from '@adukiorg/anza/ui';
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## 1. Choosing an API
|
|
14
|
+
|
|
15
|
+
| Need | Use |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| Declarative component | `ui.element` |
|
|
18
|
+
| Router-owned layout slot | `ui.container` |
|
|
19
|
+
| Existing custom element class | `ui.define` |
|
|
20
|
+
| Stable template anchor | `refs` |
|
|
21
|
+
| Cached selector lookup | `tags` |
|
|
22
|
+
| Delegated event binding | `on` |
|
|
23
|
+
| Shadow-root mutation watching | `watch` |
|
|
24
|
+
| Raw platform observer | `ui.observe` |
|
|
25
|
+
| Deferred or priority work | `ui.schedule` |
|
|
26
|
+
| Frame-synced DOM work | `ui.scheduleFrame` |
|
|
27
|
+
| Cooperative loop pause | `ui.yield` |
|
|
28
|
+
| Visible DOM swap | `ui.transition` |
|
|
29
|
+
| Reusable fragment | `ui.template` |
|
|
30
|
+
|
|
31
|
+
## 2. Component Shape
|
|
32
|
+
|
|
33
|
+
Use `ui.element` for components, pages, and primitives.
|
|
34
|
+
|
|
35
|
+
```javascript
|
|
36
|
+
ui.element('ui-counter', {
|
|
37
|
+
template: './index.html',
|
|
38
|
+
style: './style.css',
|
|
39
|
+
|
|
40
|
+
props: {
|
|
41
|
+
count: { type: Number, default: 0 },
|
|
42
|
+
disabled: { type: Boolean, default: false, state: true },
|
|
43
|
+
label: { type: String, default: 'Count' }
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
mount({ el, refs, on }) {
|
|
47
|
+
refs.label.textContent = el.label;
|
|
48
|
+
|
|
49
|
+
on.click('button', () => {
|
|
50
|
+
if (el.disabled) return;
|
|
51
|
+
el.count++;
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
update({ el, name, val, prev, refs }) {
|
|
56
|
+
if (name === 'count') {
|
|
57
|
+
refs.value.textContent = String(val);
|
|
58
|
+
refs.value.dataset.prev = String(prev);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (name === 'disabled') {
|
|
62
|
+
refs.button.disabled = val;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
unmount({ refs }) {
|
|
67
|
+
refs.value.textContent = '';
|
|
68
|
+
}
|
|
69
|
+
}, import.meta.url);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Always pass `import.meta.url` as the third argument when `template` or `style` is a relative path. If a relative `template`/`style` is given without a base, the element logs an error (the resource would otherwise silently fail to load).
|
|
73
|
+
|
|
74
|
+
### Prop shorthand
|
|
75
|
+
|
|
76
|
+
Props accept a shorthand: a literal default whose type is inferred. Use the full
|
|
77
|
+
object form only when you need `state`, `reflect`, or an explicit `type`.
|
|
78
|
+
|
|
79
|
+
```javascript
|
|
80
|
+
ui.element('ui-counter', {
|
|
81
|
+
props: {
|
|
82
|
+
count: 0, // -> { type: Number, default: 0 }
|
|
83
|
+
label: 'Count', // -> { type: String, default: 'Count' }
|
|
84
|
+
open: false, // -> { type: Boolean, default: false }
|
|
85
|
+
selected: { type: Boolean, default: false, state: true } // full form
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Splitting a component across files
|
|
91
|
+
|
|
92
|
+
Because the toolchain resolves the full import graph, keep `index.js` small and
|
|
93
|
+
import lifecycle/logic from sibling modules with plain ESM — the imported files
|
|
94
|
+
are emitted into `dist/` automatically. (No special spec field is needed.)
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
// counter/logic.js
|
|
98
|
+
export function mount({ el, on }) {
|
|
99
|
+
on.click('button', () => el.count++);
|
|
100
|
+
}
|
|
101
|
+
export function update({ name, val, refs }) {
|
|
102
|
+
if (name === 'count') refs.value.textContent = String(val);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// counter/index.js
|
|
106
|
+
import { ui } from '../../core/ui/index.js';
|
|
107
|
+
import { mount, update } from './logic.js';
|
|
108
|
+
|
|
109
|
+
ui.element('ui-counter', {
|
|
110
|
+
template: './index.html',
|
|
111
|
+
style: './style.css',
|
|
112
|
+
props: { count: 0 },
|
|
113
|
+
mount,
|
|
114
|
+
update
|
|
115
|
+
}, import.meta.url);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Component methods can be installed on the generated element prototype.
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
ui.element('ui-counter', {
|
|
122
|
+
props: {
|
|
123
|
+
count: { type: Number, default: 0 }
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
methods: {
|
|
127
|
+
increment() {
|
|
128
|
+
this.count++;
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
mount({ el, on }) {
|
|
133
|
+
on.click('button', () => el.increment());
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Rules:
|
|
139
|
+
|
|
140
|
+
- Methods run with `this` set to the host element.
|
|
141
|
+
- Methods are installed before `customElements.define`.
|
|
142
|
+
- Reserved lifecycle names such as `connectedCallback`, `disconnectedCallback`, `attributeChangedCallback`, `mount`, and `unmount` are skipped with a warning.
|
|
143
|
+
- Methods are prototype methods, not per-instance bound functions.
|
|
144
|
+
|
|
145
|
+
## 3. Component Files
|
|
146
|
+
|
|
147
|
+
Keep component files together.
|
|
148
|
+
|
|
149
|
+
```text
|
|
150
|
+
src/elements/primitives/button/
|
|
151
|
+
index.js
|
|
152
|
+
index.html
|
|
153
|
+
index.tags.json
|
|
154
|
+
style.css
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`index.js` registers the element.
|
|
158
|
+
|
|
159
|
+
```javascript
|
|
160
|
+
import { ui } from '../../../core/ui/index.js';
|
|
161
|
+
|
|
162
|
+
ui.element('ui-button', {
|
|
163
|
+
template: './index.html',
|
|
164
|
+
style: './style.css',
|
|
165
|
+
props: {
|
|
166
|
+
disabled: { type: Boolean, default: false, state: true }
|
|
167
|
+
}
|
|
168
|
+
}, import.meta.url);
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`index.html` contains the shadow template.
|
|
172
|
+
|
|
173
|
+
```html
|
|
174
|
+
<button ref="button" id="button" type="button">
|
|
175
|
+
<slot></slot>
|
|
176
|
+
</button>
|
|
177
|
+
<span ref="status" hidden></span>
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`style.css` contains component-scoped styles adopted into the shadow root.
|
|
181
|
+
|
|
182
|
+
```css
|
|
183
|
+
:host {
|
|
184
|
+
display: inline-block;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
button:disabled {
|
|
188
|
+
opacity: 0.5;
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## 4. Lifecycle Context
|
|
193
|
+
|
|
194
|
+
`mount` receives a stable context for the current connection lifecycle.
|
|
195
|
+
|
|
196
|
+
```javascript
|
|
197
|
+
mount({
|
|
198
|
+
el,
|
|
199
|
+
ctrl,
|
|
200
|
+
tags,
|
|
201
|
+
on,
|
|
202
|
+
refs,
|
|
203
|
+
watch,
|
|
204
|
+
internals
|
|
205
|
+
}) {}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
`update` receives the same helpers plus the changed property.
|
|
209
|
+
|
|
210
|
+
```javascript
|
|
211
|
+
update({
|
|
212
|
+
el,
|
|
213
|
+
ctrl,
|
|
214
|
+
tags,
|
|
215
|
+
on,
|
|
216
|
+
refs,
|
|
217
|
+
watch,
|
|
218
|
+
internals,
|
|
219
|
+
name,
|
|
220
|
+
val,
|
|
221
|
+
prev,
|
|
222
|
+
old
|
|
223
|
+
}) {}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
`unmount` receives cleanup-friendly helpers.
|
|
227
|
+
|
|
228
|
+
```javascript
|
|
229
|
+
unmount({
|
|
230
|
+
el,
|
|
231
|
+
tags,
|
|
232
|
+
refs,
|
|
233
|
+
watch,
|
|
234
|
+
internals
|
|
235
|
+
}) {}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Rules:
|
|
239
|
+
|
|
240
|
+
- `el` is the host custom element.
|
|
241
|
+
- `ctrl` is an `AbortController` created on connect and aborted on disconnect.
|
|
242
|
+
- `refs`, `tags`, `on`, and `watch` are scoped to the component shadow root.
|
|
243
|
+
- `internals` is available when `form: true` is set.
|
|
244
|
+
- Do not store lifecycle context globally.
|
|
245
|
+
|
|
246
|
+
## 5. Props
|
|
247
|
+
|
|
248
|
+
Props define reflected element properties and observed attributes.
|
|
249
|
+
|
|
250
|
+
```javascript
|
|
251
|
+
props: {
|
|
252
|
+
open: { type: Boolean, default: false, state: true },
|
|
253
|
+
count: { type: Number, default: 0 },
|
|
254
|
+
label: { type: String, default: 'Untitled' },
|
|
255
|
+
cache: { type: String, default: '', reflect: false }
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
Supported types:
|
|
260
|
+
|
|
261
|
+
- `Boolean`: present attribute is `true`; missing attribute is `false`.
|
|
262
|
+
- `Number`: attribute value is cast with `Number(...)`.
|
|
263
|
+
- `String`: attribute value is used as text.
|
|
264
|
+
|
|
265
|
+
Boolean example:
|
|
266
|
+
|
|
267
|
+
```javascript
|
|
268
|
+
el.open = true;
|
|
269
|
+
el.hasAttribute('open'); // true
|
|
270
|
+
|
|
271
|
+
el.open = false;
|
|
272
|
+
el.hasAttribute('open'); // false
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Number and string example:
|
|
276
|
+
|
|
277
|
+
```javascript
|
|
278
|
+
el.count = 3;
|
|
279
|
+
el.getAttribute('count'); // "3"
|
|
280
|
+
|
|
281
|
+
el.label = 'Ready';
|
|
282
|
+
el.getAttribute('label'); // "Ready"
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Use `state: true` to mirror truthy values into `ElementInternals.states` when supported.
|
|
286
|
+
|
|
287
|
+
```javascript
|
|
288
|
+
props: {
|
|
289
|
+
selected: { type: Boolean, default: false, state: true }
|
|
290
|
+
}
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
Reflection defaults to enabled. Set `reflect: false` when a property should stay internal.
|
|
294
|
+
|
|
295
|
+
```javascript
|
|
296
|
+
props: {
|
|
297
|
+
label: { type: String, default: '', reflect: true },
|
|
298
|
+
token: { type: String, default: '', reflect: false }
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
Rules:
|
|
303
|
+
|
|
304
|
+
- `reflect: true` writes property changes to attributes.
|
|
305
|
+
- `reflect: false` keeps property writes off attributes.
|
|
306
|
+
- Attribute changes still update observed properties.
|
|
307
|
+
|
|
308
|
+
## 6. Updates
|
|
309
|
+
|
|
310
|
+
Updates are batched after initialization.
|
|
311
|
+
|
|
312
|
+
```javascript
|
|
313
|
+
update({ name, val, prev, refs }) {
|
|
314
|
+
if (name === 'label') {
|
|
315
|
+
refs.label.textContent = val;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (name === 'count') {
|
|
319
|
+
refs.count.textContent = String(val);
|
|
320
|
+
refs.count.dataset.prev = String(prev);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
By default, updates flush in a microtask. If the update is visual and should wait for a frame, mark the update function:
|
|
326
|
+
|
|
327
|
+
```javascript
|
|
328
|
+
function update(ctx) {
|
|
329
|
+
ctx.refs.panel.hidden = !ctx.el.open;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
update.visual = true;
|
|
333
|
+
|
|
334
|
+
ui.element('ui-panel', {
|
|
335
|
+
props: {
|
|
336
|
+
open: { type: Boolean, default: false }
|
|
337
|
+
},
|
|
338
|
+
update
|
|
339
|
+
});
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Rules:
|
|
343
|
+
|
|
344
|
+
- Keep `update` idempotent.
|
|
345
|
+
- Branch on `name`.
|
|
346
|
+
- Use `val` for the new value.
|
|
347
|
+
- Use `prev` or `old` for the previous value.
|
|
348
|
+
- Prefer property writes, `textContent`, attributes, and class lists.
|
|
349
|
+
|
|
350
|
+
## 7. Templates
|
|
351
|
+
|
|
352
|
+
External HTML templates are fetched once per component registration and cloned per instance.
|
|
353
|
+
|
|
354
|
+
```javascript
|
|
355
|
+
ui.element('ui-card', {
|
|
356
|
+
template: './index.html',
|
|
357
|
+
style: './style.css'
|
|
358
|
+
}, import.meta.url);
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
Inline templates are also supported.
|
|
362
|
+
|
|
363
|
+
```javascript
|
|
364
|
+
ui.element('ui-dot', {
|
|
365
|
+
template: '<span ref="dot" part="dot"></span>',
|
|
366
|
+
style: ':host { display: inline-block; }'
|
|
367
|
+
});
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
Rules:
|
|
371
|
+
|
|
372
|
+
- Use external files for reusable elements.
|
|
373
|
+
- Use inline templates only for tiny internal elements.
|
|
374
|
+
- Assign dynamic data through DOM APIs after mount.
|
|
375
|
+
- Avoid `innerHTML` for user data.
|
|
376
|
+
|
|
377
|
+
## 8. Refs
|
|
378
|
+
|
|
379
|
+
Use `ref="name"` for stable anchors that component code needs often.
|
|
380
|
+
|
|
381
|
+
```html
|
|
382
|
+
<button ref="button" type="button"></button>
|
|
383
|
+
<span ref="label"></span>
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Access refs in lifecycle hooks:
|
|
387
|
+
|
|
388
|
+
```javascript
|
|
389
|
+
mount({ refs }) {
|
|
390
|
+
refs.button.disabled = false;
|
|
391
|
+
refs.label.textContent = 'Ready';
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Rules:
|
|
396
|
+
|
|
397
|
+
- `refs.name` is a direct element reference.
|
|
398
|
+
- Missing refs are `undefined`.
|
|
399
|
+
- Duplicate refs warn and the first match wins.
|
|
400
|
+
- Refs are frozen for the current mount lifecycle.
|
|
401
|
+
- Use refs for stable anchors, not repeated dynamic list items.
|
|
402
|
+
|
|
403
|
+
## 9. Tags
|
|
404
|
+
|
|
405
|
+
`tags` is a cached selector helper scoped to the shadow root.
|
|
406
|
+
|
|
407
|
+
```javascript
|
|
408
|
+
mount({ tags }) {
|
|
409
|
+
const button = tags.one('button');
|
|
410
|
+
const items = tags.all('[data-item]');
|
|
411
|
+
|
|
412
|
+
tags.each('[data-item]', (item, index) => {
|
|
413
|
+
item.dataset.index = String(index);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Methods:
|
|
419
|
+
|
|
420
|
+
```javascript
|
|
421
|
+
tags.one(selector) // Element | null
|
|
422
|
+
tags.all(selector) // Element[]
|
|
423
|
+
tags.each(selector, fn) // void
|
|
424
|
+
tags.has(selector) // boolean
|
|
425
|
+
tags.clear() // void
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Rules:
|
|
429
|
+
|
|
430
|
+
- Use `refs` when an element has a stable name.
|
|
431
|
+
- Use `tags` for selector-based access.
|
|
432
|
+
- `tags.one` and `tags.all` have separate caches.
|
|
433
|
+
- The cache clears when direct shadow children change.
|
|
434
|
+
- Call `tags.clear()` after structural changes that the automatic invalidation cannot see.
|
|
435
|
+
|
|
436
|
+
## 10. Delegated Events
|
|
437
|
+
|
|
438
|
+
`on` binds delegated event handlers to the shadow root and cleans up with `ctrl.signal`.
|
|
439
|
+
|
|
440
|
+
```javascript
|
|
441
|
+
mount({ on }) {
|
|
442
|
+
on.click('button', (event, button) => {
|
|
443
|
+
button.classList.add('active');
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
on.input('input[type="search"]', (event, input) => {
|
|
447
|
+
filter(input.value);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
on.submit('form', (event, form) => {
|
|
451
|
+
event.preventDefault();
|
|
452
|
+
save(new FormData(form));
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
on['nav:change']('[data-tab]', (event, tab) => {
|
|
456
|
+
activate(tab.dataset.tab);
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Handler shape:
|
|
462
|
+
|
|
463
|
+
```javascript
|
|
464
|
+
(event, matchedElement) => void
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Supported call shapes:
|
|
468
|
+
|
|
469
|
+
```javascript
|
|
470
|
+
on.click(selector, handler)
|
|
471
|
+
on.click(selector, handler, ctrl.signal)
|
|
472
|
+
on.click(selector, handler, { signal: ctrl.signal, passive: false })
|
|
473
|
+
on.click(selector, handler, { once: true })
|
|
474
|
+
on.click.once(selector, handler)
|
|
475
|
+
on.click.once(selector, handler, ctrl.signal)
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
Rules:
|
|
479
|
+
|
|
480
|
+
- Events are delegated, so dynamically added matching children work.
|
|
481
|
+
- The matched element is found with `closest(selector)`.
|
|
482
|
+
- Invalid selectors warn and do not throw.
|
|
483
|
+
- Handlers are passive by default unless `passive: false` is passed.
|
|
484
|
+
- Use `passive: false` when calling `preventDefault()`.
|
|
485
|
+
- The returned disposer removes only that binding.
|
|
486
|
+
|
|
487
|
+
Example with a disposer:
|
|
488
|
+
|
|
489
|
+
```javascript
|
|
490
|
+
mount({ on }) {
|
|
491
|
+
const stop = on.click('.remove', remove);
|
|
492
|
+
stop();
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
## 11. Raw Events
|
|
497
|
+
|
|
498
|
+
Use raw platform events when delegation is not the right shape.
|
|
499
|
+
|
|
500
|
+
```javascript
|
|
501
|
+
mount({ refs, ctrl }) {
|
|
502
|
+
refs.scroller.addEventListener('scroll', onScroll, {
|
|
503
|
+
signal: ctrl.signal,
|
|
504
|
+
passive: true
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
refs.video.addEventListener('loadedmetadata', onLoad, {
|
|
508
|
+
signal: ctrl.signal
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
Use raw events for:
|
|
514
|
+
|
|
515
|
+
- `scroll`
|
|
516
|
+
- media events
|
|
517
|
+
- global `window` or `document` events
|
|
518
|
+
- non-bubbling events that delegation cannot catch cleanly
|
|
519
|
+
|
|
520
|
+
Always pass `ctrl.signal` or another abort signal.
|
|
521
|
+
|
|
522
|
+
## 12. Watch
|
|
523
|
+
|
|
524
|
+
`watch` observes mutations inside the component shadow root.
|
|
525
|
+
|
|
526
|
+
```javascript
|
|
527
|
+
mount({ refs, watch }) {
|
|
528
|
+
watch.attr(refs.button, 'disabled', (attr, next, prev, button) => {
|
|
529
|
+
button.dataset.wasDisabled = String(prev !== null);
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
```
|
|
533
|
+
|
|
534
|
+
Targets can be selector strings or direct elements.
|
|
535
|
+
|
|
536
|
+
```javascript
|
|
537
|
+
watch.attr('button', 'disabled', handler);
|
|
538
|
+
watch.attr(refs.button, 'disabled', handler);
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
Every watch call returns a disposer.
|
|
542
|
+
|
|
543
|
+
```javascript
|
|
544
|
+
const stop = watch.text(refs.status, sync);
|
|
545
|
+
stop();
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
The final argument can be an `AbortSignal` or an options object.
|
|
549
|
+
|
|
550
|
+
```javascript
|
|
551
|
+
watch.attr(refs.button, 'disabled', handler, ctrl.signal);
|
|
552
|
+
watch.attr(refs.button, 'disabled', handler, { signal: ctrl.signal, once: true });
|
|
553
|
+
watch.attr.once(refs.button, 'disabled', handler);
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
## 13. Attribute Watch
|
|
557
|
+
|
|
558
|
+
Use `watch.attr` for one, many, or all attributes.
|
|
559
|
+
|
|
560
|
+
```javascript
|
|
561
|
+
watch.attr('button', 'disabled', (attr, next, prev, button) => {});
|
|
562
|
+
watch.attr('.card', ['aria-expanded', 'data-state'], handler);
|
|
563
|
+
watch.attr('.card', '*', handler);
|
|
564
|
+
watch.attr.once('details', 'open', handler);
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
Handler shape:
|
|
568
|
+
|
|
569
|
+
```javascript
|
|
570
|
+
(attrName, newValue, oldValue, element) => void
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
Notes:
|
|
574
|
+
|
|
575
|
+
- `newValue` is read with `element.getAttribute(attrName)`.
|
|
576
|
+
- Removed attributes produce `null`.
|
|
577
|
+
- Boolean attributes usually produce `''` when present and `null` when absent.
|
|
578
|
+
|
|
579
|
+
## 14. Children Watch
|
|
580
|
+
|
|
581
|
+
Use `watch.kids` for child additions and removals.
|
|
582
|
+
|
|
583
|
+
```javascript
|
|
584
|
+
watch.kids('ul', ({ added, removed }, list) => {
|
|
585
|
+
for (const node of added) {
|
|
586
|
+
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
587
|
+
node.classList.add('new');
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
Direct target:
|
|
594
|
+
|
|
595
|
+
```javascript
|
|
596
|
+
watch.kids(refs.list, handler);
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
Deep child watching:
|
|
600
|
+
|
|
601
|
+
```javascript
|
|
602
|
+
watch.kids('section', { deep: true }, handler);
|
|
603
|
+
watch.kids.once('section', { deep: true }, handler);
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
Handler shape:
|
|
607
|
+
|
|
608
|
+
```javascript
|
|
609
|
+
({ added, removed }, element) => void
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
`added` and `removed` are arrays.
|
|
613
|
+
|
|
614
|
+
## 15. Text Watch
|
|
615
|
+
|
|
616
|
+
Use `watch.text` for text-node changes inside a target.
|
|
617
|
+
|
|
618
|
+
```javascript
|
|
619
|
+
watch.text(refs.counter, (next, prev, counter) => {
|
|
620
|
+
counter.dataset.changed = String(next !== prev);
|
|
621
|
+
});
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
Handler shape:
|
|
625
|
+
|
|
626
|
+
```javascript
|
|
627
|
+
(newText, oldText, element) => void
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
Rules:
|
|
631
|
+
|
|
632
|
+
- `newText` is the current `textContent`.
|
|
633
|
+
- `oldText` comes from the mutation record.
|
|
634
|
+
- `element` can be `null` if the text node no longer has a parent.
|
|
635
|
+
|
|
636
|
+
## 16. Tree Watch
|
|
637
|
+
|
|
638
|
+
Use `watch.tree` only when you need raw mutation records.
|
|
639
|
+
|
|
640
|
+
```javascript
|
|
641
|
+
watch.tree(refs.editor, (records, editor) => {
|
|
642
|
+
for (const record of records) {
|
|
643
|
+
if (record.type === 'childList') syncOutline();
|
|
644
|
+
if (record.type === 'attributes') syncToolbar();
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
Handler shape:
|
|
650
|
+
|
|
651
|
+
```javascript
|
|
652
|
+
(records, element) => void
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
Prefer `watch.attr`, `watch.kids`, or `watch.text` when the mutation type is known.
|
|
656
|
+
|
|
657
|
+
## 17. Containers
|
|
658
|
+
|
|
659
|
+
Use `ui.container` for router-owned layout slots.
|
|
660
|
+
|
|
661
|
+
```javascript
|
|
662
|
+
ui.container('ui-main', {
|
|
663
|
+
template: './index.html',
|
|
664
|
+
style: './style.css',
|
|
665
|
+
|
|
666
|
+
mount({ el, on }) {
|
|
667
|
+
on.click('[data-close]', () => {
|
|
668
|
+
el.dispatchEvent(new CustomEvent('close', {
|
|
669
|
+
bubbles: true,
|
|
670
|
+
composed: true
|
|
671
|
+
}));
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
}, import.meta.url);
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
Containers inherit the same lifecycle helpers as elements and register themselves with the router. The runtime adds `swapView(newElement, options)`.
|
|
678
|
+
|
|
679
|
+
```javascript
|
|
680
|
+
await container.swapView(page, {
|
|
681
|
+
direction: 'push'
|
|
682
|
+
});
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
Rules:
|
|
686
|
+
|
|
687
|
+
- Use containers for layout outlets.
|
|
688
|
+
- A container name is read from its `name` attribute or the tag name.
|
|
689
|
+
- Only one container with the same name can be mounted at a time.
|
|
690
|
+
- `swapView` uses element-scoped View Transitions when available, then document View Transitions, then direct replacement.
|
|
691
|
+
|
|
692
|
+
## 18. Declarative Routes
|
|
693
|
+
|
|
694
|
+
`ui.element` can register a route when `url` is provided.
|
|
695
|
+
|
|
696
|
+
```javascript
|
|
697
|
+
ui.element('page-profile', {
|
|
698
|
+
url: '/members/:member',
|
|
699
|
+
container: 'main',
|
|
700
|
+
props: {
|
|
701
|
+
member: { type: String, default: '' }
|
|
702
|
+
},
|
|
703
|
+
template: './profile.html',
|
|
704
|
+
|
|
705
|
+
mount({ el }) {
|
|
706
|
+
loadMember(el.member);
|
|
707
|
+
}
|
|
708
|
+
}, import.meta.url);
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
When the router finds the route, the UI orchestrator creates the page element and swaps it into the named container.
|
|
712
|
+
|
|
713
|
+
## 19. Form-Associated Elements
|
|
714
|
+
|
|
715
|
+
Set `form: true` to attach `ElementInternals`.
|
|
716
|
+
|
|
717
|
+
```javascript
|
|
718
|
+
ui.element('ui-field', {
|
|
719
|
+
form: true,
|
|
720
|
+
template: './index.html',
|
|
721
|
+
style: './style.css',
|
|
722
|
+
|
|
723
|
+
props: {
|
|
724
|
+
value: { type: String, default: '' },
|
|
725
|
+
required: { type: Boolean, default: false }
|
|
726
|
+
},
|
|
727
|
+
|
|
728
|
+
mount({ el, refs, internals, on }) {
|
|
729
|
+
internals.setFormValue(el.value);
|
|
730
|
+
|
|
731
|
+
on.input('input', (_event, input) => {
|
|
732
|
+
el.value = input.value;
|
|
733
|
+
internals.setFormValue(el.value);
|
|
734
|
+
}, { passive: false });
|
|
735
|
+
},
|
|
736
|
+
|
|
737
|
+
update({ el, name, val, internals, refs }) {
|
|
738
|
+
if (name === 'value') {
|
|
739
|
+
refs.input.value = val;
|
|
740
|
+
internals.setFormValue(val);
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (name === 'required') {
|
|
744
|
+
internals.setValidity(
|
|
745
|
+
val && !el.value ? { valueMissing: true } : {},
|
|
746
|
+
val && !el.value ? 'Required' : ''
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
},
|
|
750
|
+
|
|
751
|
+
associated(form) {
|
|
752
|
+
this.dataset.form = form?.id || '';
|
|
753
|
+
},
|
|
754
|
+
|
|
755
|
+
disabled(value) {
|
|
756
|
+
this.toggleAttribute('disabled', value);
|
|
757
|
+
},
|
|
758
|
+
|
|
759
|
+
reset() {
|
|
760
|
+
this.value = '';
|
|
761
|
+
},
|
|
762
|
+
|
|
763
|
+
restore(state) {
|
|
764
|
+
this.value = String(state ?? '');
|
|
765
|
+
}
|
|
766
|
+
}, import.meta.url);
|
|
767
|
+
```
|
|
768
|
+
|
|
769
|
+
Rules:
|
|
770
|
+
|
|
771
|
+
- Use `internals.setFormValue(value)` to participate in form submission.
|
|
772
|
+
- Use `internals.setValidity(flags, message)` for validation.
|
|
773
|
+
- Use `internals.form` when the element must interact with its owner form.
|
|
774
|
+
- Use `associated`, `disabled`, `reset`, and `restore` for form lifecycle hooks.
|
|
775
|
+
|
|
776
|
+
### Form Hook Mapping
|
|
777
|
+
|
|
778
|
+
The spec hook names map to the platform `ElementInternals` callbacks:
|
|
779
|
+
|
|
780
|
+
| Spec Hook | Platform Callback | Purpose |
|
|
781
|
+
| --- | --- | --- |
|
|
782
|
+
| `associated(form)` | `formAssociatedCallback` | Called when element is associated with a `<form>` |
|
|
783
|
+
| `disabled(value)` | `formDisabledCallback` | Called when the form is disabled |
|
|
784
|
+
| `reset()` | `formResetCallback` | Called when the form is reset |
|
|
785
|
+
| `restore(state, mode)` | `formStateRestoreCallback` | Called when browser restores form state |
|
|
786
|
+
|
|
787
|
+
These hooks are installed on the element prototype and called automatically by the browser.
|
|
788
|
+
|
|
789
|
+
## 20. `ui.define`
|
|
790
|
+
|
|
791
|
+
Use `ui.define` for a hand-written custom element class.
|
|
792
|
+
|
|
793
|
+
```javascript
|
|
794
|
+
import { BaseElement, ui } from '@adukiorg/anza/ui';
|
|
795
|
+
|
|
796
|
+
class NativeClock extends BaseElement {
|
|
797
|
+
mount() {
|
|
798
|
+
this.timer = setInterval(() => {
|
|
799
|
+
this.textContent = new Date().toLocaleTimeString();
|
|
800
|
+
}, 1000);
|
|
801
|
+
|
|
802
|
+
this.ctrl.signal.addEventListener('abort', () => {
|
|
803
|
+
clearInterval(this.timer);
|
|
804
|
+
}, { once: true });
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
ui.define('native-clock', NativeClock);
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
`BaseElement` creates `this.ctrl` in `connectedCallback` and aborts it in `disconnectedCallback`.
|
|
812
|
+
|
|
813
|
+
## 21. Scheduling
|
|
814
|
+
|
|
815
|
+
Use scheduler helpers for expensive work that should not block interaction.
|
|
816
|
+
|
|
817
|
+
```javascript
|
|
818
|
+
await ui.schedule(() => {
|
|
819
|
+
buildSecondaryIndex();
|
|
820
|
+
});
|
|
821
|
+
```
|
|
822
|
+
|
|
823
|
+
Priorities:
|
|
824
|
+
|
|
825
|
+
```javascript
|
|
826
|
+
await ui.schedule(work, 'user-blocking');
|
|
827
|
+
await ui.schedule(work, 'user-visible');
|
|
828
|
+
await ui.schedule(work, 'background');
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
Use `scheduleFrame` for DOM writes that should happen in a frame.
|
|
832
|
+
|
|
833
|
+
```javascript
|
|
834
|
+
await ui.scheduleFrame(() => {
|
|
835
|
+
refs.panel.hidden = false;
|
|
836
|
+
});
|
|
837
|
+
```
|
|
838
|
+
|
|
839
|
+
Use `ui.yield()` inside long loops.
|
|
840
|
+
|
|
841
|
+
```javascript
|
|
842
|
+
for (let i = 0; i < rows.length; i++) {
|
|
843
|
+
renderRow(rows[i]);
|
|
844
|
+
if (i % 50 === 0) await ui.yield();
|
|
845
|
+
}
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
Fallbacks:
|
|
849
|
+
|
|
850
|
+
- `scheduler.postTask` when available.
|
|
851
|
+
- `requestIdleCallback` for background tasks when available.
|
|
852
|
+
- `setTimeout` for broad browser support.
|
|
853
|
+
|
|
854
|
+
## 22. Observers
|
|
855
|
+
|
|
856
|
+
`ui.observe` wraps platform observers with disposer functions and optional `AbortSignal` cleanup.
|
|
857
|
+
|
|
858
|
+
```javascript
|
|
859
|
+
const stopResize = ui.observe.resize(refs.panel, (entries) => {
|
|
860
|
+
syncSize(entries[0].contentRect);
|
|
861
|
+
}, ctrl.signal);
|
|
862
|
+
|
|
863
|
+
const stopVisible = ui.observe.intersection(refs.sentinel, (entries) => {
|
|
864
|
+
if (entries[0].isIntersecting) loadMore();
|
|
865
|
+
}, ctrl.signal, {
|
|
866
|
+
rootMargin: '200px'
|
|
867
|
+
});
|
|
868
|
+
|
|
869
|
+
const stopMutation = ui.observe.mutation(refs.list, (records) => {
|
|
870
|
+
console.log(records.length);
|
|
871
|
+
}, ctrl.signal, {
|
|
872
|
+
childList: true
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
const stopPerf = ui.observe.performance(['longtask'], (list) => {
|
|
876
|
+
report(list.getEntries());
|
|
877
|
+
}, ctrl.signal);
|
|
878
|
+
```
|
|
879
|
+
|
|
880
|
+
Rules:
|
|
881
|
+
|
|
882
|
+
- Prefer injected `watch` for component shadow-root mutations.
|
|
883
|
+
- Use `ui.observe.*` for lower-level platform observer access.
|
|
884
|
+
- Pass `ctrl.signal` from component code.
|
|
885
|
+
- Call the returned disposer for early cleanup.
|
|
886
|
+
|
|
887
|
+
## 23. Transitions
|
|
888
|
+
|
|
889
|
+
`ui.transition` wraps document View Transitions and respects reduced motion.
|
|
890
|
+
|
|
891
|
+
```javascript
|
|
892
|
+
const tx = await ui.transition(() => {
|
|
893
|
+
refs.panel.replaceChildren(next);
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
await tx.finished;
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
Fallback behavior:
|
|
900
|
+
|
|
901
|
+
- If View Transitions are unsupported, the callback runs directly.
|
|
902
|
+
- If `prefers-reduced-motion: reduce` matches, the callback runs directly.
|
|
903
|
+
- The returned object has `finished`, `updateCallbackDone`, `ready`, and `skipTransition`.
|
|
904
|
+
|
|
905
|
+
For router containers, prefer `swapView`.
|
|
906
|
+
|
|
907
|
+
## 24. Template Helper
|
|
908
|
+
|
|
909
|
+
`ui.template` creates a cached fragment from a tagged template literal.
|
|
910
|
+
|
|
911
|
+
```javascript
|
|
912
|
+
const row = ui.template`
|
|
913
|
+
<li class="row">
|
|
914
|
+
<span ref="label"></span>
|
|
915
|
+
</li>
|
|
916
|
+
`;
|
|
917
|
+
|
|
918
|
+
row.querySelector('[ref="label"]').textContent = label;
|
|
919
|
+
refs.list.appendChild(row);
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
Interpolated values are intentionally ignored.
|
|
923
|
+
|
|
924
|
+
```javascript
|
|
925
|
+
const bad = ui.template`<span>${label}</span>`;
|
|
926
|
+
```
|
|
927
|
+
|
|
928
|
+
Use DOM APIs after cloning:
|
|
929
|
+
|
|
930
|
+
```javascript
|
|
931
|
+
const item = ui.template`<li><span class="label"></span></li>`;
|
|
932
|
+
item.querySelector('.label').textContent = label;
|
|
933
|
+
```
|
|
934
|
+
|
|
935
|
+
Rules:
|
|
936
|
+
|
|
937
|
+
- Use static markup inside the tagged template.
|
|
938
|
+
- Do not put user data into template strings.
|
|
939
|
+
- Clone first, then assign dynamic data with DOM APIs.
|
|
940
|
+
|
|
941
|
+
## 25. Template Scanning
|
|
942
|
+
|
|
943
|
+
The Rust tool scans component HTML and emits `.tags.json` descriptors used for refs and tag prewarming.
|
|
944
|
+
|
|
945
|
+
Commands:
|
|
946
|
+
|
|
947
|
+
```bash
|
|
948
|
+
anza --src src --build
|
|
949
|
+
anza --src src --port 3000
|
|
950
|
+
anza scan --src src
|
|
951
|
+
anza scan --src src --watch
|
|
952
|
+
anza build --src src --dist dist
|
|
953
|
+
anza dev --src src --port 3000
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
Descriptor shape:
|
|
957
|
+
|
|
958
|
+
```json
|
|
959
|
+
{
|
|
960
|
+
"version": 1,
|
|
961
|
+
"refs": ["button", "status"],
|
|
962
|
+
"ids": ["button"],
|
|
963
|
+
"classes": ["btn", "status"],
|
|
964
|
+
"tags": ["button", "span"],
|
|
965
|
+
"compound": ["button.btn", "span.status"],
|
|
966
|
+
"attrs": ["type", "hidden"],
|
|
967
|
+
"refTypes": {
|
|
968
|
+
"button": "HTMLButtonElement",
|
|
969
|
+
"status": "HTMLSpanElement"
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
```
|
|
973
|
+
|
|
974
|
+
Rules:
|
|
975
|
+
|
|
976
|
+
- Descriptors are optional at runtime.
|
|
977
|
+
- If a descriptor is missing, refs are discovered by scanning `[ref]`.
|
|
978
|
+
- Malformed descriptor fields are ignored safely.
|
|
979
|
+
- Keep descriptors committed when generated for library components.
|
|
980
|
+
- Regenerate descriptors after template structure changes.
|
|
981
|
+
|
|
982
|
+
The toolchain also scans `ui.element` specs and emits `routes.json` for declarative routes.
|
|
983
|
+
|
|
984
|
+
```json
|
|
985
|
+
{
|
|
986
|
+
"version": 1,
|
|
987
|
+
"routes": [
|
|
988
|
+
{
|
|
989
|
+
"tag": "page-member",
|
|
990
|
+
"path": "/members/:member",
|
|
991
|
+
"container": "main",
|
|
992
|
+
"params": ["member"]
|
|
993
|
+
}
|
|
994
|
+
]
|
|
995
|
+
}
|
|
996
|
+
```
|
|
997
|
+
|
|
998
|
+
## 26. Typing Components
|
|
999
|
+
|
|
1000
|
+
The package exports TypeScript declarations for strict component contexts.
|
|
1001
|
+
|
|
1002
|
+
```typescript
|
|
1003
|
+
import { ui, type MountContext, type UpdateContext } from '@adukiorg/anza/ui';
|
|
1004
|
+
|
|
1005
|
+
interface Refs {
|
|
1006
|
+
button: HTMLButtonElement;
|
|
1007
|
+
status: HTMLSpanElement;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const props = {
|
|
1011
|
+
count: { type: Number, default: 0 },
|
|
1012
|
+
open: { type: Boolean, default: false },
|
|
1013
|
+
label: { type: String, default: '' }
|
|
1014
|
+
};
|
|
1015
|
+
|
|
1016
|
+
ui.element<'ui-typed', typeof props, Refs>('ui-typed', {
|
|
1017
|
+
props,
|
|
1018
|
+
|
|
1019
|
+
mount({ el, refs, tags, on, watch }) {
|
|
1020
|
+
el.count.toFixed();
|
|
1021
|
+
refs.button.disabled = true;
|
|
1022
|
+
|
|
1023
|
+
const input = tags.one<HTMLInputElement>('input');
|
|
1024
|
+
input?.value.trim();
|
|
1025
|
+
|
|
1026
|
+
on.click<HTMLButtonElement>('button', (event, button) => {
|
|
1027
|
+
event.clientX.toFixed();
|
|
1028
|
+
button.disabled = false;
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
watch.attr(refs.button, 'disabled', (_attr, next, prev, button) => {
|
|
1032
|
+
button.disabled = next !== null;
|
|
1033
|
+
});
|
|
1034
|
+
},
|
|
1035
|
+
|
|
1036
|
+
update(ctx) {
|
|
1037
|
+
if (ctx.name === 'count') {
|
|
1038
|
+
ctx.val.toFixed();
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
if (ctx.name === 'label') {
|
|
1042
|
+
ctx.val.trim();
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
declare const mount: MountContext<typeof props, Refs>;
|
|
1048
|
+
mount.refs.button.disabled = false;
|
|
1049
|
+
|
|
1050
|
+
declare const update: UpdateContext<typeof props, Refs>;
|
|
1051
|
+
if (update.name === 'open') {
|
|
1052
|
+
update.val.valueOf();
|
|
1053
|
+
}
|
|
1054
|
+
```
|
|
1055
|
+
|
|
1056
|
+
For deeper type details, see `src/core/ui/ui.types.md`.
|
|
1057
|
+
|
|
1058
|
+
## 27. Security
|
|
1059
|
+
|
|
1060
|
+
- Avoid `innerHTML` for dynamic content.
|
|
1061
|
+
- Use `textContent` for text.
|
|
1062
|
+
- Use properties, attributes, and class lists for state.
|
|
1063
|
+
- Validate tag names before creating dynamic elements.
|
|
1064
|
+
- Dispatch outward with composed custom events when the event must cross shadow boundaries.
|
|
1065
|
+
- Treat slotted content as external input.
|
|
1066
|
+
|
|
1067
|
+
Safe custom event:
|
|
1068
|
+
|
|
1069
|
+
```javascript
|
|
1070
|
+
el.dispatchEvent(new CustomEvent('activate', {
|
|
1071
|
+
bubbles: true,
|
|
1072
|
+
composed: true,
|
|
1073
|
+
detail: { value: el.value }
|
|
1074
|
+
}));
|
|
1075
|
+
```
|
|
1076
|
+
|
|
1077
|
+
## 28. Performance
|
|
1078
|
+
|
|
1079
|
+
- Prefer `refs` for stable anchors.
|
|
1080
|
+
- Prefer `tags` over repeated `shadowRoot.querySelector`.
|
|
1081
|
+
- Prefer delegated `on` over repeated child listeners.
|
|
1082
|
+
- Prefer `watch` over ad hoc `MutationObserver` inside shadow roots.
|
|
1083
|
+
- Use `ui.schedule` for expensive non-visual work.
|
|
1084
|
+
- Use `ui.scheduleFrame` for frame-synced DOM writes.
|
|
1085
|
+
- Use `ui.yield` in long loops.
|
|
1086
|
+
- Keep update handlers small and branch by `name`.
|
|
1087
|
+
- Clear `tags` after structural changes not covered by direct shadow child invalidation.
|
|
1088
|
+
|
|
1089
|
+
## 29. Escape Hatches
|
|
1090
|
+
|
|
1091
|
+
Use platform APIs directly when they are clearer.
|
|
1092
|
+
|
|
1093
|
+
```javascript
|
|
1094
|
+
refs.input.focus();
|
|
1095
|
+
refs.dialog.showModal();
|
|
1096
|
+
refs.video.play();
|
|
1097
|
+
refs.scroller.scrollTo({ top: 0, behavior: 'smooth' });
|
|
1098
|
+
```
|
|
1099
|
+
|
|
1100
|
+
Rules:
|
|
1101
|
+
|
|
1102
|
+
- Keep cleanup tied to `ctrl.signal`.
|
|
1103
|
+
- Keep DOM writes explicit.
|
|
1104
|
+
- Do not hide heavy work in property setters.
|
|
1105
|
+
- Do not rely on undocumented spec fields.
|
|
1106
|
+
|
|
1107
|
+
## 30. Checklist
|
|
1108
|
+
|
|
1109
|
+
- Use `ui.element` for components and pages.
|
|
1110
|
+
- Use `ui.container` only for router-owned layout slots.
|
|
1111
|
+
- Use `ui.define` for hand-written element classes.
|
|
1112
|
+
- Keep each component's `index.js`, `index.html`, and `style.css` together.
|
|
1113
|
+
- Pass `import.meta.url` for relative template or style assets.
|
|
1114
|
+
- Prefer `refs` for stable anchors.
|
|
1115
|
+
- Prefer `tags` for cached selector access.
|
|
1116
|
+
- Prefer `on` for delegated shadow-root events.
|
|
1117
|
+
- Use `passive: false` when a delegated handler calls `preventDefault()`.
|
|
1118
|
+
- Prefer `watch` for shadow-root mutations.
|
|
1119
|
+
- Prefer `ui.observe` for raw platform observers.
|
|
1120
|
+
- Use scheduler helpers for heavy or deferred work.
|
|
1121
|
+
- Keep cleanup tied to `ctrl.signal`.
|
|
1122
|
+
- Use `textContent` and properties for dynamic data.
|
|
1123
|
+
- Avoid `innerHTML` for user data.
|
|
1124
|
+
- Dispatch outward with `CustomEvent`.
|