@usenagi/core 0.1.0 → 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/README.ja.md ADDED
@@ -0,0 +1,248 @@
1
+ [English](./README.md) | **日本語**
2
+
3
+ # nagi
4
+
5
+ **Composition-style ergonomics for vanilla DOM. Bring your own mounter.**
6
+
7
+ [![npm](https://img.shields.io/npm/v/@usenagi/core)](https://www.npmjs.com/package/@usenagi/core)
8
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/@usenagi/core)](https://bundlephobia.com/package/@usenagi/core)
9
+ [![license](https://img.shields.io/npm/l/@usenagi/core)](./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 { createScheduler } from "@usenagi/core/addons/scheduler";
99
+ import { visible, idle } from "@usenagi/core/addons/cue";
100
+
101
+ const app = create({ scheduler: createScheduler() });
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
+ `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 { createScheduler } from "@usenagi/core/addons/scheduler";
183
+ import { visible, idle, interaction, media } from "@usenagi/core/addons/cue";
184
+ ```
185
+
186
+ | API | 説明 |
187
+ | ------------------------ | ------------------------------------------------------------------------------------- |
188
+ | `createScheduler(opts?)` | `schedule(task, { priority, signal })` を実装した Scheduler を返す |
189
+ | `visible(opts?)` | 要素が viewport に入ったときに解決する Cue |
190
+ | `idle(timeout?)` | `requestIdleCallback` で解決する Cue |
191
+ | `interaction(events?)` | 最初のユーザー操作で解決する Cue |
192
+ | `media(query)` | media query が一致したときに解決する Cue |
193
+
194
+ ---
195
+
196
+ ## Comparison
197
+
198
+ | | **nagi** | Alpine.js | Stimulus | petite-vue |
199
+ | ----------------------- | ----------- | --------- | -------- | ---------- |
200
+ | Inline JS in HTML | ✗ | ◯ | ✗ | ◯ |
201
+ | Composition-style setup | ◯ | △ | ✗ | ◯ |
202
+ | BYO mounter | ◯ | △ | △ | △ |
203
+ | Async mount cue | ◯ | ✗ | ✗ | ✗ |
204
+ | Lifecycle cleanup | ◯ | △ | ◯ | △ |
205
+ | `useComputed` (derived signals) | ◯ | ◯ | ✗ | ◯ |
206
+ | Core gzip | ~2.5 kB | ~16 kB | ~8 kB | ~6 kB |
207
+
208
+ (◯ = 組み込み、△ = 利用側の実装・規約で対応可能、✗ = 主な機能ではない)
209
+
210
+ - **vs Alpine / petite-vue**: HTML に式を直接書かず、ロジックを `.ts` に集約する。
211
+ - **vs Stimulus**: Controller 規約はない。マウント戦略は利用側で自由に組み立てられる。
212
+ - **vs React / Vue**: 宣言的 UI フレームワークではなく、既存 DOM に lifecycle を足す薄いレイヤー。
213
+
214
+ ---
215
+
216
+ ## When to use / When not to
217
+
218
+ **向いているケース:**
219
+
220
+ - React や Vue のランタイムを持ち込みにくいプロジェクト(CMS、Webflow、WordPress など)
221
+ - GSAP や Lenis を多用する、アニメーション主体のサイト
222
+ - ページの一部だけにインタラクティブな UI を追加したい場合
223
+ - `setup()`、lifecycle、reactivity による composition-style で書きたいが、仮想 DOM は不要な場合
224
+
225
+ **向いていないケース:**
226
+
227
+ - リスト描画や条件分岐を HTML テンプレートで書きたい場合(`v-for` や `v-if` 相当はない)
228
+ - 複雑なオブジェクトの深いリアクティビティが必要な場合(`reactive({})` は提供しない)
229
+ - SSR / hydration が必要な場合
230
+ - 状態管理、ルーティング、宣言的な view rendering をフレームワークにまとめて任せたい場合
231
+
232
+ ---
233
+
234
+ ## Examples
235
+
236
+ | Example | 説明 |
237
+ | ----------------------------------------------------- | ----------------------------------------------------------------- |
238
+ | [basic-counter](./examples/basic-counter/) | 最小の `signal` + `useWatch` 例 |
239
+ | [computed](./examples/computed/) | `useComputed` による派生値(width × height = area) |
240
+ | [parent-child](./examples/parent-child/) | `createContext` + `withContext` + `useSlot` |
241
+ | [lenis-scroll-scene](./examples/lenis-scroll-scene/) | Lenis + `useComputed` によるスクロール進捗連動 |
242
+ | [byo-mounter recipe](./examples/recipes/byo-mounter/) | `[data-component]` スキャン + manifest + cue |
243
+
244
+ ---
245
+
246
+ ## License
247
+
248
+ 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
- **既存 HTML に小さく足せる**
14
- WordPress、CMS、Webflow、静的サイトなどに、仮想 DOM やテンプレートを持ち込まず `setup()` / lifecycle / reactivity を追加できる。
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**
15
20
 
16
- **アニメーションと相性が良い**
17
- GSAP、Lenis、IntersectionObserver などを `setup()` で初期化し、`useUnmount()` でクリーンアップできる。
21
+ You can initialize GSAP, Lenis, IntersectionObserver, etc., in `setup()` and clean them up with `useUnmount()`.
18
22
 
19
- **マウント戦略を縛らない**
20
- `[data-component]` スキャン、manifest、lazy import、MutationObserver などは利用側で自由に組める。
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.
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, ref, useWatch, useDomRef } from "@usenagi/core";
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 = ref(0);
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, ref, useWatch, useDomRef } from "@usenagi/core";
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 = ref((props.name as string) ?? "world");
80
+ const text = signal((props.name as string) ?? "world");
76
81
 
77
82
  useWatch(text, (v) => {
78
83
  refs.message.textContent = `Hello, ${v}!`;
@@ -86,7 +91,7 @@ create().component(Greeting)(document.querySelector("#app")!);
86
91
 
87
92
  ### Scheduler + deferred mount
88
93
 
89
- 遅延マウントが必要なら scheduler / cue addons を追加する。
94
+ If delayed mounting is required, add the scheduler / cue addons.
90
95
 
91
96
  ```ts
92
97
  import { create } from "@usenagi/core";
@@ -95,18 +100,18 @@ import { visible, idle } from "@usenagi/core/addons/cue";
95
100
 
96
101
  const app = create({ scheduler: createScheduler() });
97
102
 
98
- // Intersection visible になってからマウント
103
+ // mount when the element enters the viewport
99
104
  app.component(HeavyWidget, { when: visible() })(el);
100
105
 
101
- // requestIdleCallback でマウント
106
+ // mount during browser idle time
102
107
  app.component(Analytics, { when: idle() })(el);
103
108
  ```
104
109
 
105
- `when` `setup()` の前に待機する条件、`priority` `setup()` を含む mount task の実行タイミングを決める。
110
+ `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]` スキャン、manifest、cue を組み合わせて自動マウントする一例。
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
- | `ref(value)` | リアクティブな参照を作成 |
121
- | `readonly(ref)` | 読み取り専用ラッパー |
122
- | `computed(fn)` | 依存する `ref` を自動追跡する派生値 |
123
- | `useWatch(ref, cb)` | 値変更時にコールバック。unmount 時に自動解除 |
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 = ref(10);
127
- const height = ref(5);
128
- const area = computed(() => width.value * height.value); // 自動再計算
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)` | マウント完了後に1回実行 |
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,25 +155,26 @@ setup(el) {
150
155
 
151
156
  ### DOM helpers
152
157
 
153
- | API | 説明 |
154
- | ------------------------------ | ----------------------------------------------- |
155
- | `useDomRef<T>()` | `[data-ref]` 要素を型付きで取得 |
156
- | `useRootRef()` | ルート要素を取得 |
157
- | `useEvent(el, event, handler)` | イベントリスナーを登録。unmount 時に自動除去 |
158
- | `useSlot()` | 子コンポーネントをマウント。親の unmount に連動 |
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()` で子コンポーネントをマウントできる。親から子へは `props` または `createContext` / `withContext` で値を渡せる。`addChild()` が返す child context から、子の `setup()` 返り値も参照できる。
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?)` | IntersectionObserverunmount 時に自動解除 |
171
- | `useMediaQuery(query)` | `matchMedia` の結果を `ReadonlyRef<boolean>` で返す |
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
 
@@ -177,13 +183,13 @@ import { createScheduler } from "@usenagi/core/addons/scheduler";
177
183
  import { visible, idle, interaction, media } from "@usenagi/core/addons/cue";
178
184
  ```
179
185
 
180
- | API | 説明 |
181
- | ------------------------ | ---------------------------------------------------------------------------- |
182
- | `createScheduler(opts?)` | `scheduler.schedule(task, { priority, signal })` を実装した Scheduler を返す |
183
- | `visible(opts?)` | 要素が viewport に入ったら解決する Cue |
184
- | `idle(timeout?)` | `requestIdleCallback` で解決する Cue |
185
- | `interaction(events?)` | 最初のユーザー操作で解決する Cue |
186
- | `media(query)` | media query が一致したら解決する Cue |
186
+ | API | Description |
187
+ | ------------------------ | --------------------------------------------------------------------- |
188
+ | `createScheduler(opts?)` | Returns a Scheduler implementing `schedule(task, { priority, signal })` |
189
+ | `visible(opts?)` | A Cue that resolves when the element enters the viewport |
190
+ | `idle(timeout?)` | A Cue that resolves via `requestIdleCallback` |
191
+ | `interaction(events?)` | A Cue that resolves on the first user interaction |
192
+ | `media(query)` | A Cue that resolves when the media query matches |
187
193
 
188
194
  ---
189
195
 
@@ -196,44 +202,44 @@ import { visible, idle, interaction, media } from "@usenagi/core/addons/cue";
196
202
  | BYO mounter | ◯ | △ | △ | △ |
197
203
  | Async mount cue | ◯ | ✗ | ✗ | ✗ |
198
204
  | Lifecycle cleanup | ◯ | △ | ◯ | △ |
199
- | `computed` | ◯ | ◯ | ✗ | ◯ |
200
- | Core gzip | ~2.6-2.9 kB | ~16 kB | ~8 kB | ~6 kB |
205
+ | `useComputed` (derived signals) | ◯ | ◯ | ✗ | ◯ |
206
+ | Core gzip | ~2.5 kB | ~16 kB | ~8 kB | ~6 kB |
201
207
 
202
- ◯ = built-in、△ = userland / convention で対応可能、✗ = primary feature ではない。
208
+ (◯ = built-in, = handled via userland/convention, = not a primary feature)
203
209
 
204
- - **vs Alpine / petite-vue**: HTML に式を書かず、ロジックを `.ts` に寄せる。
205
- - **vs Stimulus**: Controller 規約なし。mounter は利用側で自由に組める。
206
- - **vs React / Vue**: 宣言的 UI フレームワークではなく、既存 DOM lifecycle を足す layer。
210
+ - **vs Alpine / petite-vue**: Instead of writing logic expressions directly in HTML, you centralize your logic in `.ts` files.
211
+ - **vs Stimulus**: No controller conventions; you are free to implement your own mounting strategy.
212
+ - **vs React / Vue**: It is not a declarative UI framework, but rather a thin layer that adds lifecycle hooks to existing DOM.
207
213
 
208
214
  ---
209
215
 
210
216
  ## When to use / When not to
211
217
 
212
- **向いているケース**:
218
+ **Recommended Use Cases:**
213
219
 
214
- - React/Vue ほどのランタイムを持ち込みにくいプロジェクト(CMSWebflowWordPress 等)
215
- - GSAP/Lenis を多用する、アニメーション主体のサイト
216
- - ページの一部だけに interactive UI を追加したい
217
- - Composition-style `setup()` / lifecycle / reactivity で書きたいが、仮想 DOM は不要
220
+ - Projects where you cannot justify the runtime overhead of React or Vue (e.g., CMS, Webflow, WordPress).
221
+ - Animation-heavy sites that rely heavily on libraries like GSAP or Lenis.
222
+ - Scenarios where you only need to add interactive UI to specific parts of a page.
223
+ - When you want to use a composition-style approach with `setup()`, lifecycle hooks, and reactivity, but do not require a virtual DOM.
218
224
 
219
- **向いていないケース**:
225
+ **Not Recommended For:**
220
226
 
221
- - リスト描画や条件分岐を HTML テンプレートで書きたい(`v-for`, `v-if` 相当は持たない)
222
- - 深いオブジェクトのリアクティビティが必要(`reactive({})` は提供しない)
223
- - SSR/hydration が必要
224
- - アプリ全体の状態管理、ルーティング、宣言的な view rendering をまとめて任せたい
227
+ - When you want to handle list rendering or conditional logic via HTML templates (it does not support equivalents to `v-for` or `v-if`).
228
+ - When you need deep reactivity for complex objects (it does not provide `reactive({})`).
229
+ - When SSR/hydration is required.
230
+ - When you want a full-featured framework to handle global state management, routing, and declarative view rendering.
225
231
 
226
232
  ---
227
233
 
228
234
  ## Examples
229
235
 
230
- | Example | 説明 |
231
- | ----------------------------------------------------- | -------------------------------------------- |
232
- | [basic-counter](./examples/basic-counter/) | `ref` + `useWatch` の最小例 |
233
- | [computed](./examples/computed/) | `computed` で派生値 (width × height = area) |
234
- | [parent-child](./examples/parent-child/) | `createContext` + `withContext` + `useSlot` |
235
- | [lenis-scroll-scene](./examples/lenis-scroll-scene/) | Lenis + `computed` でスクロール進捗連動 |
236
- | [byo-mounter recipe](./examples/recipes/byo-mounter/) | `[data-component]` スキャン + manifest + cue |
236
+ | Example | Description |
237
+ | ----------------------------------------------------- | ------------------------------------------------------ |
238
+ | [basic-counter](./examples/basic-counter/) | Minimal `signal` + `useWatch` example |
239
+ | [computed](./examples/computed/) | Derived value with `useComputed` (width × height = area) |
240
+ | [parent-child](./examples/parent-child/) | `createContext` + `withContext` + `useSlot` |
241
+ | [lenis-scroll-scene](./examples/lenis-scroll-scene/) | Scroll-progress animation with Lenis + `useComputed` |
242
+ | [byo-mounter recipe](./examples/recipes/byo-mounter/) | `[data-component]` scanning + manifest + cue |
237
243
 
238
244
  ---
239
245
 
package/dist/main.es.js CHANGED
@@ -346,13 +346,8 @@ function F(e, t) {
346
346
  })), { matchesQuery: C(r) };
347
347
  }
348
348
  //#endregion
349
- //#region lib/hooks/useRootRef.ts
350
- function I() {
351
- return f("useRootRef").element;
352
- }
353
- //#endregion
354
349
  //#region lib/hooks/useSlot.ts
355
- function L() {
350
+ function I() {
356
351
  let e = f("useSlot");
357
352
  return {
358
353
  addChild(t, n, r = {}) {
@@ -374,4 +369,4 @@ function L() {
374
369
  };
375
370
  }
376
371
  //#endregion
377
- export { r as LifecycleError, E as computed, m as create, D as createContext, u as defineComponent, i as isLifecycleError, C as readonly, x as ref, M as useDomRef, N as useEvent, P as useIntersectionWatch, F as useMediaQuery, g as useMount, I as useRootRef, L as useSlot, _ as useUnmount, T as useWatch, O as withContext };
372
+ export { r as LifecycleError, m as create, D as createContext, u as defineComponent, i as isLifecycleError, 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(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){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===void 0?e=>t=>({name:e.name,setup(n){return e.setup(n,t)}}):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(e={}){let{scheduler:r}=e,i=n();return{component(e,{priority:n,when:a}={}){return(o,c={})=>{function l(){let t=m(e,o,c);return s(o,t),t.onMount(),t}if(!r)return l();let u=i.add(o),d=()=>{r.schedule(()=>{u.complete()&&l()},{priority:n,signal:u.signal})};a?a(o,u.signal).then(()=>{u.signal.aborted||d()},e=>{t(e)||(u.abort(),queueMicrotask(()=>{throw e}))}):d()}},unmount(e){for(let t of e){i.abort(t);let e=o.get(t);e&&(e.onUnmount(),o.delete(t))}}}}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(){return p(`useRootRef`).element}function R(){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))}})}}}e.LifecycleError=i,e.computed=D,e.create=h,e.createContext=O,e.defineComponent=d,e.isLifecycleError=a,e.readonly=w,e.ref=S,e.useDomRef=N,e.useEvent=P,e.useIntersectionWatch=F,e.useMediaQuery=I,e.useMount=_,e.useRootRef=L,e.useSlot=R,e.useUnmount=v,e.useWatch=E,e.withContext=k});
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(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){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===void 0?e=>t=>({name:e.name,setup(n){return e.setup(n,t)}}):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(e={}){let{scheduler:r}=e,i=n();return{component(e,{priority:n,when:a}={}){return(o,c={})=>{function l(){let t=m(e,o,c);return s(o,t),t.onMount(),t}if(!r)return l();let u=i.add(o),d=()=>{r.schedule(()=>{u.complete()&&l()},{priority:n,signal:u.signal})};a?a(o,u.signal).then(()=>{u.signal.aborted||d()},e=>{t(e)||(u.abort(),queueMicrotask(()=>{throw e}))}):d()}},unmount(e){for(let t of e){i.abort(t);let e=o.get(t);e&&(e.onUnmount(),o.delete(t))}}}}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))}})}}}e.LifecycleError=i,e.create=h,e.createContext=O,e.defineComponent=d,e.isLifecycleError=a,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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@usenagi/core",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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",
@@ -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 Ref<T> {
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 ref: <T = any>(val: T) => Ref<T>;
12
- declare class ReadonlyRef<T> {
11
+ declare const signal: <T = any>(val: T) => Signal<T>;
12
+ declare class ReadonlySignal<T> {
13
13
  #private;
14
- constructor(value: Ref<T>);
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>(ref: Ref<T>) => ReadonlyRef<T>;
19
- declare function useWatch<T>(ref: Ref<T> | ReadonlyRef<T>, callback: WatchCallback<T>): void;
20
- declare function computed<T>(getter: () => T): ReadonlyRef<T>;
21
- export { computed, readonly, ref, useWatch };
22
- export type { ReadonlyRef, Ref };
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 };
@@ -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").ReadonlyRef<boolean>;
3
+ readonly matchesQuery: import("../main").ReadonlySignal<boolean>;
4
4
  };
package/types/main.d.ts CHANGED
@@ -2,16 +2,15 @@ export { create } from "./core/app";
2
2
  export { defineComponent } from "./core/component";
3
3
  export { isLifecycleError, LifecycleError } from "./core/error";
4
4
  export { useMount, useUnmount } from "./core/lifecycle";
5
- export { computed, readonly, ref, useWatch } from "./core/reactivity";
5
+ export { readonly, signal, useComputed, useWatch } from "./core/reactivity";
6
6
  export { createContext, withContext } from "./hooks/createContext";
7
7
  export { useDomRef } from "./hooks/useDomRef";
8
8
  export { useEvent } from "./hooks/useEvent";
9
9
  export { useIntersectionWatch } from "./hooks/useIntersectionWatch";
10
10
  export { useMediaQuery } from "./hooks/useMediaQuery";
11
- export { useRootRef } from "./hooks/useRootRef";
12
11
  export { useSlot } from "./hooks/useSlot";
13
12
  export type { ComponentContext } from "./core/component";
14
13
  export type { LifecycleErrorDetails } from "./core/error";
15
- export type { ReadonlyRef, Ref } from "./core/reactivity";
14
+ export type { ReadonlySignal, Signal } from "./core/reactivity";
16
15
  export type { Provider } from "./hooks/createContext";
17
16
  export type { ComponentSetup, Cue, IComponent, RefElement, SchedulePriority, Scheduler, } from "./types";
@@ -1,2 +0,0 @@
1
- import type { RefElement } from "../types";
2
- export declare function useRootRef<T extends RefElement = RefElement>(): T;