@usenagi/core 0.1.0 → 0.3.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/README.ja.md +269 -0
- package/README.md +106 -79
- package/dist/addons/scheduler.cjs.js +1 -1
- package/dist/addons/scheduler.es.js +76 -17
- package/dist/main.es.js +56 -71
- package/dist/main.umd.js +1 -1
- package/package.json +1 -1
- package/types/addons/scheduler/addon.d.ts +10 -0
- package/types/addons/scheduler/index.d.ts +2 -4
- package/types/addons/scheduler/pending.d.ts +11 -0
- package/types/addons/scheduler/scheduler.d.ts +4 -0
- package/types/core/addon.d.ts +35 -0
- package/types/core/app.d.ts +6 -23
- package/types/core/component.d.ts +14 -5
- package/types/core/reactivity.d.ts +9 -9
- package/types/core/runtime.d.ts +1 -1
- package/types/hooks/useMediaQuery.d.ts +1 -1
- package/types/hooks/useSlot.d.ts +1 -1
- package/types/main.d.ts +5 -3
- package/types/props.d.ts +2 -0
- package/types/types.d.ts +2 -2
- package/types/core/internal/pending.d.ts +0 -11
- package/types/hooks/useRootRef.d.ts +0 -2
package/README.ja.md
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
[English](./README.md) | **日本語**
|
|
2
|
+
|
|
3
|
+
# nagi
|
|
4
|
+
|
|
5
|
+
**Composition-style ergonomics for vanilla DOM. Bring your own mounter.**
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/@usenagi/core)
|
|
8
|
+
[](https://bundlephobia.com/package/@usenagi/core)
|
|
9
|
+
[](./LICENSE)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Why nagi?
|
|
14
|
+
|
|
15
|
+
**既存 HTML に小さく足せる**
|
|
16
|
+
|
|
17
|
+
WordPress、CMS、Webflow、静的サイトなどに、仮想 DOM やテンプレートを持ち込まず、`setup()` / lifecycle / reactivity を追加できる。
|
|
18
|
+
|
|
19
|
+
**アニメーションと相性が良い**
|
|
20
|
+
|
|
21
|
+
GSAP、Lenis、IntersectionObserver などを `setup()` で初期化し、`useUnmount()` でクリーンアップできる。
|
|
22
|
+
|
|
23
|
+
**マウント戦略を縛らない**
|
|
24
|
+
|
|
25
|
+
`[data-component]` スキャン、manifest、lazy import、MutationObserver などは、利用側で自由に組み立てられる。
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## 30-second example
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// counter.ts
|
|
33
|
+
import { create, signal, useWatch, useDomRef } from "@usenagi/core";
|
|
34
|
+
|
|
35
|
+
const { component } = create();
|
|
36
|
+
|
|
37
|
+
component({
|
|
38
|
+
name: "counter",
|
|
39
|
+
setup() {
|
|
40
|
+
const { refs } = useDomRef<{
|
|
41
|
+
count: HTMLSpanElement;
|
|
42
|
+
btn: HTMLButtonElement;
|
|
43
|
+
}>();
|
|
44
|
+
|
|
45
|
+
const n = signal(0);
|
|
46
|
+
useWatch(n, (v) => {
|
|
47
|
+
refs.count.textContent = String(v);
|
|
48
|
+
});
|
|
49
|
+
refs.btn.addEventListener("click", () => {
|
|
50
|
+
n.value++;
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
})(document.querySelector("#counter")!);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```html
|
|
57
|
+
<div id="counter">
|
|
58
|
+
<span data-ref="count">0</span>
|
|
59
|
+
<button data-ref="btn">+</button>
|
|
60
|
+
</div>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Quick start
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
npm i @usenagi/core
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### First component
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { create, defineComponent, signal, useWatch, useDomRef } from "@usenagi/core";
|
|
75
|
+
|
|
76
|
+
const Greeting = defineComponent({
|
|
77
|
+
name: "greeting",
|
|
78
|
+
setup(el, props) {
|
|
79
|
+
const { refs } = useDomRef<{ message: HTMLParagraphElement }>();
|
|
80
|
+
const text = signal((props.name as string) ?? "world");
|
|
81
|
+
|
|
82
|
+
useWatch(text, (v) => {
|
|
83
|
+
refs.message.textContent = `Hello, ${v}!`;
|
|
84
|
+
});
|
|
85
|
+
refs.message.textContent = `Hello, ${text.value}!`;
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
create().component(Greeting)(document.querySelector("#app")!);
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Scheduler + deferred mount
|
|
93
|
+
|
|
94
|
+
遅延マウントが必要な場合は、scheduler / cue addons を追加する。
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { create } from "@usenagi/core";
|
|
98
|
+
import { schedulerAddon } from "@usenagi/core/addons/scheduler";
|
|
99
|
+
import { visible, idle } from "@usenagi/core/addons/cue";
|
|
100
|
+
|
|
101
|
+
const app = create().install(schedulerAddon());
|
|
102
|
+
|
|
103
|
+
// mount when the element enters the viewport
|
|
104
|
+
app.component(HeavyWidget, { when: visible() })(el);
|
|
105
|
+
|
|
106
|
+
// mount during browser idle time
|
|
107
|
+
app.component(Analytics, { when: idle() })(el);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`schedulerAddon()` を使うと、`when` は `setup()` の前に待機する条件、`priority` は `setup()` を含む mount task の実行タイミングを決める。
|
|
111
|
+
|
|
112
|
+
### BYO mounter recipe
|
|
113
|
+
|
|
114
|
+
`[data-component]` スキャン、manifest、cue を組み合わせた自動マウントの例。
|
|
115
|
+
→ [examples/recipes/byo-mounter](./examples/recipes/byo-mounter/main.ts)
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## API
|
|
120
|
+
|
|
121
|
+
### Reactivity
|
|
122
|
+
|
|
123
|
+
| API | 説明 |
|
|
124
|
+
| ---------------------- | ------------------------------------------------------ |
|
|
125
|
+
| `signal(value)` | `.value` を持つリアクティブな値コンテナを作成する |
|
|
126
|
+
| `readonly(signal)` | 書き込み可能な `signal` の読み取り専用ラッパー |
|
|
127
|
+
| `useComputed(fn)` | `signal` の依存を自動追跡する派生値 |
|
|
128
|
+
| `useWatch(target, cb)` | 値変更時に `cb` を呼ぶ。unmount 時に自動で購読解除する |
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
const width = signal(10);
|
|
132
|
+
const height = signal(5);
|
|
133
|
+
const area = useComputed(() => width.value * height.value); // auto-recomputed
|
|
134
|
+
|
|
135
|
+
useWatch(area, (v) => {
|
|
136
|
+
output.textContent = String(v);
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Lifecycle
|
|
141
|
+
|
|
142
|
+
| API | 説明 |
|
|
143
|
+
| ---------------- | ------------------------------------------- |
|
|
144
|
+
| `useMount(fn)` | コンポーネントのマウント完了後に1回実行する |
|
|
145
|
+
| `useUnmount(fn)` | unmount 時に実行する。クリーンアップに使う |
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
import gsap from 'gsap';
|
|
149
|
+
|
|
150
|
+
setup(el) {
|
|
151
|
+
const tween = gsap.from(el, { opacity: 0, duration: 0.4 });
|
|
152
|
+
useUnmount(() => tween.kill());
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### DOM helpers
|
|
157
|
+
|
|
158
|
+
ルート要素には **`setup(el)`** を、**`[data-ref]`** の子要素には **`useDomRef()`** を使う。
|
|
159
|
+
|
|
160
|
+
| API | 説明 |
|
|
161
|
+
| ------------------------------ | ------------------------------------------------------- |
|
|
162
|
+
| `useDomRef<T>()` | `[data-ref]` 要素への型付きアクセス |
|
|
163
|
+
| `useEvent(el, event, handler)` | イベントリスナーを追加する。unmount 時に自動で除去する |
|
|
164
|
+
| `useSlot()` | 子コンポーネントをマウントする。親の unmount に連動する |
|
|
165
|
+
|
|
166
|
+
### Parent / child
|
|
167
|
+
|
|
168
|
+
`useSlot()` で子コンポーネントをマウントできる。親から子へは `props` または `createContext` / `withContext` で値を渡せる。`addChild()` が返す child context から、子の `setup()` の返り値も参照できる。
|
|
169
|
+
|
|
170
|
+
→ [examples/parent-child](./examples/parent-child/main.ts)
|
|
171
|
+
|
|
172
|
+
### Observers
|
|
173
|
+
|
|
174
|
+
| API | 説明 |
|
|
175
|
+
| --------------------------------- | ----------------------------------------------------------- |
|
|
176
|
+
| `useIntersectionWatch(cb, opts?)` | IntersectionObserver のラッパー。unmount 時に自動で切断する |
|
|
177
|
+
| `useMediaQuery(query)` | `matchMedia` の結果を `ReadonlySignal<boolean>` で返す |
|
|
178
|
+
|
|
179
|
+
### Addons
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import { create, defineAddon } from "@usenagi/core";
|
|
183
|
+
import { schedulerAddon } from "@usenagi/core/addons/scheduler";
|
|
184
|
+
|
|
185
|
+
const app = create().install(schedulerAddon(), myAddon());
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
| API | 説明 |
|
|
189
|
+
| --- | --- |
|
|
190
|
+
| `defineAddon({ name, install(ctx) })` | addon を定義する(`ctx` は `AddonContext`) |
|
|
191
|
+
| `app.install(...addons)` | app に addon を登録する(複数可) |
|
|
192
|
+
| `ctx.addMountMiddleware` / `addUnmountMiddleware` / `addComponentMiddleware` | mount / unmount / ComponentSetup の middleware を追加する |
|
|
193
|
+
| `ctx.installedAddons` | この app に install 済みの addon 名 |
|
|
194
|
+
|
|
195
|
+
`addMountMiddleware` / `addUnmountMiddleware` / `addComponentMiddleware` は **後から install した addon ほど外側**に適用される(`install(a, b)` なら実行順は `b → a → コア`)。
|
|
196
|
+
|
|
197
|
+
遅延 mount には `schedulerAddon()` が必要。`when` や `priority` を使う場合も同様で、これらの mount option は scheduler addon が解釈する。addon の状態(scheduler / pending)は **各 app の `install` ごと**に作られる。
|
|
198
|
+
|
|
199
|
+
#### Scheduler + cue
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
import { schedulerAddon } from "@usenagi/core/addons/scheduler";
|
|
203
|
+
import { visible, idle, interaction, media } from "@usenagi/core/addons/cue";
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
| API | 説明 |
|
|
207
|
+
| --- | --- |
|
|
208
|
+
| `schedulerAddon(opts?)` | 遅延 mount 用 addon(内部で `createScheduler` を使用) |
|
|
209
|
+
| `createScheduler(opts?)` | カスタム Scheduler 実装用の low-level API |
|
|
210
|
+
| `visible(opts?)` | 要素が viewport に入ったときに解決する Cue |
|
|
211
|
+
| `idle(timeout?)` | `requestIdleCallback` で解決する Cue |
|
|
212
|
+
| `interaction(events?)` | 最初のユーザー操作で解決する Cue |
|
|
213
|
+
| `media(query)` | media query が一致したときに解決する Cue |
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Comparison
|
|
218
|
+
|
|
219
|
+
| | **nagi** | Alpine.js | Stimulus | petite-vue |
|
|
220
|
+
| -------------------------- | -------- | --------- | -------- | ---------- |
|
|
221
|
+
| Inline JS in HTML | ✗ | ◯ | ✗ | ◯ |
|
|
222
|
+
| Composition-style setup | ◯ | △ | ✗ | ◯ |
|
|
223
|
+
| BYO mounter | ◯ | △ | △ | △ |
|
|
224
|
+
| Async mount cue | ◯ | ✗ | ✗ | ✗ |
|
|
225
|
+
| Lifecycle cleanup | ◯ | △ | ◯ | △ |
|
|
226
|
+
| computed (derived signals) | ◯ | ◯ | ✗ | ◯ |
|
|
227
|
+
| Core gzip | ~2.5 kB | ~16 kB | ~8 kB | ~6 kB |
|
|
228
|
+
|
|
229
|
+
(◯ = 組み込み、△ = 利用側の実装・規約で対応可能、✗ = 主な機能ではない)
|
|
230
|
+
|
|
231
|
+
- **vs Alpine / petite-vue**: HTML に式を直接書かず、ロジックを `.ts` に集約する。
|
|
232
|
+
- **vs Stimulus**: Controller 規約はない。マウント戦略は利用側で自由に組み立てられる。
|
|
233
|
+
- **vs React / Vue**: 宣言的 UI フレームワークではなく、既存 DOM に lifecycle を足す薄いレイヤー。
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## When to use / When not to
|
|
238
|
+
|
|
239
|
+
**向いているケース:**
|
|
240
|
+
|
|
241
|
+
- React や Vue のランタイムを持ち込みにくいプロジェクト(CMS、Webflow、WordPress など)
|
|
242
|
+
- GSAP や Lenis を多用する、アニメーション主体のサイト
|
|
243
|
+
- ページの一部だけにインタラクティブな UI を追加したい場合
|
|
244
|
+
- `setup()`、lifecycle、reactivity による composition-style で書きたいが、仮想 DOM は不要な場合
|
|
245
|
+
|
|
246
|
+
**向いていないケース:**
|
|
247
|
+
|
|
248
|
+
- リスト描画や条件分岐を HTML テンプレートで書きたい場合(`v-for` や `v-if` 相当はない)
|
|
249
|
+
- 複雑なオブジェクトの深いリアクティビティが必要な場合(`reactive({})` は提供しない)
|
|
250
|
+
- SSR / hydration が必要な場合
|
|
251
|
+
- 状態管理、ルーティング、宣言的な view rendering をフレームワークにまとめて任せたい場合
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
## Examples
|
|
256
|
+
|
|
257
|
+
| Example | 説明 |
|
|
258
|
+
| ----------------------------------------------------- | --------------------------------------------------- |
|
|
259
|
+
| [basic-counter](./examples/basic-counter/) | 最小の `signal` + `useWatch` 例 |
|
|
260
|
+
| [computed](./examples/computed/) | `useComputed` による派生値(width × height = area) |
|
|
261
|
+
| [parent-child](./examples/parent-child/) | `createContext` + `withContext` + `useSlot` |
|
|
262
|
+
| [lenis-scroll-scene](./examples/lenis-scroll-scene/) | Lenis + `useComputed` によるスクロール進捗連動 |
|
|
263
|
+
| [byo-mounter recipe](./examples/recipes/byo-mounter/) | `[data-component]` スキャン + manifest + cue |
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
## License
|
|
268
|
+
|
|
269
|
+
MIT © [hayakawasho](https://github.com/hayakawasho)
|
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
**English** | [日本語](./README.ja.md)
|
|
2
|
+
|
|
1
3
|
# nagi
|
|
2
4
|
|
|
3
5
|
**Composition-style ergonomics for vanilla DOM. Bring your own mounter.**
|
|
@@ -10,14 +12,17 @@
|
|
|
10
12
|
|
|
11
13
|
## Why nagi?
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
**Can be added in small parts to existing HTML**
|
|
16
|
+
|
|
17
|
+
You can add `setup()`, lifecycle, and reactivity to WordPress, CMS, Webflow, static sites, etc., without introducing a virtual DOM or templates.
|
|
18
|
+
|
|
19
|
+
**Compatible with animation**
|
|
20
|
+
|
|
21
|
+
You can initialize GSAP, Lenis, IntersectionObserver, etc., in `setup()` and clean them up with `useUnmount()`.
|
|
15
22
|
|
|
16
|
-
|
|
17
|
-
GSAP、Lenis、IntersectionObserver などを `setup()` で初期化し、`useUnmount()` でクリーンアップできる。
|
|
23
|
+
**Does not restrict mounting strategies**
|
|
18
24
|
|
|
19
|
-
|
|
20
|
-
`[data-component]` スキャン、manifest、lazy import、MutationObserver などは利用側で自由に組める。
|
|
25
|
+
You are free to implement `[data-component]` scanning, manifests, lazy imports, MutationObserver, and so on, on the consuming side.
|
|
21
26
|
|
|
22
27
|
---
|
|
23
28
|
|
|
@@ -25,7 +30,7 @@ GSAP、Lenis、IntersectionObserver などを `setup()` で初期化し、`useUn
|
|
|
25
30
|
|
|
26
31
|
```ts
|
|
27
32
|
// counter.ts
|
|
28
|
-
import { create,
|
|
33
|
+
import { create, signal, useWatch, useDomRef } from "@usenagi/core";
|
|
29
34
|
|
|
30
35
|
const { component } = create();
|
|
31
36
|
|
|
@@ -37,7 +42,7 @@ component({
|
|
|
37
42
|
btn: HTMLButtonElement;
|
|
38
43
|
}>();
|
|
39
44
|
|
|
40
|
-
const n =
|
|
45
|
+
const n = signal(0);
|
|
41
46
|
useWatch(n, (v) => {
|
|
42
47
|
refs.count.textContent = String(v);
|
|
43
48
|
});
|
|
@@ -66,13 +71,13 @@ npm i @usenagi/core
|
|
|
66
71
|
### First component
|
|
67
72
|
|
|
68
73
|
```ts
|
|
69
|
-
import { create, defineComponent,
|
|
74
|
+
import { create, defineComponent, signal, useWatch, useDomRef } from "@usenagi/core";
|
|
70
75
|
|
|
71
76
|
const Greeting = defineComponent({
|
|
72
77
|
name: "greeting",
|
|
73
78
|
setup(el, props) {
|
|
74
79
|
const { refs } = useDomRef<{ message: HTMLParagraphElement }>();
|
|
75
|
-
const text =
|
|
80
|
+
const text = signal((props.name as string) ?? "world");
|
|
76
81
|
|
|
77
82
|
useWatch(text, (v) => {
|
|
78
83
|
refs.message.textContent = `Hello, ${v}!`;
|
|
@@ -86,27 +91,27 @@ create().component(Greeting)(document.querySelector("#app")!);
|
|
|
86
91
|
|
|
87
92
|
### Scheduler + deferred mount
|
|
88
93
|
|
|
89
|
-
|
|
94
|
+
If delayed mounting is required, add the scheduler / cue addons.
|
|
90
95
|
|
|
91
96
|
```ts
|
|
92
97
|
import { create } from "@usenagi/core";
|
|
93
|
-
import {
|
|
98
|
+
import { schedulerAddon } from "@usenagi/core/addons/scheduler";
|
|
94
99
|
import { visible, idle } from "@usenagi/core/addons/cue";
|
|
95
100
|
|
|
96
|
-
const app = create(
|
|
101
|
+
const app = create().install(schedulerAddon());
|
|
97
102
|
|
|
98
|
-
//
|
|
103
|
+
// mount when the element enters the viewport
|
|
99
104
|
app.component(HeavyWidget, { when: visible() })(el);
|
|
100
105
|
|
|
101
|
-
//
|
|
106
|
+
// mount during browser idle time
|
|
102
107
|
app.component(Analytics, { when: idle() })(el);
|
|
103
108
|
```
|
|
104
109
|
|
|
105
|
-
`when`
|
|
110
|
+
With `schedulerAddon()`, `when` is a condition to wait for before `setup()`, and `priority` determines the execution timing of the mount task that includes `setup()`.
|
|
106
111
|
|
|
107
112
|
### BYO mounter recipe
|
|
108
113
|
|
|
109
|
-
`[data-component]`
|
|
114
|
+
An example of automatic mounting by combining `[data-component]` scanning, manifests, and cues.
|
|
110
115
|
→ [examples/recipes/byo-mounter](./examples/recipes/byo-mounter/main.ts)
|
|
111
116
|
|
|
112
117
|
---
|
|
@@ -115,17 +120,17 @@ app.component(Analytics, { when: idle() })(el);
|
|
|
115
120
|
|
|
116
121
|
### Reactivity
|
|
117
122
|
|
|
118
|
-
| API
|
|
119
|
-
|
|
|
120
|
-
| `
|
|
121
|
-
| `readonly(
|
|
122
|
-
| `
|
|
123
|
-
| `useWatch(
|
|
123
|
+
| API | Description |
|
|
124
|
+
| ---------------------- | ----------------------------------------------------------------- |
|
|
125
|
+
| `signal(value)` | Creates a reactive value container (`.value`) |
|
|
126
|
+
| `readonly(signal)` | Read-only wrapper around a writable `signal` |
|
|
127
|
+
| `useComputed(fn)` | Derived value that auto-tracks `signal` dependencies |
|
|
128
|
+
| `useWatch(target, cb)` | Calls `cb` on value change; automatically unsubscribes on unmount |
|
|
124
129
|
|
|
125
130
|
```ts
|
|
126
|
-
const width =
|
|
127
|
-
const height =
|
|
128
|
-
const area =
|
|
131
|
+
const width = signal(10);
|
|
132
|
+
const height = signal(5);
|
|
133
|
+
const area = useComputed(() => width.value * height.value); // auto-recomputed
|
|
129
134
|
|
|
130
135
|
useWatch(area, (v) => {
|
|
131
136
|
output.textContent = String(v);
|
|
@@ -134,10 +139,10 @@ useWatch(area, (v) => {
|
|
|
134
139
|
|
|
135
140
|
### Lifecycle
|
|
136
141
|
|
|
137
|
-
| API |
|
|
138
|
-
| ---------------- |
|
|
139
|
-
| `useMount(fn)` |
|
|
140
|
-
| `useUnmount(fn)` |
|
|
142
|
+
| API | Description |
|
|
143
|
+
| ---------------- | ------------------------------------ |
|
|
144
|
+
| `useMount(fn)` | Runs once after the component mounts |
|
|
145
|
+
| `useUnmount(fn)` | Runs on unmount; use for cleanup |
|
|
141
146
|
|
|
142
147
|
```ts
|
|
143
148
|
import gsap from 'gsap';
|
|
@@ -150,90 +155,112 @@ setup(el) {
|
|
|
150
155
|
|
|
151
156
|
### DOM helpers
|
|
152
157
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
|
156
|
-
|
|
|
157
|
-
| `
|
|
158
|
-
| `
|
|
158
|
+
Use **`setup(el)`** for the root element and **`useDomRef()`** for `[data-ref]` descendants.
|
|
159
|
+
|
|
160
|
+
| API | Description |
|
|
161
|
+
| ------------------------------ | -------------------------------------------------------- |
|
|
162
|
+
| `useDomRef<T>()` | Typed access to `[data-ref]` elements |
|
|
163
|
+
| `useEvent(el, event, handler)` | Adds an event listener; automatically removed on unmount |
|
|
164
|
+
| `useSlot()` | Mounts child components; tied to the parent's unmount |
|
|
159
165
|
|
|
160
166
|
### Parent / child
|
|
161
167
|
|
|
162
|
-
`useSlot()
|
|
168
|
+
You can mount child components with `useSlot()`. You can pass values from parent to child via `props` or `createContext` / `withContext`. From the child context returned by `addChild()`, you can also reference the return value of the child's `setup()`.
|
|
163
169
|
|
|
164
170
|
→ [examples/parent-child](./examples/parent-child/main.ts)
|
|
165
171
|
|
|
166
172
|
### Observers
|
|
167
173
|
|
|
168
|
-
| API |
|
|
169
|
-
| --------------------------------- |
|
|
170
|
-
| `useIntersectionWatch(cb, opts?)` | IntersectionObserver
|
|
171
|
-
| `useMediaQuery(query)` | `matchMedia`
|
|
174
|
+
| API | Description |
|
|
175
|
+
| --------------------------------- | ------------------------------------------------------------------- |
|
|
176
|
+
| `useIntersectionWatch(cb, opts?)` | IntersectionObserver wrapper; automatically disconnected on unmount |
|
|
177
|
+
| `useMediaQuery(query)` | Returns `matchMedia` result as a `ReadonlySignal<boolean>` |
|
|
172
178
|
|
|
173
179
|
### Addons
|
|
174
180
|
|
|
175
181
|
```ts
|
|
176
|
-
import {
|
|
182
|
+
import { create, defineAddon } from "@usenagi/core";
|
|
183
|
+
import { schedulerAddon } from "@usenagi/core/addons/scheduler";
|
|
184
|
+
|
|
185
|
+
const app = create().install(schedulerAddon(), myAddon());
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
| API | Description |
|
|
189
|
+
| --- | --- |
|
|
190
|
+
| `defineAddon({ name, install(ctx) })` | Defines an addon (`ctx` is `AddonContext`) |
|
|
191
|
+
| `app.install(...addons)` | Registers one or more addons on the app |
|
|
192
|
+
| `ctx.addMountMiddleware` / `addUnmountMiddleware` / `addComponentMiddleware` | Add mount / unmount / ComponentSetup middleware |
|
|
193
|
+
| `ctx.installedAddons` | Addon names already installed on this app |
|
|
194
|
+
|
|
195
|
+
`addMountMiddleware`, `addUnmountMiddleware`, and `addComponentMiddleware` apply **outermost for addons installed later** (`install(a, b)` runs as `b → a → core`).
|
|
196
|
+
|
|
197
|
+
Deferred mounting requires `schedulerAddon()`. The same applies when using `when` or `priority`; these mount options are interpreted by the scheduler addon. Addon state (scheduler / pending) is created **per app `install`**, not per addon instance.
|
|
198
|
+
|
|
199
|
+
#### Scheduler + cue
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
import { schedulerAddon } from "@usenagi/core/addons/scheduler";
|
|
177
203
|
import { visible, idle, interaction, media } from "@usenagi/core/addons/cue";
|
|
178
204
|
```
|
|
179
205
|
|
|
180
|
-
| API
|
|
181
|
-
|
|
|
182
|
-
| `
|
|
183
|
-
| `
|
|
184
|
-
| `
|
|
185
|
-
| `
|
|
186
|
-
| `
|
|
206
|
+
| API | Description |
|
|
207
|
+
| --- | --- |
|
|
208
|
+
| `schedulerAddon(opts?)` | Addon for deferred mount (uses `createScheduler` internally) |
|
|
209
|
+
| `createScheduler(opts?)` | Low-level API for custom Scheduler implementations |
|
|
210
|
+
| `visible(opts?)` | A Cue that resolves when the element enters the viewport |
|
|
211
|
+
| `idle(timeout?)` | A Cue that resolves via `requestIdleCallback` |
|
|
212
|
+
| `interaction(events?)` | A Cue that resolves on the first user interaction |
|
|
213
|
+
| `media(query)` | A Cue that resolves when the media query matches |
|
|
187
214
|
|
|
188
215
|
---
|
|
189
216
|
|
|
190
217
|
## Comparison
|
|
191
218
|
|
|
192
|
-
|
|
|
193
|
-
|
|
|
194
|
-
| Inline JS in HTML
|
|
195
|
-
| Composition-style setup
|
|
196
|
-
| BYO mounter
|
|
197
|
-
| Async mount cue
|
|
198
|
-
| Lifecycle cleanup
|
|
199
|
-
|
|
|
200
|
-
| Core gzip
|
|
219
|
+
| | **nagi** | Alpine.js | Stimulus | petite-vue |
|
|
220
|
+
| -------------------------- | -------- | --------- | -------- | ---------- |
|
|
221
|
+
| Inline JS in HTML | ✗ | ◯ | ✗ | ◯ |
|
|
222
|
+
| Composition-style setup | ◯ | △ | ✗ | ◯ |
|
|
223
|
+
| BYO mounter | ◯ | △ | △ | △ |
|
|
224
|
+
| Async mount cue | ◯ | ✗ | ✗ | ✗ |
|
|
225
|
+
| Lifecycle cleanup | ◯ | △ | ◯ | △ |
|
|
226
|
+
| computed (derived signals) | ◯ | ◯ | ✗ | ◯ |
|
|
227
|
+
| Core gzip | ~2.5 kB | ~16 kB | ~8 kB | ~6 kB |
|
|
201
228
|
|
|
202
|
-
◯ = built-in
|
|
229
|
+
(◯ = built-in, △ = handled via userland/convention, ✗ = not a primary feature)
|
|
203
230
|
|
|
204
|
-
- **vs Alpine / petite-vue**: HTML
|
|
205
|
-
- **vs Stimulus**:
|
|
206
|
-
- **vs React / Vue**:
|
|
231
|
+
- **vs Alpine / petite-vue**: Instead of writing logic expressions directly in HTML, you centralize your logic in `.ts` files.
|
|
232
|
+
- **vs Stimulus**: No controller conventions; you are free to implement your own mounting strategy.
|
|
233
|
+
- **vs React / Vue**: It is not a declarative UI framework, but rather a thin layer that adds lifecycle hooks to existing DOM.
|
|
207
234
|
|
|
208
235
|
---
|
|
209
236
|
|
|
210
237
|
## When to use / When not to
|
|
211
238
|
|
|
212
|
-
|
|
239
|
+
**Recommended Use Cases:**
|
|
213
240
|
|
|
214
|
-
- React
|
|
215
|
-
- GSAP
|
|
216
|
-
-
|
|
217
|
-
-
|
|
241
|
+
- Projects where you cannot justify the runtime overhead of React or Vue (e.g., CMS, Webflow, WordPress).
|
|
242
|
+
- Animation-heavy sites that rely heavily on libraries like GSAP or Lenis.
|
|
243
|
+
- Scenarios where you only need to add interactive UI to specific parts of a page.
|
|
244
|
+
- When you want to use a composition-style approach with `setup()`, lifecycle hooks, and reactivity, but do not require a virtual DOM.
|
|
218
245
|
|
|
219
|
-
|
|
246
|
+
**Not Recommended For:**
|
|
220
247
|
|
|
221
|
-
-
|
|
222
|
-
-
|
|
223
|
-
- SSR/hydration
|
|
224
|
-
-
|
|
248
|
+
- When you want to handle list rendering or conditional logic via HTML templates (it does not support equivalents to `v-for` or `v-if`).
|
|
249
|
+
- When you need deep reactivity for complex objects (it does not provide `reactive({})`).
|
|
250
|
+
- When SSR/hydration is required.
|
|
251
|
+
- When you want a full-featured framework to handle global state management, routing, and declarative view rendering.
|
|
225
252
|
|
|
226
253
|
---
|
|
227
254
|
|
|
228
255
|
## Examples
|
|
229
256
|
|
|
230
|
-
| Example |
|
|
231
|
-
| ----------------------------------------------------- |
|
|
232
|
-
| [basic-counter](./examples/basic-counter/) | `
|
|
233
|
-
| [computed](./examples/computed/) | `
|
|
234
|
-
| [parent-child](./examples/parent-child/) | `createContext` + `withContext` + `useSlot`
|
|
235
|
-
| [lenis-scroll-scene](./examples/lenis-scroll-scene/) | Lenis + `
|
|
236
|
-
| [byo-mounter recipe](./examples/recipes/byo-mounter/) | `[data-component]`
|
|
257
|
+
| Example | Description |
|
|
258
|
+
| ----------------------------------------------------- | -------------------------------------------------------- |
|
|
259
|
+
| [basic-counter](./examples/basic-counter/) | Minimal `signal` + `useWatch` example |
|
|
260
|
+
| [computed](./examples/computed/) | Derived value with `useComputed` (width × height = area) |
|
|
261
|
+
| [parent-child](./examples/parent-child/) | `createContext` + `withContext` + `useSlot` |
|
|
262
|
+
| [lenis-scroll-scene](./examples/lenis-scroll-scene/) | Scroll-progress animation with Lenis + `useComputed` |
|
|
263
|
+
| [byo-mounter recipe](./examples/recipes/byo-mounter/) | `[data-component]` scanning + manifest + cue |
|
|
237
264
|
|
|
238
265
|
---
|
|
239
266
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});function e(e){return(e instanceof DOMException||e instanceof Error)&&e.name===`AbortError`}function t(t,r,
|
|
1
|
+
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});function e(e){return e}function t(e){return(e instanceof DOMException||e instanceof Error)&&e.name===`AbortError`}function n(){let e=new Map;return{add(t){let n=e.get(t);n&&n.abort();let r=new AbortController;return e.set(t,r),{signal:r.signal,complete(){return e.get(t)!==r||r.signal.aborted?!1:(e.delete(t),!0)},abort(){e.get(t)===r&&(r.abort(),e.delete(t))}}},abort(t){let n=e.get(t);n&&(n.abort(),e.delete(t))}}}function r(e,n,r){if(r?.aborted)return;let{scheduler:a}=globalThis;if(typeof a?.postTask==`function`){a.postTask(e,{priority:n,signal:r}).catch(e=>{t(e)||queueMicrotask(()=>{throw e})});return}i(e,n,r)}function i(e,t,n){function r(){n?.aborted||e()}function i(e,t){let r=e();n?.addEventListener(`abort`,()=>t(r),{once:!0})}switch(t){case`user-blocking`:queueMicrotask(r);break;case`user-visible`:i(()=>requestAnimationFrame(r),cancelAnimationFrame);break;case`background`:typeof requestIdleCallback==`function`?i(()=>requestIdleCallback(r),cancelIdleCallback):i(()=>setTimeout(r,0),clearTimeout);break}}function a(e={}){let t=e.priority??`user-visible`;return{schedule(e,n={}){r(e,n.priority??t,n.signal)}}}function o(r){return e({name:`@usenagi/scheduler`,install(e){let i=a(r),o=n();e.addMountMiddleware((e,n,r)=>(n,a)=>{let s=o.add(n),c=()=>{i.schedule(()=>{s.complete()&&e(n,a)},{priority:r.priority,signal:s.signal})},{when:l}=r;l?l(n,s.signal).then(()=>{s.signal.aborted||c()},e=>{t(e)||(s.abort(),queueMicrotask(()=>{throw e}))}):c()}),e.addUnmountMiddleware(e=>t=>{t.forEach(o.abort),e(t)})}})}exports.createScheduler=a,exports.schedulerAddon=o;
|
|
@@ -1,26 +1,56 @@
|
|
|
1
|
-
//#region lib/
|
|
1
|
+
//#region lib/core/addon.ts
|
|
2
2
|
function e(e) {
|
|
3
|
+
return e;
|
|
4
|
+
}
|
|
5
|
+
//#endregion
|
|
6
|
+
//#region lib/utils/isAbortError.ts
|
|
7
|
+
function t(e) {
|
|
3
8
|
return (e instanceof DOMException || e instanceof Error) && e.name === "AbortError";
|
|
4
9
|
}
|
|
5
10
|
//#endregion
|
|
11
|
+
//#region lib/addons/scheduler/pending.ts
|
|
12
|
+
function n() {
|
|
13
|
+
let e = /* @__PURE__ */ new Map();
|
|
14
|
+
return {
|
|
15
|
+
add(t) {
|
|
16
|
+
let n = e.get(t);
|
|
17
|
+
n && n.abort();
|
|
18
|
+
let r = new AbortController();
|
|
19
|
+
return e.set(t, r), {
|
|
20
|
+
signal: r.signal,
|
|
21
|
+
complete() {
|
|
22
|
+
return e.get(t) !== r || r.signal.aborted ? !1 : (e.delete(t), !0);
|
|
23
|
+
},
|
|
24
|
+
abort() {
|
|
25
|
+
e.get(t) === r && (r.abort(), e.delete(t));
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
},
|
|
29
|
+
abort(t) {
|
|
30
|
+
let n = e.get(t);
|
|
31
|
+
n && (n.abort(), e.delete(t));
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
6
36
|
//#region lib/addons/scheduler/task.ts
|
|
7
|
-
function
|
|
8
|
-
if (
|
|
37
|
+
function r(e, n, r) {
|
|
38
|
+
if (r?.aborted) return;
|
|
9
39
|
let { scheduler: a } = globalThis;
|
|
10
40
|
if (typeof a?.postTask == "function") {
|
|
11
|
-
a.postTask(
|
|
12
|
-
priority:
|
|
13
|
-
signal:
|
|
14
|
-
}).catch((
|
|
15
|
-
e
|
|
16
|
-
throw
|
|
41
|
+
a.postTask(e, {
|
|
42
|
+
priority: n,
|
|
43
|
+
signal: r
|
|
44
|
+
}).catch((e) => {
|
|
45
|
+
t(e) || queueMicrotask(() => {
|
|
46
|
+
throw e;
|
|
17
47
|
});
|
|
18
48
|
});
|
|
19
49
|
return;
|
|
20
50
|
}
|
|
21
|
-
|
|
51
|
+
i(e, n, r);
|
|
22
52
|
}
|
|
23
|
-
function
|
|
53
|
+
function i(e, t, n) {
|
|
24
54
|
function r() {
|
|
25
55
|
n?.aborted || e();
|
|
26
56
|
}
|
|
@@ -41,12 +71,41 @@ function n(e, t, n) {
|
|
|
41
71
|
}
|
|
42
72
|
}
|
|
43
73
|
//#endregion
|
|
44
|
-
//#region lib/addons/scheduler/
|
|
45
|
-
function
|
|
46
|
-
let
|
|
47
|
-
return { schedule(e,
|
|
48
|
-
|
|
74
|
+
//#region lib/addons/scheduler/scheduler.ts
|
|
75
|
+
function a(e = {}) {
|
|
76
|
+
let t = e.priority ?? "user-visible";
|
|
77
|
+
return { schedule(e, n = {}) {
|
|
78
|
+
r(e, n.priority ?? t, n.signal);
|
|
49
79
|
} };
|
|
50
80
|
}
|
|
51
81
|
//#endregion
|
|
52
|
-
|
|
82
|
+
//#region lib/addons/scheduler/addon.ts
|
|
83
|
+
function o(r) {
|
|
84
|
+
return e({
|
|
85
|
+
name: "@usenagi/scheduler",
|
|
86
|
+
install(e) {
|
|
87
|
+
let i = a(r), o = n();
|
|
88
|
+
e.addMountMiddleware((e, n, r) => (n, a) => {
|
|
89
|
+
let s = o.add(n), c = () => {
|
|
90
|
+
i.schedule(() => {
|
|
91
|
+
s.complete() && e(n, a);
|
|
92
|
+
}, {
|
|
93
|
+
priority: r.priority,
|
|
94
|
+
signal: s.signal
|
|
95
|
+
});
|
|
96
|
+
}, { when: l } = r;
|
|
97
|
+
l ? l(n, s.signal).then(() => {
|
|
98
|
+
s.signal.aborted || c();
|
|
99
|
+
}, (e) => {
|
|
100
|
+
t(e) || (s.abort(), queueMicrotask(() => {
|
|
101
|
+
throw e;
|
|
102
|
+
}));
|
|
103
|
+
}) : c();
|
|
104
|
+
}), e.addUnmountMiddleware((e) => (t) => {
|
|
105
|
+
t.forEach(o.abort), e(t);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
export { a as createScheduler, o as schedulerAddon };
|
package/dist/main.es.js
CHANGED
|
@@ -1,31 +1,36 @@
|
|
|
1
|
-
//#region lib/
|
|
2
|
-
function e(
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return {
|
|
10
|
-
add(t) {
|
|
11
|
-
let n = e.get(t);
|
|
12
|
-
n && n.abort();
|
|
13
|
-
let r = new AbortController();
|
|
14
|
-
return e.set(t, r), {
|
|
15
|
-
signal: r.signal,
|
|
16
|
-
complete() {
|
|
17
|
-
return e.get(t) !== r || r.signal.aborted ? !1 : (e.delete(t), !0);
|
|
18
|
-
},
|
|
19
|
-
abort() {
|
|
20
|
-
e.get(t) === r && (r.abort(), e.delete(t));
|
|
21
|
-
}
|
|
22
|
-
};
|
|
1
|
+
//#region lib/core/addon.ts
|
|
2
|
+
function e() {
|
|
3
|
+
let e = /* @__PURE__ */ new Set(), t = [], n = [], r = [], i = {
|
|
4
|
+
get installedAddons() {
|
|
5
|
+
return e;
|
|
6
|
+
},
|
|
7
|
+
addComponentMiddleware(e) {
|
|
8
|
+
t.push(e);
|
|
23
9
|
},
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
10
|
+
addMountMiddleware(e) {
|
|
11
|
+
n.push(e);
|
|
12
|
+
},
|
|
13
|
+
addUnmountMiddleware(e) {
|
|
14
|
+
r.push(e);
|
|
15
|
+
},
|
|
16
|
+
composeComponent(e) {
|
|
17
|
+
return t.reduce((e, t) => t(e), e);
|
|
18
|
+
},
|
|
19
|
+
composeMount(e, t, r) {
|
|
20
|
+
return n.reduce((e, n) => n(e, t, r), e);
|
|
21
|
+
},
|
|
22
|
+
composeUnmount(e) {
|
|
23
|
+
return r.reduce((e, t) => t(e), e);
|
|
24
|
+
},
|
|
25
|
+
install(t) {
|
|
26
|
+
if (e.has(t.name)) throw Error(`[nagi] addon "${t.name}" is already installed`);
|
|
27
|
+
t.install(i), e.add(t.name);
|
|
27
28
|
}
|
|
28
29
|
};
|
|
30
|
+
return i;
|
|
31
|
+
}
|
|
32
|
+
function t(e) {
|
|
33
|
+
return e;
|
|
29
34
|
}
|
|
30
35
|
//#endregion
|
|
31
36
|
//#region lib/core/error.ts
|
|
@@ -118,12 +123,7 @@ var s = /* @__PURE__ */ function(e) {
|
|
|
118
123
|
}
|
|
119
124
|
};
|
|
120
125
|
function u(e) {
|
|
121
|
-
return e
|
|
122
|
-
name: e.name,
|
|
123
|
-
setup(n) {
|
|
124
|
-
return e.setup(n, t);
|
|
125
|
-
}
|
|
126
|
-
}) : e;
|
|
126
|
+
return e;
|
|
127
127
|
}
|
|
128
128
|
//#endregion
|
|
129
129
|
//#region lib/core/runtime.ts
|
|
@@ -132,7 +132,7 @@ function f(e) {
|
|
|
132
132
|
if (!d) throw Error(`"${e}" called outside setup() will never be run.`);
|
|
133
133
|
return d;
|
|
134
134
|
}
|
|
135
|
-
function p(e, t, n) {
|
|
135
|
+
function p(e, t, n = {}) {
|
|
136
136
|
let a = new l(t, e.name), o = d;
|
|
137
137
|
d = a;
|
|
138
138
|
try {
|
|
@@ -144,41 +144,28 @@ function p(e, t, n) {
|
|
|
144
144
|
}
|
|
145
145
|
//#endregion
|
|
146
146
|
//#region lib/core/app.ts
|
|
147
|
-
function m(
|
|
148
|
-
let
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
});
|
|
164
|
-
};
|
|
165
|
-
a ? a(s, u.signal).then(() => {
|
|
166
|
-
u.signal.aborted || d();
|
|
167
|
-
}, (t) => {
|
|
168
|
-
e(t) || (u.abort(), queueMicrotask(() => {
|
|
169
|
-
throw t;
|
|
170
|
-
}));
|
|
171
|
-
}) : d();
|
|
172
|
-
};
|
|
147
|
+
function m() {
|
|
148
|
+
let t = e(), n = (e) => {
|
|
149
|
+
for (let t of e) {
|
|
150
|
+
let e = a.get(t);
|
|
151
|
+
e && (e.onUnmount(), a.delete(t));
|
|
152
|
+
}
|
|
153
|
+
}, r = {
|
|
154
|
+
install(...e) {
|
|
155
|
+
return e.forEach(t.install), r;
|
|
156
|
+
},
|
|
157
|
+
component(e, n = {}) {
|
|
158
|
+
let r = t.composeComponent(e), i = t.composeMount((e, t) => {
|
|
159
|
+
let n = p(r, e, t);
|
|
160
|
+
return o(e, n), n.onMount(), n;
|
|
161
|
+
}, r, n);
|
|
162
|
+
return (e, t = {}) => i(e, t);
|
|
173
163
|
},
|
|
174
164
|
unmount(e) {
|
|
175
|
-
|
|
176
|
-
i.abort(t);
|
|
177
|
-
let e = a.get(t);
|
|
178
|
-
e && (e.onUnmount(), a.delete(t));
|
|
179
|
-
}
|
|
165
|
+
t.composeUnmount(n)(e);
|
|
180
166
|
}
|
|
181
167
|
};
|
|
168
|
+
return r;
|
|
182
169
|
}
|
|
183
170
|
//#endregion
|
|
184
171
|
//#region lib/core/lifecycle.ts
|
|
@@ -346,16 +333,11 @@ function F(e, t) {
|
|
|
346
333
|
})), { matchesQuery: C(r) };
|
|
347
334
|
}
|
|
348
335
|
//#endregion
|
|
349
|
-
//#region lib/hooks/useRootRef.ts
|
|
350
|
-
function I() {
|
|
351
|
-
return f("useRootRef").element;
|
|
352
|
-
}
|
|
353
|
-
//#endregion
|
|
354
336
|
//#region lib/hooks/useSlot.ts
|
|
355
|
-
function
|
|
337
|
+
function I() {
|
|
356
338
|
let e = f("useSlot");
|
|
357
339
|
return {
|
|
358
|
-
addChild(t, n, r
|
|
340
|
+
addChild(t, n, r) {
|
|
359
341
|
let i = (t) => {
|
|
360
342
|
let i = p(n, t, r);
|
|
361
343
|
return e.addChild(i), i;
|
|
@@ -374,4 +356,7 @@ function L() {
|
|
|
374
356
|
};
|
|
375
357
|
}
|
|
376
358
|
//#endregion
|
|
377
|
-
|
|
359
|
+
//#region lib/props.ts
|
|
360
|
+
function L() {}
|
|
361
|
+
//#endregion
|
|
362
|
+
export { r as LifecycleError, m as create, D as createContext, t as defineAddon, u as defineComponent, i as isLifecycleError, L as propTypes, C as readonly, x as signal, E as useComputed, M as useDomRef, N as useEvent, P as useIntersectionWatch, F as useMediaQuery, g as useMount, I as useSlot, _ as useUnmount, T as useWatch, O as withContext };
|
package/dist/main.umd.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
(function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.Lake={}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});function t(
|
|
1
|
+
(function(e,t){typeof exports==`object`&&typeof module<`u`?t(exports):typeof define==`function`&&define.amd?define([`exports`],t):(e=typeof globalThis<`u`?globalThis:e||self,t(e.Lake={}))})(this,function(e){Object.defineProperty(e,Symbol.toStringTag,{value:`Module`});function t(){let e=new Set,t=[],n=[],r=[],i={get installedAddons(){return e},addComponentMiddleware(e){t.push(e)},addMountMiddleware(e){n.push(e)},addUnmountMiddleware(e){r.push(e)},composeComponent(e){return t.reduce((e,t)=>t(e),e)},composeMount(e,t,r){return n.reduce((e,n)=>n(e,t,r),e)},composeUnmount(e){return r.reduce((e,t)=>t(e),e)},install(t){if(e.has(t.name))throw Error(`[nagi] addon "${t.name}" is already installed`);t.install(i),e.add(t.name)}};return i}function n(e){return e}function r(e){let t=[],n=e;for(;n;)t.unshift(n.name),n=n.parent;return t.join(` > `)}var i=class e extends Error{details;constructor(e){super(`[nagi] Component error in phase "${e.phase}" for "${e.name}"${e.path?` (${e.path})`:``}`,{cause:e.cause}),this.name=`LifecycleError`,this.details=e}static create(t,n,i,a=n.parent,o){return new e({phase:t,name:n.name,uid:n.uid,path:r(n),parentName:a?.name,parentUid:a?.uid,element:n.element,cause:i,...o})}};function a(e){return e instanceof i}var o=new WeakMap;function s(e,t){let n=o.get(e);if(n)throw i.create(`mount`,t,Error(`Component "${n.name}" (${n.uid}) is already mounted on this element`),n);o.set(e,t)}var c=function(e){return e.MOUNTED=`Mounted`,e.UNMOUNTED=`Unmounted`,e}({}),l=0,u=class{Mounted=[];Unmounted=[];parent=null;#e=[];uid;name;current={};props={};element;provides=new Map;constructor(e,t){this.uid=`${t}.${l++}`,this.name=t,this.element=e}onMount=()=>{let e=[];for(let t of this.Mounted)try{let n=t();typeof n==`function`&&e.push(n)}catch(e){console.error(`[nagi] onMount hook failed`,i.create(`mount`,this,e))}this.Unmounted.push(...e)};onUnmount=()=>{for(let e of this.Unmounted)try{e()}catch(e){console.error(`[nagi] onUnmount cleanup failed`,i.create(`unmount`,this,e))}for(let e of this.#e)e.onUnmount()};addChild=e=>{this.#e.push(e),e.parent=this;try{e.onMount()}catch(t){let n=this.#e.indexOf(e);throw n!==-1&&this.#e.splice(n,1),e.parent=null,t}};removeChild=e=>{let t=this.#e.indexOf(e);t!==-1&&(this.#e.splice(t,1),e.parent=null,e.onUnmount())};get childElements(){return this.#e.map(e=>e.element)}};function d(e){return e}var f;function p(e){if(!f)throw Error(`"${e}" called outside setup() will never be run.`);return f}function m(e,t,n={}){let r=new u(t,e.name),o=f;f=r;try{o&&(r.parent=o),r.props=n,r.current=e.setup(t,n)||{}}catch(e){throw f=o,a(e)?e:i.create(`setup`,r,e,o,{props:r.props})}return f=o,r}function h(){let e=t(),n=e=>{for(let t of e){let e=o.get(t);e&&(e.onUnmount(),o.delete(t))}},r={install(...t){return t.forEach(e.install),r},component(t,n={}){let r=e.composeComponent(t),i=e.composeMount((e,t)=>{let n=m(r,e,t);return s(e,n),n.onMount(),n},r,n);return(e,t={})=>i(e,t)},unmount(t){e.composeUnmount(n)(t)}};return r}function g(e){return t=>{p(e)[e].push(t)}}var _=g(c.MOUNTED),v=g(c.UNMOUNTED),y=Symbol(`watch`),b=null,x=class{#e;#t=new Set;constructor(e){this.#e=e}get value(){return b!==null&&b.add(this),this.#e}set value(e){if(Object.is(e,this.#e))return;let t=this.#e;this.#e=e;for(let n of Array.from(this.#t))n(e,t)}[y](e){return this.#t.add(e),()=>{this.#t.delete(e)}}},S=e=>new x(e),C=class{#e;constructor(e){this.#e=e}get value(){return this.#e.value}[y](e){return this.#e[y](e)}},w=e=>new C(e);function T(e,t){return e[y](t)}function E(e,t){v(T(e,t))}function D(e){let t=S(void 0),n=[],r=()=>{n.forEach(e=>{e()}),n=[]},i=()=>{r();let a=b,o=new Set;b=o;let s;try{s=e()}finally{b=a}t.value=s;for(let e of o)n.push(e[y](()=>{i()}))};return i(),v(r),w(t)}function O(){let e=Symbol();return[{_id:e},()=>{let t=p(`createContext.use`);for(;t!==null;){if(t.provides.has(e))return t.provides.get(e);t=t.parent}throw Error(`createContext.use: no provider found`)}]}function k(e,t){return n=>({name:n.name,setup(r,i){return p(`withContext.${n.name}`).provides.set(e._id,t),n.setup(r,i)}})}function A(e,t){return t.some(t=>t!==e&&t.contains(e))}function j(e,t,n){let r=`[data-ref="${CSS.escape(e)}"]`,i=Array.from(t.querySelectorAll(r)).filter(e=>!A(e,n));return i.length===0?null:i.length===1?i[0]:i}function M(e,t){let n=new Map;return new Proxy({},{get(r,i){if(typeof i==`symbol`||i===`then`)return;if(n.has(i))return n.get(i);let a=j(i,e,t());return n.set(i,a),a},has(e,t){return typeof t==`string`},ownKeys(){return[]},getOwnPropertyDescriptor(){},set(){return!1},deleteProperty(){return!1}})}function N(){let e=p(`useDomRef`);return{refs:M(e.element,()=>e.childElements)}}function P(e,t,n,r){_(()=>(e.addEventListener(t,n,r),()=>{e.removeEventListener(t,n,r)}))}function F(e,t,n={rootMargin:`0px`,threshold:.1}){let r=new IntersectionObserver(t,n);function i(e){Array.isArray(e)?e.forEach(e=>{r.observe(e)}):r.observe(e)}i(e),v(()=>{r.disconnect()});function a(e){r.unobserve(e)}return{unwatch:a}}function I(e,t){let n=window.matchMedia(e),r=S(n.matches),i=null;function a(e){r.value=e.matches,e.matches?i=t():(i?.(),i=null)}return _(()=>(n.addEventListener(`change`,a),n.matches&&(i=t()),()=>{i?.(),n.removeEventListener(`change`,a)})),{matchesQuery:w(r)}}function L(){let e=p(`useSlot`);return{addChild(t,n,r){let i=t=>{let i=m(n,t,r);return e.addChild(i),i};return Array.isArray(t)?t.map(e=>i(e)):[i(t)]},removeChild(t){t.forEach(t=>{try{e.removeChild(t)}catch(n){console.error(`[nagi] removeChild failed`,i.create(`removeChild`,t,n,e))}})}}}function R(){}e.LifecycleError=i,e.create=h,e.createContext=O,e.defineAddon=n,e.defineComponent=d,e.isLifecycleError=a,e.propTypes=R,e.readonly=w,e.signal=S,e.useComputed=D,e.useDomRef=N,e.useEvent=P,e.useIntersectionWatch=F,e.useMediaQuery=I,e.useMount=_,e.useSlot=L,e.useUnmount=v,e.useWatch=E,e.withContext=k});
|
package/package.json
CHANGED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Cue, SchedulePriority } from "../../types";
|
|
2
|
+
declare module "../../core/addon" {
|
|
3
|
+
interface MountOptions {
|
|
4
|
+
priority?: SchedulePriority;
|
|
5
|
+
when?: Cue;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export declare function schedulerAddon(opts?: {
|
|
9
|
+
priority?: SchedulePriority;
|
|
10
|
+
}): import("../../main").Addon;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { RefElement } from "../../types";
|
|
2
|
+
export type PendingMount = {
|
|
3
|
+
readonly signal: AbortSignal;
|
|
4
|
+
complete(): boolean;
|
|
5
|
+
abort(): void;
|
|
6
|
+
};
|
|
7
|
+
export type PendingMounts = {
|
|
8
|
+
add(el: RefElement): PendingMount;
|
|
9
|
+
abort(el: RefElement): void;
|
|
10
|
+
};
|
|
11
|
+
export declare function createPendingMounts(): PendingMounts;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { ComponentSetup, RefElement } from "../types";
|
|
2
|
+
declare const mountOptionsBrand: unique symbol;
|
|
3
|
+
/** Options for `app.component(setup, opts)` — extended by mount addons. */
|
|
4
|
+
export interface MountOptions {
|
|
5
|
+
readonly [mountOptionsBrand]?: never;
|
|
6
|
+
}
|
|
7
|
+
/** Mount after addon middleware runs — may return void when mount is deferred (e.g. scheduler). */
|
|
8
|
+
export type MountFn = (el: RefElement, props: Record<string, any>) => any;
|
|
9
|
+
export type UnmountFn = (targets: RefElement[]) => void;
|
|
10
|
+
export type ComponentMiddleware = <S extends ComponentSetup>(comp: S) => S;
|
|
11
|
+
export type MountMiddleware = (next: MountFn, setup: ComponentSetup, opts: MountOptions) => MountFn;
|
|
12
|
+
export type UnmountMiddleware = (next: UnmountFn) => UnmountFn;
|
|
13
|
+
export type Addon = {
|
|
14
|
+
readonly name: string;
|
|
15
|
+
install(ctx: AddonContext): void;
|
|
16
|
+
};
|
|
17
|
+
export type AddonContext = {
|
|
18
|
+
readonly installedAddons: ReadonlySet<string>;
|
|
19
|
+
addComponentMiddleware(middleware: ComponentMiddleware): void;
|
|
20
|
+
addMountMiddleware(middleware: MountMiddleware): void;
|
|
21
|
+
addUnmountMiddleware(middleware: UnmountMiddleware): void;
|
|
22
|
+
};
|
|
23
|
+
type AddonRegistry = AddonContext & {
|
|
24
|
+
composeComponent<S extends ComponentSetup>(setup: S): S;
|
|
25
|
+
composeMount(mountFn: MountFn, setup: ComponentSetup, opts: MountOptions): MountFn;
|
|
26
|
+
composeUnmount(unmountFn: UnmountFn): UnmountFn;
|
|
27
|
+
install(addon: Addon): void;
|
|
28
|
+
};
|
|
29
|
+
export declare function createAddonRegistry(): AddonRegistry;
|
|
30
|
+
/**
|
|
31
|
+
* Identity helper for type inference only — no runtime effect.
|
|
32
|
+
*/
|
|
33
|
+
export declare function defineAddon(addon: Addon): Addon;
|
|
34
|
+
export declare function defineAddon<TOptions>(factory: (options?: TOptions) => Addon): (options?: TOptions) => Addon;
|
|
35
|
+
export {};
|
package/types/core/app.d.ts
CHANGED
|
@@ -1,27 +1,10 @@
|
|
|
1
|
-
import type { ComponentSetup,
|
|
1
|
+
import type { ComponentSetup, RefElement } from "../types";
|
|
2
|
+
import type { Addon, MountOptions } from "./addon";
|
|
2
3
|
import type { ComponentContext } from "./component";
|
|
3
|
-
type
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
type AsyncAppOptions = AppOptions & {
|
|
7
|
-
when?: Cue;
|
|
8
|
-
};
|
|
9
|
-
type SyncApp = {
|
|
10
|
-
component<S extends ComponentSetup>(wrap: S, opts?: AppOptions): (el: RefElement, props?: Record<string, any>) => ComponentContext<ReturnType<S["setup"]>>;
|
|
11
|
-
unmount(targets: RefElement[]): void;
|
|
12
|
-
};
|
|
13
|
-
type AsyncApp = {
|
|
14
|
-
component<S extends ComponentSetup>(wrap: S, opts?: AsyncAppOptions): (el: RefElement, props?: Record<string, any>) => void;
|
|
4
|
+
type App = {
|
|
5
|
+
install(...addons: Addon[]): App;
|
|
6
|
+
component<S extends ComponentSetup>(component: S, opts?: MountOptions): (el: RefElement, props?: Record<string, any>) => ComponentContext<ReturnType<S["setup"]>> | void;
|
|
15
7
|
unmount(targets: RefElement[]): void;
|
|
16
8
|
};
|
|
17
|
-
export declare function create():
|
|
18
|
-
export declare function create(config: {
|
|
19
|
-
scheduler?: undefined;
|
|
20
|
-
}): SyncApp;
|
|
21
|
-
export declare function create(config: {
|
|
22
|
-
scheduler: Scheduler;
|
|
23
|
-
}): AsyncApp;
|
|
24
|
-
export declare function create(config: {
|
|
25
|
-
scheduler?: Scheduler | undefined;
|
|
26
|
-
}): SyncApp | AsyncApp;
|
|
9
|
+
export declare function create(): App;
|
|
27
10
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ComponentSetup, RefElement } from "../types";
|
|
1
|
+
import type { ComponentProps, ComponentSetup, RefElement } from "../types";
|
|
2
2
|
export declare enum LifecycleHooks {
|
|
3
3
|
MOUNTED = "Mounted",
|
|
4
4
|
UNMOUNTED = "Unmounted"
|
|
@@ -21,8 +21,17 @@ export declare class ComponentContext<T = any> {
|
|
|
21
21
|
removeChild: (child: ComponentContext) => void;
|
|
22
22
|
get childElements(): RefElement[];
|
|
23
23
|
}
|
|
24
|
-
export declare function defineComponent<
|
|
24
|
+
export declare function defineComponent<SetupResult extends Record<string, unknown> | void, Props extends Record<string, unknown>>(opts: {
|
|
25
25
|
name: string;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
props: Props;
|
|
27
|
+
setup(el: RefElement, props: ComponentProps<Props>): SetupResult;
|
|
28
|
+
}): ComponentSetup<SetupResult, Props>;
|
|
29
|
+
export declare function defineComponent<SetupResult extends Record<string, unknown> | void, Props extends Record<string, unknown>>(opts: {
|
|
30
|
+
name: string;
|
|
31
|
+
setup(el: RefElement, props: ComponentProps<Props>): SetupResult;
|
|
32
|
+
}): ComponentSetup<SetupResult, Props>;
|
|
33
|
+
export declare function defineComponent<SetupResult extends Record<string, unknown> | void>(opts: {
|
|
34
|
+
name: string;
|
|
35
|
+
setup(el: RefElement): SetupResult;
|
|
36
|
+
}): ComponentSetup<SetupResult, Record<string, never>>;
|
|
37
|
+
export declare function defineComponent(opts: ComponentSetup): ComponentSetup;
|
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
type WatchCallback<T> = (newVal: T, oldVal: T) => void;
|
|
2
2
|
type Unwatch = () => void;
|
|
3
3
|
declare const WATCH: unique symbol;
|
|
4
|
-
declare class
|
|
4
|
+
declare class Signal<T> {
|
|
5
5
|
#private;
|
|
6
6
|
constructor(value: T);
|
|
7
7
|
get value(): T;
|
|
8
8
|
set value(newVal: T);
|
|
9
9
|
[WATCH](callback: WatchCallback<T>): Unwatch;
|
|
10
10
|
}
|
|
11
|
-
declare const
|
|
12
|
-
declare class
|
|
11
|
+
declare const signal: <T = any>(val: T) => Signal<T>;
|
|
12
|
+
declare class ReadonlySignal<T> {
|
|
13
13
|
#private;
|
|
14
|
-
constructor(value:
|
|
14
|
+
constructor(value: Signal<T>);
|
|
15
15
|
get value(): T;
|
|
16
16
|
[WATCH](callback: WatchCallback<T>): Unwatch;
|
|
17
17
|
}
|
|
18
|
-
declare const readonly: <T = any>(
|
|
19
|
-
declare function useWatch<T>(
|
|
20
|
-
declare function
|
|
21
|
-
export {
|
|
22
|
-
export type {
|
|
18
|
+
declare const readonly: <T = any>(s: Signal<T>) => ReadonlySignal<T>;
|
|
19
|
+
declare function useWatch<T>(target: Signal<T> | ReadonlySignal<T>, callback: WatchCallback<T>): void;
|
|
20
|
+
declare function useComputed<T>(getter: () => T): ReadonlySignal<T>;
|
|
21
|
+
export { readonly, signal, useComputed, useWatch };
|
|
22
|
+
export type { ReadonlySignal, Signal };
|
package/types/core/runtime.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { ComponentContext } from "./component";
|
|
2
2
|
import type { ComponentSetup, RefElement } from "../types";
|
|
3
3
|
export declare function getCurrentComponent(hookName: string): ComponentContext;
|
|
4
|
-
export declare function createComponent(wrap: ComponentSetup, root: RefElement, props
|
|
4
|
+
export declare function createComponent(wrap: ComponentSetup, root: RefElement, props?: Record<string, any>): ComponentContext<any>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { Cleanup } from "../types";
|
|
2
2
|
export declare function useMediaQuery(query: string, callbackWhenMatches: () => Cleanup): {
|
|
3
|
-
readonly matchesQuery: import("../main").
|
|
3
|
+
readonly matchesQuery: import("../main").ReadonlySignal<boolean>;
|
|
4
4
|
};
|
package/types/hooks/useSlot.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ComponentContext } from "../core/component";
|
|
2
2
|
import type { ComponentSetup, RefElement } from "../types";
|
|
3
3
|
export declare function useSlot(): {
|
|
4
|
-
addChild<Child extends ComponentSetup>(targetOrTargets: RefElement | RefElement[], child: Child, props?: Parameters<Child["setup"]>[1]): ComponentContext<ReturnType<Child["setup"]>>[];
|
|
4
|
+
addChild<Child extends ComponentSetup>(targetOrTargets: RefElement | RefElement[], child: Child, props?: Partial<Parameters<Child["setup"]>[1]>): ComponentContext<ReturnType<Child["setup"]>>[];
|
|
5
5
|
removeChild(children: ComponentContext[]): void;
|
|
6
6
|
};
|
package/types/main.d.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
|
+
export { defineAddon } from "./core/addon";
|
|
1
2
|
export { create } from "./core/app";
|
|
2
3
|
export { defineComponent } from "./core/component";
|
|
3
4
|
export { isLifecycleError, LifecycleError } from "./core/error";
|
|
4
5
|
export { useMount, useUnmount } from "./core/lifecycle";
|
|
5
|
-
export {
|
|
6
|
+
export { readonly, signal, useComputed, useWatch } from "./core/reactivity";
|
|
6
7
|
export { createContext, withContext } from "./hooks/createContext";
|
|
7
8
|
export { useDomRef } from "./hooks/useDomRef";
|
|
8
9
|
export { useEvent } from "./hooks/useEvent";
|
|
9
10
|
export { useIntersectionWatch } from "./hooks/useIntersectionWatch";
|
|
10
11
|
export { useMediaQuery } from "./hooks/useMediaQuery";
|
|
11
|
-
export { useRootRef } from "./hooks/useRootRef";
|
|
12
12
|
export { useSlot } from "./hooks/useSlot";
|
|
13
|
+
export { propTypes } from "./props";
|
|
14
|
+
export type { Addon, AddonContext, ComponentMiddleware, MountFn, MountMiddleware, MountOptions, UnmountFn, UnmountMiddleware, } from "./core/addon";
|
|
13
15
|
export type { ComponentContext } from "./core/component";
|
|
14
16
|
export type { LifecycleErrorDetails } from "./core/error";
|
|
15
|
-
export type {
|
|
17
|
+
export type { ReadonlySignal, Signal } from "./core/reactivity";
|
|
16
18
|
export type { Provider } from "./hooks/createContext";
|
|
17
19
|
export type { ComponentSetup, Cue, IComponent, RefElement, SchedulePriority, Scheduler, } from "./types";
|
package/types/props.d.ts
ADDED
package/types/types.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
export type RefElement = HTMLElement | SVGElement;
|
|
2
2
|
export type ComponentProps<Props> = Readonly<Props>;
|
|
3
|
-
export type ComponentSetup<SetupResult = void | Record<string, unknown>, Props = Record<string, unknown>> = {
|
|
3
|
+
export type ComponentSetup<SetupResult = void | Record<string, unknown>, Props extends Record<string, unknown> = Record<string, unknown>> = {
|
|
4
4
|
name: string;
|
|
5
5
|
setup(el: RefElement, props: ComponentProps<Props>): SetupResult;
|
|
6
6
|
};
|
|
7
7
|
/** @deprecated Use `ComponentSetup` instead. */
|
|
8
|
-
export type IComponent<SetupResult = void | Record<string, unknown>, Props = Record<string, unknown>> = ComponentSetup<SetupResult, Props>;
|
|
8
|
+
export type IComponent<SetupResult = void | Record<string, unknown>, Props extends Record<string, unknown> = Record<string, unknown>> = ComponentSetup<SetupResult, Props>;
|
|
9
9
|
export type Cleanup = () => void;
|
|
10
10
|
export type LifecycleHandler = () => void | Cleanup;
|
|
11
11
|
export type SchedulePriority = "user-blocking" | "user-visible" | "background";
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { RefElement } from "../../types";
|
|
2
|
-
export type PendingMountTask = {
|
|
3
|
-
readonly signal: AbortSignal;
|
|
4
|
-
complete(): boolean;
|
|
5
|
-
abort(): void;
|
|
6
|
-
};
|
|
7
|
-
export type PendingMountTasks = {
|
|
8
|
-
add(el: RefElement): PendingMountTask;
|
|
9
|
-
abort(el: RefElement): void;
|
|
10
|
-
};
|
|
11
|
-
export declare function createPendingMountTasks(): PendingMountTasks;
|