@usenagi/core 0.4.0 → 0.4.1
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/dist/addons/cue.es.js +1 -1
- package/dist/addons/scheduler.es.js +3 -3
- package/package.json +3 -17
- package/types/addons/cue/index.d.ts +1 -1
- package/types/addons/scheduler/_internal/pending.d.ts +1 -1
- package/types/addons/scheduler/_internal/schedule.d.ts +1 -1
- package/types/addons/scheduler/index.d.ts +3 -3
- package/LICENSE +0 -21
- package/README.ja.md +0 -276
- package/README.md +0 -276
package/dist/addons/cue.es.js
CHANGED
|
@@ -3,7 +3,7 @@ function e(e) {
|
|
|
3
3
|
return e;
|
|
4
4
|
}
|
|
5
5
|
//#endregion
|
|
6
|
-
//#region
|
|
6
|
+
//#region ../addons/scheduler/_internal/pending.ts
|
|
7
7
|
function t() {
|
|
8
8
|
let e = /* @__PURE__ */ new Map();
|
|
9
9
|
return {
|
|
@@ -28,7 +28,7 @@ function t() {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
//#endregion
|
|
31
|
-
//#region
|
|
31
|
+
//#region ../addons/scheduler/_internal/schedule.ts
|
|
32
32
|
function n(e) {
|
|
33
33
|
return (e instanceof DOMException || e instanceof Error) && e.name === "AbortError";
|
|
34
34
|
}
|
|
@@ -75,7 +75,7 @@ function a(e = {}) {
|
|
|
75
75
|
} };
|
|
76
76
|
}
|
|
77
77
|
//#endregion
|
|
78
|
-
//#region
|
|
78
|
+
//#region ../addons/scheduler/index.ts
|
|
79
79
|
function o(e) {
|
|
80
80
|
return (e instanceof DOMException || e instanceof Error) && e.name === "AbortError";
|
|
81
81
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@usenagi/core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Composition-API ergonomics for vanilla DOM. Bring your own mounter.",
|
|
5
5
|
"main": "./dist/main.umd.js",
|
|
6
6
|
"module": "./dist/main.es.js",
|
|
@@ -49,22 +49,8 @@
|
|
|
49
49
|
"access": "public"
|
|
50
50
|
},
|
|
51
51
|
"scripts": {
|
|
52
|
-
"build": "NODE_ENV=production tsc && vite build && vite build --config vite.addons.config.ts",
|
|
53
|
-
"watch": "vite build -w"
|
|
54
|
-
"test": "vitest run",
|
|
55
|
-
"test:watch": "vitest",
|
|
56
|
-
"lint": "oxlint ./lib",
|
|
57
|
-
"format": "biome format --write ./lib",
|
|
58
|
-
"fix:biome": "biome check --write ./lib",
|
|
59
|
-
"fix": "npm run fix:biome"
|
|
60
|
-
},
|
|
61
|
-
"devDependencies": {
|
|
62
|
-
"@biomejs/biome": "^2.4.13",
|
|
63
|
-
"happy-dom": "^20.9.0",
|
|
64
|
-
"oxlint": "^1.66.0",
|
|
65
|
-
"typescript": "^6.0.3",
|
|
66
|
-
"vite": "^8.0.14",
|
|
67
|
-
"vitest": "^4.1.7"
|
|
52
|
+
"build": "NODE_ENV=production tsc && tsc -p ../addons/tsconfig.json && vite build && vite build --config vite.addons.config.ts",
|
|
53
|
+
"watch": "vite build -w"
|
|
68
54
|
},
|
|
69
55
|
"type": "module",
|
|
70
56
|
"sideEffects": false
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Cue, SchedulePriority } from "
|
|
2
|
-
declare module "
|
|
1
|
+
import type { Cue, SchedulePriority } from "@usenagi/core";
|
|
2
|
+
declare module "@usenagi/core" {
|
|
3
3
|
interface MountOptions {
|
|
4
4
|
priority?: SchedulePriority;
|
|
5
5
|
when?: Cue;
|
|
@@ -7,4 +7,4 @@ declare module "../../core/addon" {
|
|
|
7
7
|
}
|
|
8
8
|
export declare function schedulerAddon(opts?: {
|
|
9
9
|
priority?: SchedulePriority;
|
|
10
|
-
}): import("
|
|
10
|
+
}): import("@usenagi/core").Addon;
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2022 hayakawasho
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
package/README.ja.md
DELETED
|
@@ -1,276 +0,0 @@
|
|
|
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 app = create();
|
|
36
|
-
|
|
37
|
-
app.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, propTypes, signal, useWatch, useDomRef } from "@usenagi/core";
|
|
75
|
-
|
|
76
|
-
const Greeting = defineComponent({
|
|
77
|
-
name: "greeting",
|
|
78
|
-
props: propTypes<{ name: string }>(),
|
|
79
|
-
setup(el, props) {
|
|
80
|
-
const { refs } = useDomRef<{ message: HTMLParagraphElement }>();
|
|
81
|
-
const text = signal(props.name ?? "world");
|
|
82
|
-
|
|
83
|
-
useWatch(text, (v) => {
|
|
84
|
-
refs.message.textContent = `Hello, ${v}!`;
|
|
85
|
-
});
|
|
86
|
-
refs.message.textContent = `Hello, ${text.value}!`;
|
|
87
|
-
},
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
create().component(Greeting)(document.querySelector("#app")!);
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
### Scheduler + deferred mount
|
|
94
|
-
|
|
95
|
-
遅延マウントが必要な場合は、scheduler / cue addons を追加する。
|
|
96
|
-
|
|
97
|
-
```ts
|
|
98
|
-
import { create } from "@usenagi/core";
|
|
99
|
-
import { schedulerAddon } from "@usenagi/core/addons/scheduler";
|
|
100
|
-
import { visible, idle } from "@usenagi/core/addons/cue";
|
|
101
|
-
|
|
102
|
-
const app = create().install(schedulerAddon());
|
|
103
|
-
|
|
104
|
-
// mount when the element enters the viewport
|
|
105
|
-
app.component(HeavyWidget, { when: visible() })(el);
|
|
106
|
-
|
|
107
|
-
// mount during browser idle time
|
|
108
|
-
app.component(Analytics, { when: idle() })(el);
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
`schedulerAddon()` を使うと、`when` は `setup()` の前に待機する条件、`priority` は `setup()` を含む mount task の実行タイミングを決める。
|
|
112
|
-
|
|
113
|
-
### BYO mounter recipe
|
|
114
|
-
|
|
115
|
-
`[data-component]` スキャン、manifest、cue を組み合わせた自動マウントの例。
|
|
116
|
-
→ [examples/recipes/byo-mounter](./examples/recipes/byo-mounter/main.ts)
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
|
120
|
-
## API
|
|
121
|
-
|
|
122
|
-
### Component Definition
|
|
123
|
-
|
|
124
|
-
| API | 説明 |
|
|
125
|
-
| ---------------------- | ---------------------------------------------------------------- |
|
|
126
|
-
| `defineComponent(opts)` | 型安全な `ComponentSetup` 定義ヘルパー |
|
|
127
|
-
| `propTypes<T>()` | コンポーネント props の型マーカー(ランタイムコストゼロ) |
|
|
128
|
-
|
|
129
|
-
### Reactivity
|
|
130
|
-
|
|
131
|
-
| API | 説明 |
|
|
132
|
-
| ---------------------- | ------------------------------------------------------ |
|
|
133
|
-
| `signal(value)` | `.value` を持つリアクティブな値コンテナを作成する |
|
|
134
|
-
| `readonly(signal)` | 書き込み可能な `signal` の読み取り専用ラッパー |
|
|
135
|
-
| `useComputed(fn)` | `signal` の依存を自動追跡する派生値 |
|
|
136
|
-
| `useWatch(target, cb)` | 値変更時に `cb` を呼ぶ。unmount 時に自動で購読解除する |
|
|
137
|
-
|
|
138
|
-
```ts
|
|
139
|
-
const width = signal(10);
|
|
140
|
-
const height = signal(5);
|
|
141
|
-
const area = useComputed(() => width.value * height.value); // auto-recomputed
|
|
142
|
-
|
|
143
|
-
useWatch(area, (v) => {
|
|
144
|
-
output.textContent = String(v);
|
|
145
|
-
});
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### Lifecycle
|
|
149
|
-
|
|
150
|
-
| API | 説明 |
|
|
151
|
-
| ---------------- | ------------------------------------------- |
|
|
152
|
-
| `useMount(fn)` | コンポーネントのマウント完了後に1回実行する |
|
|
153
|
-
| `useUnmount(fn)` | unmount 時に実行する。クリーンアップに使う |
|
|
154
|
-
|
|
155
|
-
```ts
|
|
156
|
-
import gsap from 'gsap';
|
|
157
|
-
|
|
158
|
-
setup(el) {
|
|
159
|
-
const tween = gsap.from(el, { opacity: 0, duration: 0.4 });
|
|
160
|
-
useUnmount(() => tween.kill());
|
|
161
|
-
}
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
### DOM helpers
|
|
165
|
-
|
|
166
|
-
ルート要素には **`setup(el)`** を、**`[data-ref]`** の子要素には **`useDomRef()`** を使う。
|
|
167
|
-
|
|
168
|
-
| API | 説明 |
|
|
169
|
-
| ------------------------------ | ------------------------------------------------------- |
|
|
170
|
-
| `useDomRef<T>()` | `[data-ref]` 要素への型付きアクセス |
|
|
171
|
-
| `useEvent(el, event, handler)` | イベントリスナーを追加する。unmount 時に自動で除去する |
|
|
172
|
-
| `useSlot()` | 子コンポーネントをマウントする。親の unmount に連動する |
|
|
173
|
-
|
|
174
|
-
### Parent / child
|
|
175
|
-
|
|
176
|
-
`useSlot()` で子コンポーネントをマウントできる。親から子へは `props` または `createContext` / `withContext` で値を渡せる。`addChild()` が返す child context から、子の `setup()` の返り値も参照できる。
|
|
177
|
-
|
|
178
|
-
→ [examples/parent-child](./examples/parent-child/main.ts)
|
|
179
|
-
|
|
180
|
-
### Observers
|
|
181
|
-
|
|
182
|
-
| API | 説明 |
|
|
183
|
-
| --------------------------------- | ----------------------------------------------------------- |
|
|
184
|
-
| `useIntersectionWatch(cb, opts?)` | IntersectionObserver のラッパー。unmount 時に自動で切断する |
|
|
185
|
-
| `useMediaQuery(query, cb)` | query 一致時に callback を実行し、`matchesQuery` を `ReadonlySignal<boolean>` で返す |
|
|
186
|
-
|
|
187
|
-
### Addons
|
|
188
|
-
|
|
189
|
-
```ts
|
|
190
|
-
import { create, defineAddon } from "@usenagi/core";
|
|
191
|
-
import { schedulerAddon } from "@usenagi/core/addons/scheduler";
|
|
192
|
-
|
|
193
|
-
const app = create().install(schedulerAddon(), myAddon());
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
| API | 説明 |
|
|
197
|
-
| --- | --- |
|
|
198
|
-
| `defineAddon({ name, install(ctx) })` | addon を定義する(`ctx` は `AddonContext`) |
|
|
199
|
-
| `app.install(...addons)` | app に addon を登録する(複数可) |
|
|
200
|
-
| `ctx.addMountMiddleware` / `addUnmountMiddleware` / `addComponentMiddleware` | mount / unmount / ComponentSetup の middleware を追加する |
|
|
201
|
-
| `ctx.installedAddons` | この app に install 済みの addon 名 |
|
|
202
|
-
|
|
203
|
-
`addMountMiddleware` / `addUnmountMiddleware` / `addComponentMiddleware` は **後から install した addon ほど外側**に適用される(`install(a, b)` なら実行順は `b → a → コア`)。
|
|
204
|
-
|
|
205
|
-
遅延 mount には `schedulerAddon()` が必要。`when` や `priority` を使う場合も同様で、これらの mount option は scheduler addon が解釈する。addon の状態(scheduler / pending)は **各 app の `install` ごと**に作られる。
|
|
206
|
-
|
|
207
|
-
#### Scheduler + cue
|
|
208
|
-
|
|
209
|
-
```ts
|
|
210
|
-
import { schedulerAddon } from "@usenagi/core/addons/scheduler";
|
|
211
|
-
import { visible, idle, interaction, media } from "@usenagi/core/addons/cue";
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
| API | 説明 |
|
|
215
|
-
| --- | --- |
|
|
216
|
-
| `schedulerAddon(opts?)` | 遅延 mount 用 addon |
|
|
217
|
-
| `visible(opts?)` | 要素が viewport に入ったときに解決する Cue |
|
|
218
|
-
| `idle(timeout?)` | `requestIdleCallback` で解決する Cue |
|
|
219
|
-
| `interaction(events?)` | 最初のユーザー操作で解決する Cue |
|
|
220
|
-
| `media(query)` | media query が一致したときに解決する Cue |
|
|
221
|
-
|
|
222
|
-
---
|
|
223
|
-
|
|
224
|
-
## Comparison
|
|
225
|
-
|
|
226
|
-
| | **nagi** | Alpine.js | Stimulus | petite-vue |
|
|
227
|
-
| -------------------------- | -------- | --------- | -------- | ---------- |
|
|
228
|
-
| Inline JS in HTML | ✗ | ◯ | ✗ | ◯ |
|
|
229
|
-
| Composition-style setup | ◯ | △ | ✗ | ◯ |
|
|
230
|
-
| BYO mounter | ◯ | △ | △ | △ |
|
|
231
|
-
| Async mount cue | ◯ | ✗ | ✗ | ✗ |
|
|
232
|
-
| Lifecycle cleanup | ◯ | △ | ◯ | △ |
|
|
233
|
-
| computed (derived signals) | ◯ | ◯ | ✗ | ◯ |
|
|
234
|
-
| Core gzip | ~2.5 kB | ~16 kB | ~8 kB | ~6 kB |
|
|
235
|
-
|
|
236
|
-
(◯ = 組み込み、△ = 利用側の実装・規約で対応可能、✗ = 主な機能ではない)
|
|
237
|
-
|
|
238
|
-
- **vs Alpine / petite-vue**: HTML に式を直接書かず、ロジックを `.ts` に集約する。
|
|
239
|
-
- **vs Stimulus**: Controller 規約はない。マウント戦略は利用側で自由に組み立てられる。
|
|
240
|
-
- **vs React / Vue**: 宣言的 UI フレームワークではなく、既存 DOM に lifecycle を足す薄いレイヤー。
|
|
241
|
-
|
|
242
|
-
---
|
|
243
|
-
|
|
244
|
-
## When to use / When not to
|
|
245
|
-
|
|
246
|
-
**向いているケース:**
|
|
247
|
-
|
|
248
|
-
- React や Vue のランタイムを持ち込みにくいプロジェクト(CMS、Webflow、WordPress など)
|
|
249
|
-
- GSAP や Lenis を多用する、アニメーション主体のサイト
|
|
250
|
-
- ページの一部だけにインタラクティブな UI を追加したい場合
|
|
251
|
-
- `setup()`、lifecycle、reactivity による composition-style で書きたいが、仮想 DOM は不要な場合
|
|
252
|
-
|
|
253
|
-
**向いていないケース:**
|
|
254
|
-
|
|
255
|
-
- リスト描画や条件分岐を HTML テンプレートで書きたい場合(`v-for` や `v-if` 相当はない)
|
|
256
|
-
- 複雑なオブジェクトの深いリアクティビティが必要な場合(`reactive({})` は提供しない)
|
|
257
|
-
- SSR / hydration が必要な場合
|
|
258
|
-
- 状態管理、ルーティング、宣言的な view rendering をフレームワークにまとめて任せたい場合
|
|
259
|
-
|
|
260
|
-
---
|
|
261
|
-
|
|
262
|
-
## Examples
|
|
263
|
-
|
|
264
|
-
| Example | 説明 |
|
|
265
|
-
| ----------------------------------------------------- | --------------------------------------------------- |
|
|
266
|
-
| [basic-counter](./examples/basic-counter/) | 最小の `signal` + `useWatch` 例 |
|
|
267
|
-
| [computed](./examples/computed/) | `useComputed` による派生値(width × height = area) |
|
|
268
|
-
| [parent-child](./examples/parent-child/) | `createContext` + `withContext` + `useSlot` |
|
|
269
|
-
| [lenis-scroll-scene](./examples/lenis-scroll-scene/) | Lenis + `useComputed` によるスクロール進捗連動 |
|
|
270
|
-
| [byo-mounter recipe](./examples/recipes/byo-mounter/) | `[data-component]` スキャン + manifest + cue |
|
|
271
|
-
|
|
272
|
-
---
|
|
273
|
-
|
|
274
|
-
## License
|
|
275
|
-
|
|
276
|
-
MIT © [hayakawasho](https://github.com/hayakawasho)
|
package/README.md
DELETED
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
**English** | [日本語](./README.ja.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
|
-
**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()`.
|
|
22
|
-
|
|
23
|
-
**Does not restrict mounting strategies**
|
|
24
|
-
|
|
25
|
-
You are free to implement `[data-component]` scanning, manifests, lazy imports, MutationObserver, and so on, on the consuming side.
|
|
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 app = create();
|
|
36
|
-
|
|
37
|
-
app.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, propTypes, signal, useWatch, useDomRef } from "@usenagi/core";
|
|
75
|
-
|
|
76
|
-
const Greeting = defineComponent({
|
|
77
|
-
name: "greeting",
|
|
78
|
-
props: propTypes<{ name: string }>(),
|
|
79
|
-
setup(el, props) {
|
|
80
|
-
const { refs } = useDomRef<{ message: HTMLParagraphElement }>();
|
|
81
|
-
const text = signal(props.name ?? "world");
|
|
82
|
-
|
|
83
|
-
useWatch(text, (v) => {
|
|
84
|
-
refs.message.textContent = `Hello, ${v}!`;
|
|
85
|
-
});
|
|
86
|
-
refs.message.textContent = `Hello, ${text.value}!`;
|
|
87
|
-
},
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
create().component(Greeting)(document.querySelector("#app")!);
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
### Scheduler + deferred mount
|
|
94
|
-
|
|
95
|
-
If delayed mounting is required, add the scheduler / cue addons.
|
|
96
|
-
|
|
97
|
-
```ts
|
|
98
|
-
import { create } from "@usenagi/core";
|
|
99
|
-
import { schedulerAddon } from "@usenagi/core/addons/scheduler";
|
|
100
|
-
import { visible, idle } from "@usenagi/core/addons/cue";
|
|
101
|
-
|
|
102
|
-
const app = create().install(schedulerAddon());
|
|
103
|
-
|
|
104
|
-
// mount when the element enters the viewport
|
|
105
|
-
app.component(HeavyWidget, { when: visible() })(el);
|
|
106
|
-
|
|
107
|
-
// mount during browser idle time
|
|
108
|
-
app.component(Analytics, { when: idle() })(el);
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
With `schedulerAddon()`, `when` is a condition to wait for before `setup()`, and `priority` determines the execution timing of the mount task that includes `setup()`.
|
|
112
|
-
|
|
113
|
-
### BYO mounter recipe
|
|
114
|
-
|
|
115
|
-
An example of automatic mounting by combining `[data-component]` scanning, manifests, and cues.
|
|
116
|
-
→ [examples/recipes/byo-mounter](./examples/recipes/byo-mounter/main.ts)
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
|
120
|
-
## API
|
|
121
|
-
|
|
122
|
-
### Component Definition
|
|
123
|
-
|
|
124
|
-
| API | Description |
|
|
125
|
-
| ---------------------- | --------------------------------------------------------------------------- |
|
|
126
|
-
| `defineComponent(opts)` | Type-safe helper to define a `ComponentSetup` object |
|
|
127
|
-
| `propTypes<T>()` | Type-only marker for declaring component props shape (zero runtime cost) |
|
|
128
|
-
|
|
129
|
-
### Reactivity
|
|
130
|
-
|
|
131
|
-
| API | Description |
|
|
132
|
-
| ---------------------- | ----------------------------------------------------------------- |
|
|
133
|
-
| `signal(value)` | Creates a reactive value container (`.value`) |
|
|
134
|
-
| `readonly(signal)` | Read-only wrapper around a writable `signal` |
|
|
135
|
-
| `useComputed(fn)` | Derived value that auto-tracks `signal` dependencies |
|
|
136
|
-
| `useWatch(target, cb)` | Calls `cb` on value change; automatically unsubscribes on unmount |
|
|
137
|
-
|
|
138
|
-
```ts
|
|
139
|
-
const width = signal(10);
|
|
140
|
-
const height = signal(5);
|
|
141
|
-
const area = useComputed(() => width.value * height.value); // auto-recomputed
|
|
142
|
-
|
|
143
|
-
useWatch(area, (v) => {
|
|
144
|
-
output.textContent = String(v);
|
|
145
|
-
});
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
### Lifecycle
|
|
149
|
-
|
|
150
|
-
| API | Description |
|
|
151
|
-
| ---------------- | ------------------------------------ |
|
|
152
|
-
| `useMount(fn)` | Runs once after the component mounts |
|
|
153
|
-
| `useUnmount(fn)` | Runs on unmount; use for cleanup |
|
|
154
|
-
|
|
155
|
-
```ts
|
|
156
|
-
import gsap from 'gsap';
|
|
157
|
-
|
|
158
|
-
setup(el) {
|
|
159
|
-
const tween = gsap.from(el, { opacity: 0, duration: 0.4 });
|
|
160
|
-
useUnmount(() => tween.kill());
|
|
161
|
-
}
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
### DOM helpers
|
|
165
|
-
|
|
166
|
-
Use **`setup(el)`** for the root element and **`useDomRef()`** for `[data-ref]` descendants.
|
|
167
|
-
|
|
168
|
-
| API | Description |
|
|
169
|
-
| ------------------------------ | -------------------------------------------------------- |
|
|
170
|
-
| `useDomRef<T>()` | Typed access to `[data-ref]` elements |
|
|
171
|
-
| `useEvent(el, event, handler)` | Adds an event listener; automatically removed on unmount |
|
|
172
|
-
| `useSlot()` | Mounts child components; tied to the parent's unmount |
|
|
173
|
-
|
|
174
|
-
### Parent / child
|
|
175
|
-
|
|
176
|
-
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()`.
|
|
177
|
-
|
|
178
|
-
→ [examples/parent-child](./examples/parent-child/main.ts)
|
|
179
|
-
|
|
180
|
-
### Observers
|
|
181
|
-
|
|
182
|
-
| API | Description |
|
|
183
|
-
| --------------------------------- | ------------------------------------------------------------------- |
|
|
184
|
-
| `useIntersectionWatch(cb, opts?)` | IntersectionObserver wrapper; automatically disconnected on unmount |
|
|
185
|
-
| `useMediaQuery(query, cb)` | Runs `callback` when the query matches; returns `matchesQuery` as `ReadonlySignal<boolean>` |
|
|
186
|
-
|
|
187
|
-
### Addons
|
|
188
|
-
|
|
189
|
-
```ts
|
|
190
|
-
import { create, defineAddon } from "@usenagi/core";
|
|
191
|
-
import { schedulerAddon } from "@usenagi/core/addons/scheduler";
|
|
192
|
-
|
|
193
|
-
const app = create().install(schedulerAddon(), myAddon());
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
| API | Description |
|
|
197
|
-
| --- | --- |
|
|
198
|
-
| `defineAddon({ name, install(ctx) })` | Defines an addon (`ctx` is `AddonContext`) |
|
|
199
|
-
| `app.install(...addons)` | Registers one or more addons on the app |
|
|
200
|
-
| `ctx.addMountMiddleware` / `addUnmountMiddleware` / `addComponentMiddleware` | Add mount / unmount / ComponentSetup middleware |
|
|
201
|
-
| `ctx.installedAddons` | Addon names already installed on this app |
|
|
202
|
-
|
|
203
|
-
`addMountMiddleware`, `addUnmountMiddleware`, and `addComponentMiddleware` apply **outermost for addons installed later** (`install(a, b)` runs as `b → a → core`).
|
|
204
|
-
|
|
205
|
-
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.
|
|
206
|
-
|
|
207
|
-
#### Scheduler + cue
|
|
208
|
-
|
|
209
|
-
```ts
|
|
210
|
-
import { schedulerAddon } from "@usenagi/core/addons/scheduler";
|
|
211
|
-
import { visible, idle, interaction, media } from "@usenagi/core/addons/cue";
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
| API | Description |
|
|
215
|
-
| --- | --- |
|
|
216
|
-
| `schedulerAddon(opts?)` | Addon for deferred mount |
|
|
217
|
-
| `visible(opts?)` | A Cue that resolves when the element enters the viewport |
|
|
218
|
-
| `idle(timeout?)` | A Cue that resolves via `requestIdleCallback` |
|
|
219
|
-
| `interaction(events?)` | A Cue that resolves on the first user interaction |
|
|
220
|
-
| `media(query)` | A Cue that resolves when the media query matches |
|
|
221
|
-
|
|
222
|
-
---
|
|
223
|
-
|
|
224
|
-
## Comparison
|
|
225
|
-
|
|
226
|
-
| | **nagi** | Alpine.js | Stimulus | petite-vue |
|
|
227
|
-
| -------------------------- | -------- | --------- | -------- | ---------- |
|
|
228
|
-
| Inline JS in HTML | ✗ | ◯ | ✗ | ◯ |
|
|
229
|
-
| Composition-style setup | ◯ | △ | ✗ | ◯ |
|
|
230
|
-
| BYO mounter | ◯ | △ | △ | △ |
|
|
231
|
-
| Async mount cue | ◯ | ✗ | ✗ | ✗ |
|
|
232
|
-
| Lifecycle cleanup | ◯ | △ | ◯ | △ |
|
|
233
|
-
| computed (derived signals) | ◯ | ◯ | ✗ | ◯ |
|
|
234
|
-
| Core gzip | ~2.5 kB | ~16 kB | ~8 kB | ~6 kB |
|
|
235
|
-
|
|
236
|
-
(◯ = built-in, △ = handled via userland/convention, ✗ = not a primary feature)
|
|
237
|
-
|
|
238
|
-
- **vs Alpine / petite-vue**: Instead of writing logic expressions directly in HTML, you centralize your logic in `.ts` files.
|
|
239
|
-
- **vs Stimulus**: No controller conventions; you are free to implement your own mounting strategy.
|
|
240
|
-
- **vs React / Vue**: It is not a declarative UI framework, but rather a thin layer that adds lifecycle hooks to existing DOM.
|
|
241
|
-
|
|
242
|
-
---
|
|
243
|
-
|
|
244
|
-
## When to use / When not to
|
|
245
|
-
|
|
246
|
-
**Recommended Use Cases:**
|
|
247
|
-
|
|
248
|
-
- Projects where you cannot justify the runtime overhead of React or Vue (e.g., CMS, Webflow, WordPress).
|
|
249
|
-
- Animation-heavy sites that rely heavily on libraries like GSAP or Lenis.
|
|
250
|
-
- Scenarios where you only need to add interactive UI to specific parts of a page.
|
|
251
|
-
- When you want to use a composition-style approach with `setup()`, lifecycle hooks, and reactivity, but do not require a virtual DOM.
|
|
252
|
-
|
|
253
|
-
**Not Recommended For:**
|
|
254
|
-
|
|
255
|
-
- When you want to handle list rendering or conditional logic via HTML templates (it does not support equivalents to `v-for` or `v-if`).
|
|
256
|
-
- When you need deep reactivity for complex objects (it does not provide `reactive({})`).
|
|
257
|
-
- When SSR/hydration is required.
|
|
258
|
-
- When you want a full-featured framework to handle global state management, routing, and declarative view rendering.
|
|
259
|
-
|
|
260
|
-
---
|
|
261
|
-
|
|
262
|
-
## Examples
|
|
263
|
-
|
|
264
|
-
| Example | Description |
|
|
265
|
-
| ----------------------------------------------------- | -------------------------------------------------------- |
|
|
266
|
-
| [basic-counter](./examples/basic-counter/) | Minimal `signal` + `useWatch` example |
|
|
267
|
-
| [computed](./examples/computed/) | Derived value with `useComputed` (width × height = area) |
|
|
268
|
-
| [parent-child](./examples/parent-child/) | `createContext` + `withContext` + `useSlot` |
|
|
269
|
-
| [lenis-scroll-scene](./examples/lenis-scroll-scene/) | Scroll-progress animation with Lenis + `useComputed` |
|
|
270
|
-
| [byo-mounter recipe](./examples/recipes/byo-mounter/) | `[data-component]` scanning + manifest + cue |
|
|
271
|
-
|
|
272
|
-
---
|
|
273
|
-
|
|
274
|
-
## License
|
|
275
|
-
|
|
276
|
-
MIT © [hayakawasho](https://github.com/hayakawasho)
|